fix: exempt team members from compliance cleanup

This commit is contained in:
Aiden Cline 2026-05-21 16:15:45 -05:00 committed by GitHub
parent 39e7ff932d
commit 1268f8657e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 56 additions and 31 deletions

View file

@ -34,11 +34,25 @@ jobs:
const now = Date.now();
const twoHours = 2 * 60 * 60 * 1000;
const teamAssociations = ['OWNER', 'MEMBER', 'COLLABORATOR'];
for (const item of items) {
const isPR = !!item.pull_request;
const kind = isPR ? 'PR' : 'issue';
if (teamAssociations.includes(item.author_association)) {
core.info(`Skipping ${kind} #${item.number}: author association is ${item.author_association}`);
try {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: item.number,
name: 'needs:compliance',
});
} catch (e) {}
continue;
}
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,

View file

@ -6,7 +6,7 @@ on:
jobs:
check-duplicates:
if: github.event.action == 'opened'
if: github.event.action == 'opened' && !contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.issue.author_association)
runs-on: blacksmith-4vcpu-ubuntu-2404
permissions:
contents: read
@ -118,7 +118,7 @@ jobs:
Remember: post at most ONE comment combining all findings. If everything is fine, post nothing."
recheck-compliance:
if: github.event.action == 'edited' && contains(github.event.issue.labels.*.name, 'needs:compliance')
if: github.event.action == 'edited' && contains(github.event.issue.labels.*.name, 'needs:compliance') && !contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.issue.author_association)
runs-on: blacksmith-4vcpu-ubuntu-2404
permissions:
contents: read

View file

@ -11,22 +11,25 @@ jobs:
contents: read
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 1
- name: Check team membership
id: team-check
run: |
LOGIN="${{ github.event.pull_request.user.login }}"
if [ "$LOGIN" = "opencode-agent[bot]" ] || grep -qxF "$LOGIN" .github/TEAM_MEMBERS; then
ASSOCIATION="${{ github.event.pull_request.author_association }}"
if [ "$LOGIN" = "opencode-agent[bot]" ] || [ "$ASSOCIATION" = "OWNER" ] || [ "$ASSOCIATION" = "MEMBER" ] || [ "$ASSOCIATION" = "COLLABORATOR" ]; then
echo "is_team=true" >> "$GITHUB_OUTPUT"
echo "Skipping: $LOGIN is a team member or bot"
else
echo "is_team=false" >> "$GITHUB_OUTPUT"
fi
- name: Checkout repository
if: steps.team-check.outputs.is_team != 'true'
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 1
ref: ${{ github.event.pull_request.base.sha }}
- name: Setup Bun
if: steps.team-check.outputs.is_team != 'true'
uses: ./.github/actions/setup-bun

View file

