fix: add contribution gate dry run mode

This commit is contained in:
Nightt
2026-06-02 11:17:33 +08:00
committed by Hunter B
parent c8c20e0931
commit dfe1884702
5 changed files with 92 additions and 16 deletions
+2 -1
View File
@@ -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
+29 -1
View File
@@ -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,
+19 -2
View File
@@ -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,
+19 -2
View File
@@ -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,
+23 -10
View File
@@ -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