test(ci): pin the branch-protection required-context job names (#6865) - #6983
Conversation
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F8q5J1MQyocgtNspb15fSn
…on (#6865) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F8q5J1MQyocgtNspb15fSn
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
|
PM ruling on the open question: option A — keep all eight rows, as shipped. From the dispatching Why A:
Also endorsed: dropping the card's proposed "no
Related, and separately filed rather than folded in: #6991 records that #5617's standing "never require" list is stale on Generated by Claude Code |
Fixes #6865
The defect
A GitHub required status check is matched by check-run name, and a job's check-run name is its
name:value. Nothing in a workflow file marks such a string as load-bearing, so renaming a job is a one-line edit with a silent, repo-wide consequence:The second shape is #5617 verbatim: PR #5584 merged with the ESLint job red for 19 minutes, because the job carrying 25
check:*gates was not in the required set at all. Four more merges repeated it the same night (#6067 / #6096 / #6051 / #6103) before the settings were fixed on 2026-08-07.ci.yml already writes this contract down in prose in eight places, and
check-shard-attestation.mjsasserts that the two aggregate gates' job ids still exist. Neither asserts aname:literal, and lint.yml had no note and no assertion at all.What this adds
scripts/check-required-contexts.mjs— a sibling ofcheck:shard-attestation, same idiom (pure exportedjudge(), a--self-testwhose fixtures mutate the real workflows and assert the mutation applied, missing input is a failure never a pass). Wired aspnpm check:required-contextsin the ESLint job, next to the shard-attestation gate.One registry maps workflow file + job id to the exact context name, each row carrying the ruling that authorized it:
ESLintlintTypeScript Type ChecktypecheckTest Coretest-gateDogfood Regression Gatedogfood-gateBuild Corebuild-coreBuild Docsbuild-docsConsole Pin Gateconsole-pinTemporal Conformance (live PG + MySQL)temporal-conformanceEight rows, not the six the card names. All six were re-verified against
origin/mainand all six resolve to real job-levelname:values — but four of them live in ci.yml, not lint.yml, which the card did not say.Test CoreandDogfood Regression Gateare added because their name literals have exactly the same gap:check-shard-attestationpins their job ids (REQUIRED_GATE_JOBS), soname: Test Corecan be renamed today with every gate still green. They are not invented membership — ci.yml declares both as required contexts in its own prose, #5617's audit marked them "should be required", and the closing ruling lists them as confirmation items rather than candidates. Drop the two rows if you disagree; nothing else changes.Per entry the pin asserts: the job id still exists; its
name:is exactly the registered literal; it declares nostrategy.matrix(a matrix job's check-run name has the matrix values appended, so a bare name never reports — and a skipped matrix job publishes the name unexpanded); and no truthycontinue-on-error(such a job cannot concludefailure, so it would publish a green required context over a failing run). Per workflow: amerge_group:trigger, and apull_request:trigger with nopaths:/paths-ignore:. Whole-registry: no context registered twice, and no unregistered job wearing a registered name — live ammunition here, since ci.yml's shardedtestjob is namedTest Core (${{ matrix.shard }}/3)beside a gate namedTest Core, and deleting the suffix collides them into one context whose conclusion is whichever finished last.The workflow-level assertions are the machine-readable form of #5617's audit exclusions, so enrolling one of the ⛔ contexts by mistake (
Console Pin Freshness,Spec property liveness,Validate Package Dependencies) fails here rather than in the merge queue.The card's proposed "no
if:" assertion was dropped, deliberately. #6865's body proposed it; it is right for lint.yml's two and wrong for four of the six added on 2026-08-09 —build-core,build-docs,console-pinandtemporal-conformanceeach carry a filter-drivenif:by design (THE FILTER CONTRACT, #4928). A job-levelif:that skips still publishes a check run; it is the workflow-levelpaths:filter that publishes nothing. Asserting "noif:" uniformly would have made this gate red on main the day it landed, against four correct jobs. The two aggregate gates needif: always()for a reason of their own (#3622) andcheck:shard-attestationalready owns that judgement — re-litigating it here would be the duplicate mechanism this card was told to avoid.It cannot verify that any of these names is actually in main's required set, or in the merge queue's check set. That configuration lives in Settings → Rulesets and no agent seat can read it:
GET /repos/.../branches/main/protectionanswers HTTP 403GitHub access is not enabled for this session. A gate claiming to check the required set would be a gate that cannot read the thing it names — the #4690 phantom-check shape.So the registry is this repo's declaration of what the settings are believed to reference, sourced from the #5617 rulings, and the pin enforces only the half that lives here: these names do not move. Concretely:
Reconciling registry against settings stays a human read; every row names its authorizing ruling so that read is one pass, not archaeology.
There is one further residual, recorded rather than implied: the wiring assertion is run by the step it pins, so a PR deleting both the step and the script is not caught — that is a deletion plainly visible in a
.github/**diff rather than a silent no-op, and closing it entirely needs a gate outside this file asserting this file's wiring. Same shape and same honesty ascheck-empty-changeset's consumer block (#6509).Verification
--self-test: 42 assertions, green. Beyond the in-test fixtures, five ablations were run against the real workflow files through the realpnpm check:required-contexts, direction predicted as RED before each run (exact-literal equality against a registry constant — no??chain and no count, so neither the "more diagnostics" nor the "inverted" direction applies here):name: Build Core→Build (core)(newly-approved batch) — RED,EXIT=1:ci.yml: job 'build-core' is named "Build (core)", but branch protection requires the context 'Build Core'.name: TypeScript Type Check→Type Check(original two) — RED,EXIT=1, naming jobtypecheckand the required context.wiring: lint.yml's ESLint job must run pnpm check:required-contexts — an unwired pin verifies nothing (#4690).--self-testfrom the package.json wiring — RED.merge_group:from lint.yml — RED, naming both contexts it would strand.Each restored cleanly to green afterwards. Ablations 1 and 2 additionally turned the self-test red via its anchor-staleness guard (a
String.replacematching nothing would otherwise leave the fixture judging the pristine workflow), which is the guard behaving correctly, reported here so the double red is not read as noise. No build artifacts are involved, so the stale-dist/trap does not apply.Neighbouring gates re-run green after the change:
check:shard-attestation(92 assertions),check:workflow-status-functions(23 workflows, 42 jobs),check:changeset-gate-self-tests(64 + 107 assertions — this is the one that carrieslint.ymlconsumer assertions, per the #6917 note; the new step does not interact with them),check:nul-bytes(6468 files),check:node-version, andeslinton the new file.Notes
package.jsonis private; the rest is.github/**andscripts/**).skip-changesetapplies.Generated by Claude Code