@ -28,15 +28,9 @@ jobs:
// Check if author is a team member or bot
if (login === 'opencode-agent[bot]') return;
const { data: file } = await github.rest.repos.getContent({
owner: context.repo.owner,
repo: context.repo.repo,
path: '.github/TEAM_MEMBERS',
ref: 'dev'
});
const members = Buffer.from(file.content, 'base64').toString().split('\n').map(l => l.trim()).filter(Boolean);
if (members.includes(login)) {
console.log(`Skipping: ${login} is a team member`);
const teamAssociations = ['OWNER', 'MEMBER', 'COLLABORATOR'];
if (teamAssociations.includes(pr.author_association)) {
console.log(`Skipping: ${login} has author association ${pr.author_association}`);
return;
}
@ -175,15 +169,9 @@ jobs:
// Check if author is a team member or bot
if (login === 'opencode-agent[bot]') return;
const { data: file } = await github.rest.repos.getContent({
owner: context.repo.owner,
repo: context.repo.repo,
path: '.github/TEAM_MEMBERS',
ref: 'dev'
});
const members = Buffer.from(file.content, 'base64').toString().split('\n').map(l => l.trim()).filter(Boolean);
if (members.includes(login)) {
console.log(`Skipping: ${login} is a team member`);
const teamAssociations = ['OWNER', 'MEMBER', 'COLLABORATOR'];
if (teamAssociations.includes(pr.author_association)) {
console.log(`Skipping: ${login} has author association ${pr.author_association}`);
return;
}

View file

@ -15,8 +15,11 @@ const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000)
type Issue = {
number: number
updated_at: string
author_association: string
}
const teamAssociations = new Set(["OWNER", "MEMBER", "COLLABORATOR"])
const headers = {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
@ -63,6 +66,10 @@ async function main() {
for (const i of all) {
const updated = new Date(i.updated_at)
if (updated < cutoff) {
if (teamAssociations.has(i.author_association)) {
console.log(`Skipping #${i.number}: author association is ${i.author_association}`)
continue
}
stale.push(i.number)
} else {
console.log(`\nFound fresh issue #${i.number}, stopping`)

View file

@ -69,6 +69,7 @@ const maxClose =
const sleepMs = requireNonNegativeInteger("sleep-ms", values["sleep-ms"])
const printLimit = requireNonNegativeInteger("print-limit", values["print-limit"])
const cutoff = subtractMonths(new Date(), ageMonths)
const teamAssociations = new Set(["OWNER", "MEMBER", "COLLABORATOR"])
const headers = {
Authorization: `Bearer ${token}`,
@ -82,6 +83,7 @@ type PullRequest = {
title: string
url: string
createdAt: string
authorAssociation: string
reactionGroups: Array<{
content: string
users: {
@ -145,19 +147,25 @@ async function main() {
console.log(`Threshold: fewer than ${threshold} positive reactions`)
const prs = await fetchOpenPullRequests()
const recentCount = prs.filter((pr) => new Date(pr.createdAt) >= cutoff).length
const matching = prs
.map((pr) => ({ ...pr, positiveReactions: positiveReactionCount(pr) }))
.filter((pr) => new Date(pr.createdAt) < cutoff && pr.positiveReactions < threshold)
const scored = prs.map((pr) => ({ ...pr, positiveReactions: positiveReactionCount(pr) }))
const recentCount = scored.filter((pr) => new Date(pr.createdAt) >= cutoff).length
const teamCount = scored.filter((pr) => isTeamMember(pr)).length
const matching = scored.filter(
(pr) => !isTeamMember(pr) && new Date(pr.createdAt) < cutoff && pr.positiveReactions < threshold,
)
const candidates = matching.filter((pr) => !hasPriorCleanup(pr))
const selected = maxClose === undefined ? candidates : candidates.slice(0, maxClose)
console.log(`Fetched ${prs.length} open PRs`)
console.log(`Matching cleanup criteria: ${matching.length}`)
console.log(`Skipped previously cleaned PRs: ${matching.length - candidates.length}`)
console.log(`Team member PRs untouched: ${teamCount}`)
console.log(`Recent PRs untouched: ${recentCount}`)
console.log(
`Older PRs with at least ${threshold} positive reactions untouched: ${prs.length - matching.length - recentCount}`,
`Older PRs with at least ${threshold} positive reactions untouched: ${
scored.filter((pr) => !isTeamMember(pr) && new Date(pr.createdAt) < cutoff && pr.positiveReactions >= threshold)
.length
}`,
)
if (selected.length === 0) return
@ -205,6 +213,7 @@ async function fetchOpenPullRequests() {
title
url
createdAt
authorAssociation
reactionGroups {
content
users {
@ -335,6 +344,10 @@ function hasPriorCleanup(pr: PullRequest) {
return pr.labels.nodes.some((label) => label.name === cleanupLabel)
}
function isTeamMember(pr: PullRequest) {
return teamAssociations.has(pr.authorAssociation)
}
function requireRepo(value: string | undefined) {
if (!value) throw new Error("repo is required")
const [owner, name] = value.split("/")