Skip to content

ci: add CLA gate check that exempts maintainers and bots - #2047

Open
hassiebp wants to merge 1 commit into
mainfrom
hassiebbot/cla-gate-2a2c
Open

hassiebp wants to merge 1 commit into
mainfrom
hassiebbot/cla-gate-2a2c

Conversation

@hassiebp

@hassiebp hassiebp commented Oct 23, 2026 •

Copy link
Copy Markdown
Collaborator

What does this PR do?

The required license/cla status is posted by the external cla-assistant.io service in response to a repository webhook. When a delivery is missed, the status never appears and the PR waits until someone opens the cla-assistant recheck link. Over the past week several PRs here got no status or got it hours late, most of them from maintainers who do not need a CLA check.

This adds .github/workflows/cla-gate.yml, copied unchanged from langfuse/langfuse, where it already replaces license/cla as the required check. It publishes a CLA gate commit status:

  • Same-repo PRs whose author and every commit author have write/maintain/admin access, or are Dependabot, GitHub Actions or Cursor, pass immediately without contacting cla-assistant.
  • Every other PR (forks, outside contributors) passes only once cla-assistant has posted a successful license/cla. The workflow re-runs on the status event, so the gate updates when cla-assistant reports.
  • Uses pull_request_target with metadata-only API calls; it never checks out or executes PR code. Permissions are contents: read, pull-requests: read, statuses: write.

Because pull_request_target runs the workflow from the base branch, this PR cannot exercise the gate itself; it starts running on PRs opened or updated after merge.

Follow-up after merge (settings): in the main protection ruleset, replace the required status check license/cla with CLA gate. Keep cla-assistant installed: outside contributors still sign through it.

Type of change

  • Bug fix
  • New feature
  • Breaking change
  • Refactor
  • Documentation update
  • Tooling, CI, or repo maintenance

Verification

uvx zizmor --offline .github/workflows/cla-gate.yml   # No findings to report. Good job! (1 ignored, 2 suppressed)
npx prettier@3 --check .github/workflows/cla-gate.yml # All matched files use Prettier code style!

Workflow-only change; ruff, mypy and the test suites are unaffected and were not run.

Checklist

  • I self-reviewed the diff using code_review.md.
  • I added or updated tests for behavior changes.
  • I updated docs, examples, or .env.template if needed.
  • I did not hand-edit generated files; if generated files changed, I used the upstream regeneration path.
  • I did not commit secrets or credentials.
Open in Web Open in Cursor 

RetriggerView in GreptileConfidence Score: 4/5

Fetch all commit pages before merging so large maintainer PRs can receive the promised exemption.

Summary

Adds a CLA gate commit status to replace the required external CLA check.

  • The CLA gate passes qualifying contributors and checks CLA Assistant for others.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Workflow event] --> B{Merge group?}
  B -->|Yes| C[Publish success]
  B -->|No| D[Find matching open PRs]
  D --> E{Same repo and all contributors exempt?}
  E -->|Yes| C
  E -->|No| F{Trusted license/cla status succeeded?}
  F -->|Yes| C
  F -->|No| G[Publish failure]
Loading

Reviews (1) · Last reviewed commit: "ci: add CLA gate check that exempts main..." · Reviewed by Greptile

Co-authored-by: Hassieb Pakzad <hassiebp@users.noreply.github.com>
@hassiebp
hassiebp requested a review from a team as a code owner October 23, 2026 13:38
@github-actions

Copy link
Copy Markdown

@claude review

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

No review was started: this request came from a bot account. Manual reviews can only be requested by someone with write access to this repository. Ask a maintainer to comment @claude review, or have your automation post the comment from a user account with write access.

Tip: disable this comment in your organization's Code Review settings.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-23T13:41:59.940039Z 95b6f73 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 95b6f733a2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@@ -0,0 +1,57 @@
# Require "CLA gate" instead of "license/cla" after deploying this workflow.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Document the new CLA gate in AGENTS.md

This introduces a repository-wide workflow intended to become a required status check, but leaves the canonical AGENTS.md CI 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 👍 / 👎.

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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

Prompt To Fix With AI
This 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.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Beyond the inline findings, I checked three other things in the workflow and found no problem with them. The script contains no ${{ }} interpolation of PR-controlled fields, and nothing checks out or runs PR code. A commit author with no linked GitHub account falls back to "none", so that commit is not exempt. The license/cla lookup only accepts a status whose creator ID is pinned, and it reads the newest such status first.

Extended reasoning...

The change adds one workflow, .github/workflows/cla-gate.yml. It runs on pull_request_target with statuses:write and publishes a required "CLA gate" status. The extra checks covered script injection, untrusted checkout, unlinked commit authors and the trust placed in the license/cla status, and none of them turned up a problem.

Findings marked 🟡 are optional suggestions and need no follow-up push.

