Repository navigation
57 lines (57 loc) · 3.96 KB
/
Copy pathcla-gate.yml
File metadata and controls
57 lines (57 loc) · 3.96 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# Require "CLA gate" instead of "license/cla" after deploying this workflow.
name: CLA gate
on: # zizmor: ignore[dangerous-triggers] Metadata-only API calls; never checks out or executes PR code.
pull_request_target:
types: [opened, reopened, synchronize, edited, ready_for_review]
merge_group:
status:
permissions: {}
jobs:
evaluate:
if: github.event_name != 'status' || github.event.context == 'license/cla'
runs-on: ubuntu-latest
timeout-minutes: 3
permissions:
contents: read
pull-requests: read
statuses: write
concurrency:
group: cla-gate-${{ github.event.pull_request.head.sha || github.event.merge_group.head_sha || github.event.sha }}
cancel-in-progress: false
steps:
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const { owner, repo } = context.repo, event = context.payload;
const sha = event.merge_group?.head_sha ?? event.pull_request?.head.sha ?? event.sha;
const publish = (state, description) => github.rest.repos.createCommitStatus({ owner, repo, sha, context: "CLA gate", state, description });
if (context.eventName === "merge_group") return publish("success", "CLA enforcement handled on the pull request.");
if (event.branches?.some(b => b.name.startsWith("gh-readonly-queue/"))) return;
const prs = event.pull_request ? [event.pull_request] : await github.paginate(github.rest.pulls.list, { owner, repo, state: "open", per_page: 100 });
const current = prs.filter(pr => pr.state === "open" && pr.head.sha === sha);
if (current.length === 0) return;
await publish("failure", "CLA verification incomplete. Edit PR title/body to retry.");
/** Fork author attribution does not prove who can push to the branch. */
let exempt = current.every(pr => pr.head.repo?.full_name === `${owner}/${repo}`);
const checked = new Set([49699333, 41898282, 206951365, 199161495]); // Dependabot, Actions, Cursor.
for (const pr of current) {
if (!exempt) break;
const { repository: { pullRequest: live } } = await github.graphql(`
query($owner: String!, $repo: String!, $number: Int!) { repository(owner: $owner, name: $repo) { pullRequest(number: $number) {
headRefOid commits(first: 100) { totalCount nodes { commit { authors(first: 100) { totalCount nodes { user { login databaseId } } } } } }
} } }`, { owner, repo, number: pr.number });
if (live.headRefOid !== sha) return;
exempt &&= live.commits.totalCount === live.commits.nodes.length && live.commits.nodes.every(({ commit: c }) => c.authors.totalCount > 0 && c.authors.totalCount === c.authors.nodes.length);
for (const user of [pr.user, ...live.commits.nodes.flatMap(({ commit: c }) => c.authors.nodes.map(a => a.user))]) {
if (!exempt) break;
if (checked.has(user?.databaseId ?? user?.id)) continue;
const permission = user ? await github.rest.repos.getCollaboratorPermissionLevel({ owner, repo, username: user.login }).then(r => r.data.permission).catch(e => { if (e.status === 404) return "none"; throw e; }) : "none";
exempt &&= ["write", "maintain", "admin"].includes(permission);
checked.add(user?.databaseId ?? user?.id);
}
}
if (exempt) return publish("success", "All contributors exempt.");
const statuses = await github.paginate(github.rest.repos.listCommitStatusesForRef, { owner, repo, ref: sha });
const signed = statuses.find(s => s.context === "license/cla" && s.creator?.id === 2834609)?.state === "success"; // CLA Assistant's OAuth publisher.
if (signed) return publish("success", "CLA verified.");
return publish("failure", "CLA missing or unavailable. Sign, then edit PR title/body to retry.");