Repository navigation
ci: add CLA gate check that exempts maintainers and bots #2047
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,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: | ||
|
Comment on lines
+3
to
+7
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 (optional) Every PR already open when Why this was flaggedThe workflow writes Verification: cla-gate.yml:3-7 has only these triggers: |
||
| 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}`); | ||
|
Comment on lines
+30
to
+35
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 Anyone outside the repo can turn a maintainer PR's required Why this was flaggedAn outsider fetches a maintainer's same-repo branch head X, pushes it to a fork branch, and opens a PR. The Verification: In cla-gate.yml, |
||
| 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); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
A same-repository PR with more than 100 commits cannot receive the maintainer exemption, even when every contributor has write access. Fetch the remaining commit pages before deciding whether all contributors are exempt. Prompt To Fix With AIThis is a comment left during a code review.
Path: .github/workflows/cla-gate.yml
Line: 44
Comment:
**Large maintainer PRs stay blocked**
A same-repository PR with more than 100 commits cannot receive the maintainer exemption, even when every contributor has write access. `commits(first: 100)` returns only the first page, so this count comparison always makes `exempt` false. If CLA Assistant has not posted a successful status—the case this workflow is meant to cover—the PR stays blocked.
Fetch the remaining commit pages before deciding whether all contributors are exempt.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly. |
||
| for (const user of [pr.user, ...live.commits.nodes.flatMap(({ commit: c }) => c.authors.nodes.map(a => a.user))]) { | ||
| if (!exempt) break; | ||
|
Comment on lines
+43
to
+46
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 Dependabot and GitHub Actions PRs will likely never get the exemption. They would wait on Why this was flaggedTrigger: a same-repo Dependabot PR, which Verification: This is not a regression, because Dependabot PRs fall back to |
||
| 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."); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This introduces a repository-wide workflow intended to become a required status check, but leaves the canonical
AGENTS.mdCI contract unchanged. Update that contract in this PR to describe the CLA gate, including its maintainer/bot exemptions and external CLA fallback, so future CI changes and reviews account for the new required check.AGENTS.md reference: AGENTS.md:L10-L10
Useful? React with 👍 / 👎.