From 95b6f733a28dd64a6eed1e060c69302527f88ff8 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 23 Oct 2026 13:37:42 +0000 Subject: [PATCH] ci: add CLA gate check that exempts maintainers and bots Co-authored-by: Hassieb Pakzad --- .github/workflows/cla-gate.yml | 57 ++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 .github/workflows/cla-gate.yml diff --git a/.github/workflows/cla-gate.yml b/.github/workflows/cla-gate.yml new file mode 100644 index 000000000..56e0c4e47 --- /dev/null +++ b/.github/workflows/cla-gate.yml @@ -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: +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.");