返回 CodeWhale
pr-gate.yml
根目录 / .github / workflows / pr-gate.yml
1 name: Contribution gate - pull requests
2
3 on:
4 pull_request_target:
5 types: [opened, reopened]
6
7 permissions:
8 contents: read
9 issues: write
10 pull-requests: write
11
12 env:
13 # Keep new gates observable first. Switch to "enforce" only after maintainers
14 # have seeded active contributors and reviewed the dry-run signal.
15 CONTRIBUTION_GATE_MODE: dry-run
16
17 jobs:
18 gate:
19 runs-on: ubuntu-latest
20 steps:
21 - name: Gate unapproved external pull requests
22 uses: actions/github-script@v9
23 with:
24 script: |
25 const pr = context.payload.pull_request;
26 const owner = context.repo.owner;
27 const repo = context.repo.repo;
28 const privileged = new Set(['OWNER', 'MEMBER', 'COLLABORATOR']);
29 const gateMode = (process.env.CONTRIBUTION_GATE_MODE || 'dry-run').trim().toLowerCase();
30 const enforceGate = gateMode === 'enforce';
31
32 if (!['dry-run', 'enforce'].includes(gateMode)) {
33 core.warning(`Unknown CONTRIBUTION_GATE_MODE "${gateMode}"; defaulting to dry-run.`);
34 }
35
36 if (privileged.has(pr.author_association)) return;
37 if (pr.user.login === 'github-actions[bot]') return;
38
39 function parseAllowlist(content) {
40 return new Set(
41 content
42 .split(/\r?\n/)
43 .map(line => line.replace(/#.*/, '').trim().toLowerCase())
44 .filter(Boolean)
45 );
46 }
47
48 async function readAllowlist() {
49 try {
50 const { data } = await github.rest.repos.getContent({
51 owner,
52 repo,
53 path: '.github/APPROVED_CONTRIBUTORS',
54 ref: context.payload.repository.default_branch,
55 });
56 if (Array.isArray(data) || data.type !== 'file') return new Set();
57 return parseAllowlist(
58 Buffer.from(data.content, data.encoding || 'base64').toString('utf8')
59 );
60 } catch (error) {
61 if (error.status === 404) return new Set();
62 throw error;
63 }
64 }
65
66 const allowlist = await readAllowlist();
67 const login = pr.user.login.toLowerCase();
68 if (
69 allowlist.has(`all:${login}`) ||
70 allowlist.has(`pr:${login}`)
71 ) {
72 return;
73 }
74
75 const gateMessage = enforceGate
76 ? 'This repository currently limits automated PR intake to contributors listed in `.github/APPROVED_CONTRIBUTORS`. This is a maintainer-safety control for code review and CI load, not a judgment on the contribution. A maintainer can grant recurring PR access with `/lgtm` after review; once the generated allowlist PR is merged, this pull request can be reopened or resubmitted.'
77 : 'This repository is observing a maintainer-managed PR intake gate in dry-run mode, so this pull request is staying open. This note helps maintainers prepare the allowlist before any enforcement is considered.';
78
79 const marker = '<!-- codewhale-pr-gate -->';
80 const { data: comments } = await github.rest.issues.listComments({
81 owner,
82 repo,
83 issue_number: pr.number,
84 per_page: 100,
85 });
86 const alreadyNoted = comments.some(comment => (comment.body || '').includes(marker));
87 if (!alreadyNoted) {
88 await github.rest.issues.createComment({
89 owner,
90 repo,
91 issue_number: pr.number,
92 body: [
93 marker,
94 `Thanks @${pr.user.login} for taking the time to contribute.`,
95 '',
96 gateMessage,
97 '',
98 'Please read `CONTRIBUTING.md` for the expected contribution shape. A maintainer can grant recurring PR access by commenting `/lgtm` on a pull request.',
99 ].join('\n'),
100 });
101 }
102
103 if (!enforceGate) return;
104
105 await github.rest.pulls.update({
106 owner,
107 repo,
108 pull_number: pr.number,
109 state: 'closed',
110 });
111
111 lines YAML