chore(cla-assistant): harden the reusable and drop the runner input (ENG-2017) - #55
Open
lucas-koontz wants to merge 3 commits into
Open
chore(cla-assistant): harden the reusable and drop the runner input (ENG-2017)#55lucas-koontz wants to merge 3 commits into
lucas-koontz wants to merge 3 commits into
Conversation
…ENG-2017) Ten public repos reached mdb-dev through a hand-rolled copy of this job. An outside account opening a pull request or leaving a comment started every one of those runs with no approval, because pull_request_target and issue_comment both execute in base-repo context. Delete the runs-on input so no caller can point the CLA check at a self-hosted runner. Pin contributor-assistant/github-action to the commit behind v2.6.1. Move the event filter from the step to the job, so an unrelated comment reports as skipped instead of claiming a runner. Reduce actions to read: the only write it buys is a re-run API that refuses a GITHUB_TOKEN, and dropping the scope entirely fails the job on the workflow listing that precedes it. Refs: ENG-2017
…sing (ENG-2017) The action writes the ledger through the contents API, and that API answers 404 for a branch that does not exist rather than creating one. Nothing upstream creates it, and upstream is archived, so five repos needed the branch made by hand before their first signature could land. A repo adopting this workflow would have hit the same wall, and the symptom is a red check on a contributor's pull request with nothing they can do about it. Refs: ENG-2017
This was referenced Aug 27, 2026
The ledger needs a branch and a file, and the action supplies neither. The branch this workflow already creates. The file it does not, and the action's own handler for it is dead code: setupClaCheck.ts guards the create path with `error.status === "404"`, a string compared strictly against Octokit's numeric RequestError.status, so a missing ledger falls through to "Could not retrieve repository contents. Status: 404" and main.ts fails the job. Read out of dist/index.js at the pinned commit, which is what actually executes. dataprep_ml, mindsdb_evaluator and type_infer each have a cla branch and no ledger file, so all three would have hit this on their first external pull request: a red check with nothing to act on, instead of the bot asking for a signature. That is the case this change set exists to fix. The seeded content is byte-identical to what the action would have written, JSON.stringify(content, null, 3) at 31 bytes, so the first real signature lands as a one-line diff rather than a reformat. Also points the caller example and the README at mindsdb/mindshub rather than the mindsdb/mindsdb redirect, matching the thirteen callers.
This was referenced Aug 27, 2026
Open
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
User story
As the owner of the newdev and newprod clusters
I want the CLA check to be structurally incapable of running on a self-hosted runner
So that no repository can point it at a pod in our clusters again
Why this matters
Ten public repositories ran a hand-rolled copy of this job with
runs-on: mdb-dev, which is a pod inside our newdev EKS cluster and not a GitHub VM. Its triggers,pull_request_targetandissue_comment, both run in base-repo context, so GitHub's fork-approval gate never applied. An outside account opening a pull request, or leaving a comment on any issue, started that pod with nobody clicking anything. Six accounts already have, and the job records name the runner each landed on.This reusable already defaults
runs-ontoubuntu-latest, and the three repositories that call it are fine. The gap is that the knob exists at all, and that the job it runs was pinned to a mutable third-party tag.What happens today
flowchart TD C["Caller wrapper"] --> I["Optional runs-on input"] I --> J["Job runs on whatever the caller named"] J --> S{"Step condition filters the event"} S -->|"unrelated comment"| N["Runner already claimed, step skipped"] S -->|"CLA event"| A["contributor-assistant on a mutable tag"]What should happen
flowchart TD C["Caller wrapper"] --> J{"Job condition filters the event"} J -->|"unrelated comment"| N["Job skipped, no runner claimed"] J -->|"CLA event"| R["Job runs on ubuntu-latest, no input to override it"] R --> A["contributor-assistant pinned to a commit SHA"]Acceptance criteria
runs-oninput, so a caller cannot request a self-hosted runner.issue_commentthat is not a CLA comment leaves the job skipped, with no runner claimed.pull_request_targetstill runs the check, because that is what asks a new contributor to sign.contributor-assistant/github-actionis pinned to a commit SHA.actions: readrather thanactions: write.anton,coworkandcowork-serverkeep working unchanged, and none of them passes the removed input.How to test
clabranch.ca4a40a7d1004f18d9960b404b97e5f30a505a08is the commit behind thev2.6.1tag it replaced.Notes for the reviewer
Deleting the input is the load-bearing change, not moving the default. The default was already
ubuntu-latest. A default is advice; ten repositories showed what happens when a knob exists next to a cluster. With the input gone, no caller can express the mistake.The event filter moves from the step to the job, and the condition itself is unchanged. Every caller triggers on
issue_comment: [created], which fires on a comment on any issue in the repository, from any account. At step level that still started a runner and then did nothing. This is also where four repositories got it wrong in the other direction: they gated ongithub.event_name == 'pull_request'while triggering on events that are notpull_request, so the clause was never true and their CLA silently never asked anybody to sign. Narrowingpull_request_targethere would reproduce that bug, which is why every PR event still runs.actionsbecomes read rather than being removed. The upstream README asks foractions: write. I read the source: the only write it buys ispullRerunRunner.tsre-running a previously failed CLA run, and that API refuses aGITHUB_TOKEN, so the call already fails andcore.errorswallows it. Dropping the scope entirely is not safe, because the same file callsactions.listRepoWorkflowsfirst andmain.tsturns any throw intocore.setFailed.It now creates the ledger instead of assuming one, and the ledger is two things. The branch: the action writes through the contents API, and that API answers 404 for a
branch:that does not exist rather than creating one. The file: the action does try to handle that itself, and the code is dead.setupClaCheck.tsguards its create path witherror.status === "404", a string compared strictly against Octokit's numericRequestError.status, so the branch never runs and a missing ledger falls through toCould not retrieve repository contents. Status: 404, whichmain.tsturns into a failed job. I read it out ofdist/index.jsat the pinned commit, which is what actually executes.This is not hypothetical.
dataprep_ml,mindsdb_evaluatorandtype_infereach have aclabranch and no ledger file, so all three would have hit it on their first external pull request: a red check with nothing to act on, in the three repos this change set exists to revive. Upstream is archived, so both halves are created here. It is idempotent, two API calls on every run after the first, and the seeded file is byte-identical to what the action would have written.zizmor still reports one finding on this file, and it is real.
contributor-assistant/github-actionwas archived upstream on 2026-03-23. Pinning the SHA is what makes it safe to sit on rather than urgent. Replacing the action is recorded as a follow-up, not smuggled in here.Not done here, deliberately: a
concurrencygroup. The action does a read-modify-write on a JSON ledger through the contents API, so two signatures landing in the same minute can conflict. That is a pre-existing correctness question, it is not what this change is about, and widening the diff to cover it would bury the security change.Verified locally
actionlintci.ymlpins.pytest tests/ -qworkflow_graph.py --allow-external-reusableszizmor@1.28.0, this fileunpinned-useson the mutable tag. The one left isarchived-uses, which is upstream being archived. zizmor is advisory inci.ymland does not block.gh search code 'cla-assistant.yml org:mindsdb'returnsanton,cowork,cowork-server, this file and the README. None of the three callers passesruns-on.clashort-circuits, and a missing one resolves the right base,mainformindsdb/engineandstagingformindsdb/type_infer. File:dataprep_ml,mindsdb_evaluatorandtype_inferhave the branch and not the file, so they take the create path; the other eleven skip it.printfoutput is byte-identical toJSON.stringify({signedContributors: []}, null, 3), 31 bytes, which is also the size oflightwood's existing ledger. So the first real signature is a one-line diff.error.status === "404"indist/index.jsatca4a40a…, againstthis.status = statusCodein the bundled@octokit/request-error, which is a number. Strict equality, so the create branch is unreachable.gh api /repos/contributor-assistant/github-action/git/ref/tags/v2.6.1resolves toca4a40a7d1004f18d9960b404b97e5f30a505a08.Ships with
One behaviour across fifteen repositories, one pull request each. This is the
anchor. There is no
Deploys:line and no pull request environment: everysibling is a library repository with no deploy pipeline, so nothing here brings
an environment up.
Ten public repos replacing a hand-rolled job with this reusable:
Three that already called it, moving their agreement link to the same canonical
repository so nothing in the org depends on an alias, and narrowing
actionstothe read this workflow now declares:
And one private repo carrying the eleventh copy of the same hand-rolled job. It
is outside the ticket's public-repo scope and fixed anyway, because leaving one
copy behind is how the next sweep finds eleven again:
Merge order: this one first, and that is a requirement rather than a
preference. Every caller grants
actions: read, and until this merges thereusable still declares
actions: write. A called workflow's permissions areenforced when its file is loaded, so a callee naming a scope its caller has not
granted rejects the whole run as a
startup_failurewith zero jobs, and thereis no job left to report it. A caller merged first therefore has a red, silent,
dead CLA gate on every pull request and issue comment until this lands. The
reverse is safe: a caller granting more than the reusable declares is a legal
downgrade.
Refs: ENG-2017