fix(ci): run the changeset family's --self-test in lint.yml, out of reach of skip-changeset (#6509) - #6917
Conversation
…each of skip-changeset (#6509) `check-empty-changeset.mjs` and `check-adr-0087-registration.mjs` were called from exactly one place in the repository: pr-automation.yml's `changeset-check` job, which a PR carrying `skip-changeset` is exempted from wholesale. A PR that edits a CI-internal script is the textbook case for that label, so a PR editing these two checkers was routinely the PR that skipped their own fixtures. The self-test halves are pure static checks with no merge-base dependency, so they move to lint.yml's ESLint job as one unconditional step. The real scans stay in `changeset-check`, which is where $MERGE_BASE exists — running them in a job with no branch point would be #6129 in the false-RED direction. The `chunks.length === 5` job invariant is untouched. `check-empty-changeset.mjs`'s consumer block gains 11 assertions pinning the new wiring: the step exists exactly once, carries no `if:`, lint.yml reads no label and carries no paths filter, the gates are invoked only through the self-test-only pnpm script, and that script covers both checkers. 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
|
…#6509 Weakens the live allow-major matcher in pr-automation.yml from `grep -qxF` to `grep -qF`, i.e. reintroduces exactly the #5620-class defect that PR #6876's consumer assertions were added to catch — and which never executed once on #6876's own CI, because that PR carried skip-changeset. This PR carries skip-changeset too. Predicted direction: `Check Changeset` skips its self-test steps again, and the NEW lint.yml step goes RED. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F8q5J1MQyocgtNspb15fSn
…tcher (#6509) Restores `grep -qxF 'allow-major'` in pr-automation.yml, undoing 56f7018. That commit existed only to prove on CI that the new lint.yml step goes RED on a PR carrying skip-changeset; run 31294323300 is the evidence and this returns the branch to the intended state. pr-automation.yml is now byte-identical to origin/main. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F8q5J1MQyocgtNspb15fSn
|
Note on the red
That is the reverse verification the card asks for: a green step proves only that it ran, not that it can fail. It is not drift on The read is intact on No assertion was deleted, loosened or skipped to reach green. The ablation was reverted instead. Full run-by-run accounting, including an honest correction (I predicted 1 failure and got 3, because two further assertions locate that step by the same matcher), is in the PR body's "CI evidence" section. Generated by Claude Code Generated by Claude Code |
Fixes #6509
The defect
scripts/check-empty-changeset.mjsandscripts/check-adr-0087-registration.mjswere called from exactly one place in the repository: thechangeset-checkjob of.github/workflows/pr-automation.yml(:681-682). That job is exempted wholesale when a PR carriesskip-changeset— job-levelif:plus the two per-step label reads. And "this PR edits a CI-internal script" is the textbookskip-changesetcase: such a PR releases nothing, so by the workflow's own prescription it takes the label.The consequence is the exact inversion of #4690: a PR that edits these two checkers is the PR most likely to skip their own fixtures.
Premise re-verified on
origin/main@6968885efgrep -rlnover.github/workflows/for all three script namespr-automation.ymlonly, for all threegrep -c "run: pnpm check:" .github/workflows/lint.ymlcheck:empty-changesetnorcheck:adr-0087-registrationis among themskip-changesetand added 5 consumer assertions31289894461, jobCheck Changeset(93185167327): step 12Reject an empty-frontmatter changeset added by this PR— the only thing that runs--self-test— reportsconclusion: skipped. Steps 13/14/15 likewise. Every one of those 5 assertions was verified locally and by nothing else.Both directions of the gap are real and they are not symmetric: a broken assertion is merely deferred onto the next unlabelled PR (an unrelated author eats the red), while a deleted one is silent forever, because nothing afterwards remembers it existed.
Direction taken: candidate 1, and why it survives contact with the code
The card's candidate 1 and #6883's variant A converge: wire the
--self-testhalves intolint.yml, leave the real scans inchangeset-checkwhere$MERGE_BASElives. Measured before implementing, not assumed:scripts/and.github/into a directory that is not a git repository at all (git rev-parsethere:fatal: not a git repository) and running both self-tests from there: both exit 0 (53 and 107 assertions). They build their own throwaway repos in$TMPDIRand read two files. ~0.8s + ~5.0s.pr-automation.yml.lint.ymlis the right host:on: pull_request:with nopaths:filter and anon: merge_group:trigger, so the step runs on every PR and every queue build, and the workflow reads no PR label anywhere.⛔ The
chunks.length === 5job invariant is untouched. The consumer block scopes that count to thechangeset-checkslice ofpr-automation.yml; this PR adds no step to that job and adds no failable step anywhere near it. The alternative that redraws that boundary was not needed and was not taken.pr-automation.ymlis byte-identical toorigin/mainon this branch (git diff origin/main --name-onlylists only the three files below).check-changeset-no-major.mjsis deliberately not wired: it has no--self-testto run. The new pin says so in its own assertion message, so the omission does not read as an oversight. Its lack of fixtures is filed separately as #6923.What changed (3 files, +167)
package.json— one new script, following the established self-test-only pattern already used bycheck:release-body,check:stall-guardandcheck:objectui-changeset:"check:changeset-gate-self-tests": "node scripts/check-empty-changeset.mjs --self-test && node scripts/check-adr-0087-registration.mjs --self-test".github/workflows/lint.yml— one step in the ESLint job,- name: Changeset-family gate self-tests, deliberately with noif:. An exemptable self-test is the card itself.scripts/check-empty-changeset.mjs— 11 new consumer assertions (53 → 64) pinning the new wiring, in the block that already pins this family's workflow shape (Check Changeset 会因为「别人的 PR 合进了 main」而变绿 —— merge ref 里的他人 changeset 被算成本 PR 新增的 #6129/Check Changeset 的首跑对 skip-changeset 路线结构性必红:job 在 PR 打开瞬间读标签,而标签只能在创建之后打上(今日实测复现 21 次) #6378/观察单:Resolve the diff base是 Check Changeset 里唯一只认快路径标签读取的可失败步骤,标签晚到 + git 基准不可用时会红一个本该豁免的 PR #6434/pr-automation.yml 的 allow-major 步骤仍从事件载荷读标签:#5580 同款竞态的孪生体(pre-mode 期间休眠) #5620 all live there, so the next reader has one place to remember).CI evidence — the gap, and the gap closing, on this PR's own runs
This PR carries
skip-changeset: it edits.github/, rootscripts/and aprivate: trueroot manifest, so it releases nothing and route 2 of the workflow's own prescription applies. That makes it the exact PR shape the card is about, so the card's demand — did your own new wiring actually execute on your own CI? — is answered by measurement here, not by prose.1. The defect, reproduced on this PR (commit
7a0e452b0)Check Changesetjob93196347946(run31294147110):Steps 12/13 were the only two places in the repository that ran
--self-test. Identical to PR #6876's run31289894461. On the two later commits the whole job is skipped at job level (the label is in the payload by then).2. The wiring executing — same commit, same label
ESLintjob93196348012(run31294147084), conclusion success:Same commit, same PR, same label: skipped in
changeset-check, executed inlint. That is the whole fix, measured.3. Reverse verification ON CI — a deliberate red, predicted in advance
A green step only proves it ran; it does not prove it can fail. So commit
56f701810deliberately reintroduced a real #5620-class regression intopr-automation.yml— one character class,grep -qxF 'allow-major'togrep -qF 'allow-major'— and commit0290e7747reverted it. That ablation targets an assertion PR #6876 added and never executed, on a PR carrying the same label #6876 carried.Predicted before pushing:
Check Changesetskips its self-test steps again, and the newlint.ymlstep goes RED.Observed —
ESLintjob93196831253(run31294323300), conclusion failure:and in the same run,
Check Changesetjob93196809939: skipped. Nothing else in the repository could have caught it.The failure text:
Honest correction to the prediction: I predicted 1 failure and got 3. Direction and primary message were right, count was not — because two further assertions locate the allow-major step by that same matcher, so removing it makes them fail too rather than pass vacuously. Recorded as observed rather than reshaped to fit the prediction.
This red was not drift on
main, and the check is cheap to redo:The read is intact on
main, and the commit that introduced it is PR #6876 itself — the specimen this card is built on. So the loop closes exactly: the assertion #6876 shipped without ever running is now demonstrated to be live, and to catch a regression to the very line #6876 added, on a PR carrying the label that hid it. No assertion was deleted, loosened or skipped to reach green — the ablation was reverted instead, andpr-automation.ymlis byte-identical toorigin/main.Not a stale-artifact green either (AGENTS.md §9): these are
.mjsfiles run directly bynode, nodist/anywhere in the path, and the ablation moved the step from green to red and back.Reverse verification — the five local ablations, direction predicted before each run
Run against a sandbox copy of
scripts/+.github/+package.json, all five predicted RED:lint.ymlstepskip-changesetlabelif:pinif:pin and the "lint.yml must not readskip-changeset" pin)pnpm check:empty-changeset(the full check)paths:tolint.yml'spull_request:triggerResidual, recorded rather than implied
The new assertion is run by the step it pins, so a PR that deletes that step and carries
skip-changesetis still not caught — both places that would have run it are gone in the same diff. That is a strictly smaller hole than the one it replaces (which swallowed every labelled PR, including one that merely edits an assertion), it is a deletion plainly visible in a.github/**diff rather than a silent no-op, and closing it entirely would need a gate outside this family asserting this family's wiring — a coupling with its own cost. The comment in the source says the same thing, so the next reader inherits the fact and not a false sense of closure.Gates run locally
pnpm check:changeset-gate-self-testspnpm check:workflow-status-functionsif:(proves the editedlint.ymlstill parses)pnpm check:node-versionpnpm check:nul-bytesnode scripts/check-type-check-coverage.mjs --self-testlint.ymland the root manifest; unaffecteddoc-authoring,docs-audit-scope,role-word,quick-reference-counts,adr-anchors,org-identifier,release-notes,release-body,shard-attestation,published-files,merge-driver,stall-guard,objectui-changeset)pnpm exec eslint scripts/check-empty-changeset.mjs --no-inline-configBoundaries
⛔ Untouched on the final tree:
docs/adr/**,content/docs/releases/**,pr-automation.yml(byte-identical toorigin/main), thechangeset-checkjob and itschunks.length === 5invariant, the counting/merge-base logic,check-changeset-no-major.mjs, and every existing assertion in either checker.Out-of-scope finding filed, not fixed here: #6923 —
check-changeset-no-major.mjshas no--self-test, and its enforcing half is unexecuted on every run today because Changesets is in pre-mode.Generated by Claude Code