diff --git a/.github/workflows/compliance-close.yml b/.github/workflows/compliance-close.yml index 14e68701e5..297fe786af 100644 --- a/.github/workflows/compliance-close.yml +++ b/.github/workflows/compliance-close.yml @@ -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, diff --git a/.github/workflows/duplicate-issues.yml b/.github/workflows/duplicate-issues.yml index 4648a2d0c3..3f0ce976e1 100644 --- a/.github/workflows/duplicate-issues.yml +++ b/.github/workflows/duplicate-issues.yml @@ -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 diff --git a/.github/workflows/pr-management.yml b/.github/workflows/pr-management.yml index b6aa4e589d..5d526ceaf2 100644 --- a/.github/workflows/pr-management.yml +++ b/.github/workflows/pr-management.yml @@ -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 diff --git a/.github/workflows/pr-standards.yml b/.github/workflows/pr-standards.yml index 06838089d3..6e8bfe25c6 100644 --- a/.github/workflows/pr-standards.yml +++ b/.github/workflows/pr-standards.yml @@ -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; } diff --git a/script/github/close-issues.ts b/script/github/close-issues.ts index 9e1f597951..2dbf349eb9 100755 --- a/script/github/close-issues.ts +++ b/script/github/close-issues.ts @@ -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`) diff --git a/script/github/close-prs.ts b/script/github/close-prs.ts index 0dd8953d90..d74cfa0cc1 100755 --- a/script/github/close-prs.ts +++ b/script/github/close-prs.ts @@ -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("/")