From dfe188470271ce1ffa509edbda80e48599b9a584 Mon Sep 17 00:00:00 2001 From: Nightt <87569709+nightt5879@users.noreply.github.com> Date: Tue, 2 Jun 2026 11:17:33 +0800 Subject: [PATCH] fix: add contribution gate dry run mode --- .github/APPROVED_CONTRIBUTORS | 3 ++- .github/workflows/approve-contributor.yml | 30 ++++++++++++++++++++- .github/workflows/issue-gate.yml | 21 +++++++++++++-- .github/workflows/pr-gate.yml | 21 +++++++++++++-- CONTRIBUTING.md | 33 ++++++++++++++++------- 5 files changed, 92 insertions(+), 16 deletions(-) diff --git a/.github/APPROVED_CONTRIBUTORS b/.github/APPROVED_CONTRIBUTORS index 10eae33f..23e1a5d4 100644 --- a/.github/APPROVED_CONTRIBUTORS +++ b/.github/APPROVED_CONTRIBUTORS @@ -2,9 +2,10 @@ # # Maintainers and collaborators bypass the gate automatically. Use this file # for external contributors who are allowed through the automated front door. +# Seed active contributors here before switching the gate workflows to enforce mode. # # Supported entries: # pr:username # issue:username # all:username -all:Hmbown +all:hmbown diff --git a/.github/workflows/approve-contributor.yml b/.github/workflows/approve-contributor.yml index 6c773751..5ded3965 100644 --- a/.github/workflows/approve-contributor.yml +++ b/.github/workflows/approve-contributor.yml @@ -9,6 +9,10 @@ permissions: issues: write pull-requests: write +concurrency: + group: contribution-gate-approval + cancel-in-progress: false + jobs: approve: runs-on: ubuntu-latest @@ -56,12 +60,14 @@ jobs: const targetLogin = issue.user.login; const normalizedLogin = targetLogin.toLowerCase(); const entry = `${scope}:${normalizedLogin}`; + const branchSlug = normalizedLogin.replace(/[^a-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '') || 'contributor'; const defaultContent = [ '# Scoped contribution-gate allowlist.', '#', '# Maintainers and collaborators bypass the gate automatically. Use this file', '# for external contributors who are allowed through the automated front door.', + '# Seed active contributors here before switching the gate workflows to enforce mode.', '#', '# Supported entries:', '# pr:username', @@ -119,6 +125,29 @@ jobs: return; } + const { data: openPrs } = await github.rest.pulls.list({ + owner, + repo, + state: 'open', + per_page: 100, + }); + const repoFullName = `${owner}/${repo}`.toLowerCase(); + const pendingPr = openPrs.find(openPr => { + const sameRepo = (openPr.head?.repo?.full_name || '').toLowerCase() === repoFullName; + const body = openPr.body || ''; + return sameRepo && body.includes(`Adds \`${entry}\` to \`${path}\`.`); + }); + + if (pendingPr) { + await github.rest.issues.createComment({ + owner, + repo, + issue_number: issue.number, + body: `@${targetLogin} already has a pending allowlist update PR for ${scope} contributions: ${pendingPr.html_url}`, + }); + return; + } + const nextContent = `${content.trimEnd()}\n${entry}\n`; const { data: blob } = await github.rest.git.createBlob({ owner, @@ -140,7 +169,6 @@ jobs: ], }); - const branchSlug = normalizedLogin.replace(/[^a-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '') || 'contributor'; const branchName = `contribution-gate/${scope}-${branchSlug}-${Date.now()}`; await github.rest.git.createRef({ owner, diff --git a/.github/workflows/issue-gate.yml b/.github/workflows/issue-gate.yml index e19921ec..70bab864 100644 --- a/.github/workflows/issue-gate.yml +++ b/.github/workflows/issue-gate.yml @@ -8,11 +8,16 @@ permissions: contents: read issues: write +env: + # Keep new gates observable first. Switch to "enforce" only after maintainers + # have seeded active contributors and reviewed the dry-run signal. + CONTRIBUTION_GATE_MODE: dry-run + jobs: gate: runs-on: ubuntu-latest steps: - - name: Close unapproved external issues + - name: Gate unapproved external issues uses: actions/github-script@v7 with: script: | @@ -20,6 +25,12 @@ jobs: const owner = context.repo.owner; const repo = context.repo.repo; const privileged = new Set(['OWNER', 'MEMBER', 'COLLABORATOR']); + const gateMode = (process.env.CONTRIBUTION_GATE_MODE || 'dry-run').trim().toLowerCase(); + const enforceGate = gateMode === 'enforce'; + + if (!['dry-run', 'enforce'].includes(gateMode)) { + core.warning(`Unknown CONTRIBUTION_GATE_MODE "${gateMode}"; defaulting to dry-run.`); + } if (privileged.has(issue.author_association)) return; if (issue.user.login === 'github-actions[bot]') return; @@ -60,6 +71,10 @@ jobs: return; } + const gateMessage = enforceGate + ? 'This repository currently uses a maintainer-managed contribution gate, so issues from contributors who are not listed in `.github/APPROVED_CONTRIBUTORS` are closed automatically.' + : 'This repository is currently observing a maintainer-managed contribution gate in dry-run mode, so this issue is staying open. When enforcement is enabled, issues from contributors who are not listed in `.github/APPROVED_CONTRIBUTORS` will be closed automatically.'; + await github.rest.issues.createComment({ owner, repo, @@ -67,12 +82,14 @@ jobs: body: [ `Thanks @${issue.user.login} for the report.`, '', - 'This repository currently uses a maintainer-managed contribution gate, so issues from contributors who are not listed in `.github/APPROVED_CONTRIBUTORS` are closed automatically.', + gateMessage, '', 'Please read `CONTRIBUTING.md` for the expected issue shape. A maintainer can grant issue access by commenting `/lgtmi` on an issue.', ].join('\n'), }); + if (!enforceGate) return; + await github.rest.issues.update({ owner, repo, diff --git a/.github/workflows/pr-gate.yml b/.github/workflows/pr-gate.yml index 23fe1f95..3e4052db 100644 --- a/.github/workflows/pr-gate.yml +++ b/.github/workflows/pr-gate.yml @@ -9,11 +9,16 @@ permissions: issues: write pull-requests: write +env: + # Keep new gates observable first. Switch to "enforce" only after maintainers + # have seeded active contributors and reviewed the dry-run signal. + CONTRIBUTION_GATE_MODE: dry-run + jobs: gate: runs-on: ubuntu-latest steps: - - name: Close unapproved external pull requests + - name: Gate unapproved external pull requests uses: actions/github-script@v7 with: script: | @@ -21,6 +26,12 @@ jobs: const owner = context.repo.owner; const repo = context.repo.repo; const privileged = new Set(['OWNER', 'MEMBER', 'COLLABORATOR']); + const gateMode = (process.env.CONTRIBUTION_GATE_MODE || 'dry-run').trim().toLowerCase(); + const enforceGate = gateMode === 'enforce'; + + if (!['dry-run', 'enforce'].includes(gateMode)) { + core.warning(`Unknown CONTRIBUTION_GATE_MODE "${gateMode}"; defaulting to dry-run.`); + } if (privileged.has(pr.author_association)) return; if (pr.user.login === 'github-actions[bot]') return; @@ -61,6 +72,10 @@ jobs: return; } + const gateMessage = enforceGate + ? 'This repository currently uses a maintainer-managed contribution gate, so pull requests from contributors who are not listed in `.github/APPROVED_CONTRIBUTORS` are closed automatically.' + : 'This repository is currently observing a maintainer-managed contribution gate in dry-run mode, so this pull request is staying open. When enforcement is enabled, pull requests from contributors who are not listed in `.github/APPROVED_CONTRIBUTORS` will be closed automatically.'; + await github.rest.issues.createComment({ owner, repo, @@ -68,12 +83,14 @@ jobs: body: [ `Thanks @${pr.user.login} for taking the time to contribute.`, '', - 'This repository currently uses a maintainer-managed contribution gate, so pull requests from contributors who are not listed in `.github/APPROVED_CONTRIBUTORS` are closed automatically.', + gateMessage, '', 'Please read `CONTRIBUTING.md` for the expected contribution shape. A maintainer can grant PR access by commenting `/lgtm` on a pull request.', ].join('\n'), }); + if (!enforceGate) return; + await github.rest.pulls.update({ owner, repo, diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 75dec731..7ed555b9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -170,9 +170,18 @@ Validation: ## Contribution Gate CodeWhale uses a maintainer-managed contribution gate for the community front -door. Maintainers and collaborators bypass this gate automatically. External -contributors must be listed in `.github/APPROVED_CONTRIBUTORS` before their -issues or pull requests remain open. +door. Maintainers and collaborators bypass this gate automatically. The gate +workflows default to dry-run / comment-only mode so maintainers can observe the +signal before closing contributor work. In dry-run mode, unapproved external +issues and pull requests receive a short thank-you / CONTRIBUTING pointer and +remain open. + +When maintainers are ready to enforce the gate, set +`CONTRIBUTION_GATE_MODE: enforce` in the PR and issue gate workflows. In enforce +mode, external contributors must be listed in +`.github/APPROVED_CONTRIBUTORS` before their issues or pull requests remain +open. Before enabling enforcement, seed the allowlist broadly enough for active +external contributors who should not be interrupted by the rollout. The allowlist is scoped: @@ -180,17 +189,21 @@ The allowlist is scoped: - `issue:username` allows issues. - `all:username` allows both. -When an unapproved external contributor opens an issue or pull request, the -matching gate workflow leaves a short thank-you / CONTRIBUTING pointer and -closes it. A maintainer can approve someone by commenting `/lgtm` on a pull -request for PR access, or `/lgtmi` on an issue for issue access. The exact bare -commands `lgtm` and `lgtmi` are also accepted for compatibility, but the -prefixed forms are preferred because they are harder to trigger accidentally in -ordinary review discussion. +A maintainer can approve someone by commenting `/lgtm` on a pull request for PR +access, or `/lgtmi` on an issue for issue access. The exact bare commands +`lgtm` and `lgtmi` are also accepted for compatibility, but the prefixed forms +are preferred because they are harder to trigger accidentally in ordinary review +discussion. Approvals do not edit `main` directly. The approval workflow opens a small allowlist update PR so the new entry is reviewable before it takes effect. +If the gate fires on a good contributor incorrectly, use the same approval flow +to restore them: comment `/lgtm` or `/lgtmi`, merge the generated allowlist PR, +then reopen the affected issue or pull request. If GitHub will not allow the +closed item to be reopened, ask the contributor to resubmit after the allowlist +PR is merged. + ## Agent-Assisted Improvements CodeWhale is allowed to help improve CodeWhale, but the contribution still has