Comment on lines +43 to +46
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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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 license/cla exactly as they do today, which defeats the allowlist. GraphQL GitActor.user has type User and is null for commits written under a bot's noreply email, because bots are Bot objects, not users. A null user then gets the "none" permission at line 45, so exempt becomes false. Fix: when user is null, match the commit author's email or name against the allowlisted bot identities (for example 49699333+dependabot[bot]@ users.noreply.github.com). Only the PR author, pr.user, ever matches the IDs at line 36.

Why this was flagged

Trigger: a same-repo Dependabot PR, which .github/dependabot.yml produces routinely, arrives through pull_request_target. pr.user.id is 49699333, so the PR author matches the allowlist at line 44. Every commit is authored by dependabot[bot], though. The GraphQL query at line 40 asks for authors.nodes.user { login databaseId }, and GitActor.user resolves only to User accounts, so it comes back null for bot commits. At line 45 user is null, permission becomes "none" and exempt turns false. The run then falls through to the license/cla lookup and publishes failure unless cla-assistant posted success. On the base branch these PRs already wait on license/cla, so the bot exemption the PR describes (Dependabot, Actions) never applies to bot-authored commits. The same goes for any bot-authored commit pushed onto a maintainer branch. No safeguard handles this: the checked set only holds databaseIds, and a null user has none.

Verification: This is not a regression, because Dependabot PRs fall back to license/cla just as the base requires today. GitHub types GitActor.user as User, so it is null for bot commits. Diff line 53 checks undefined against the allowlist, line 54 gives "none", and line 55 sets exempt to false. The job then waits on license/cla at lines 60-63.

Comment on lines +30 to +35
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}`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Anyone outside the repo can turn a maintainer PR's required CLA gate red by opening a fork PR whose head is the same commit. The gate is a per-SHA status. Line 35 still requires every open PR at that SHA to have a same-repo head. Fix: decide exemption per commit rather than per PR, so a SHA that a fully exempt same-repo PR points at stays exempt whichever PR's event runs. Load all open PRs at the SHA on every event, as the status path already does.

Why this was flagged

An outsider fetches a maintainer's same-repo branch head X, pushes it to a fork branch, and opens a PR. The pull_request_target run for that fork PR publishes failure on X at line 33. Line 35 then makes exempt false because pr.head.repo.full_name is the fork. Unless cla-assistant has a license/cla success on X, it ends with "CLA missing or unavailable" on X. That is the same commit the maintainer PR needs, so its required check goes red. Editing the maintainer PR re-runs with only that PR (line 30) and turns it green. Any later status event re-evaluates both PRs at line 31 and turns it red again, and each edited or synchronize event the outsider triggers on the fork PR does the same. On the base branch the required license/cla comes from cla-assistant's evaluation of X's commit authors, which is identical for both PRs, so a duplicate fork PR cannot change it. No safeguard prevents this. The fork check is meant to stop false success, but because statuses are keyed by SHA it also lets a fork PR veto a commit pushed by someone with write access.

Verification: In cla-gate.yml, publish posts a per-SHA "CLA gate" status (line 27), so both PRs share it on X. For the fork PR's event, line 30 sets prs = [event.pull_request], line 35 is false, and line 57 posts "CLA missing or unavailable" unless license/cla succeeded. On the base branch the required license/cla depended only on commit authors.

Comment on lines +3 to +7
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:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 (optional) Every PR already open when CLA gate becomes required will be blocked until someone edits it, including Dependabot auto-merge PRs and signed fork PRs. Statuses are only written on new PR events or new license/cla statuses. Existing PRs got their license/cla before the workflow existed, so they never get a CLA gate status. Fix: add a workflow_dispatch trigger that runs the same evaluation for each open PR's head SHA, and run it once after the ruleset switch.

Why this was flagged

The workflow writes CLA gate only when triggered by pull_request_target (opened, reopened, synchronize, edited, ready_for_review), merge_group, or status events with context license/cla (cla-gate.yml:3-11). There is no workflow_dispatch or schedule trigger. A PR opened before merge already received its license/cla from cla-assistant, so no further status event will arrive for its head SHA. After the planned follow-up swaps the required check from license/cla to CLA gate, that PR waits on a check that never reports. On the base branch the same PR is mergeable because license/cla is already present. Affected PRs include Dependabot PRs where gh pr merge --auto is pending (dependabot-merge.yml:23) and outside contributors' signed fork PRs. Each one needs a manual title or body edit, or a push. The PR description says the gate only starts on PRs opened or updated after merge, but it does not say that existing PRs become blocked. The status-event path already lists all open PRs (cla-gate.yml:30), so a dispatch backfill fits inside this change.

Verification: cla-gate.yml:3-7 has only these triggers: pull_request_target, merge_group, and status. There is no workflow_dispatch or schedule. A PR whose opened event and license/cla status both came before the workflow existed gets no further event, so it never receives a CLA gate status. Once CLA gate is required, such a PR cannot be merged.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants