Skip to content

fix(ci): run the changeset family's --self-test in lint.yml, out of reach of skip-changeset (#6509) - #6917

Merged
os-project-manager merged 3 commits into
mainfrom
claude/issue-6509-self-test-lint-wiring
Aug 9, 2026
Merged

fix(ci): run the changeset family's --self-test in lint.yml, out of reach of skip-changeset (#6509)#6917
os-project-manager merged 3 commits into
mainfrom
claude/issue-6509-self-test-lint-wiring

Conversation

@os-project-manager

@os-project-manageros-project-manager commented Aug 9, 2026

Copy link
Copy Markdown
Collaborator

Fixes#6509

The defect

scripts/check-empty-changeset.mjs and scripts/check-adr-0087-registration.mjs were called from exactly one place in the repository: the changeset-check job of .github/workflows/pr-automation.yml (:681-682). That job is exempted wholesale when a PR carries skip-changeset — job-level if: plus the two per-step label reads. And "this PR edits a CI-internal script" is the textbook skip-changeset case: 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 @ 6968885ef

ProbeResult
grep -rln over .github/workflows/ for all three script namespr-automation.yml only, for all three
counter-probe: grep -c "run: pnpm check:" .github/workflows/lint.yml42 steps, and neither check:empty-changeset nor check:adr-0087-registration is among them
specimen: PR #6876 (#5620, merged) carried skip-changeset and added 5 consumer assertionsrun 31289894461, job Check Changeset (93185167327): step 12 Reject an empty-frontmatter changeset added by this PR — the only thing that runs --self-test — reports conclusion: 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-test halves into lint.yml, leave the real scans in changeset-check where $MERGE_BASE lives. Measured before implementing, not assumed:

The chunks.length === 5 job invariant is untouched. The consumer block scopes that count to the changeset-check slice of pr-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.yml is byte-identical to origin/main on this branch (git diff origin/main --name-only lists only the three files below).

check-changeset-no-major.mjs is deliberately not wired: it has no --self-test to 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)

  1. package.json — one new script, following the established self-test-only pattern already used by check:release-body, check:stall-guard and check:objectui-changeset:
    "check:changeset-gate-self-tests": "node scripts/check-empty-changeset.mjs --self-test && node scripts/check-adr-0087-registration.mjs --self-test"
  2. .github/workflows/lint.yml — one step in the ESLint job, - name: Changeset-family gate self-tests, deliberately with no if:. An exemptable self-test is the card itself.
  3. 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/, root scripts/ and a private: true root 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 Changeset job 93196347946 (run 31294147110):

#StepConclusion
12Reject an empty-frontmatter changeset added by this PRskipped
13Require an ADR-0087 disposition on a declared-breaking changesetskipped

Steps 12/13 were the only two places in the repository that ran --self-test. Identical to PR #6876's run 31289894461. 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

ESLint job 93196348012 (run 31294147084), conclusion success:

#StepConclusionDuration
29Changeset-family gate self-testssuccess3s

Same commit, same PR, same label: skipped in changeset-check, executed in lint. 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 56f701810 deliberately reintroduced a real #5620-class regression into pr-automation.yml — one character class, grep -qxF 'allow-major' to grep -qF 'allow-major' — and commit 0290e7747 reverted 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 Changeset skips its self-test steps again, and the new lint.yml step goes RED.

Observed — ESLint job 93196831253 (run 31294323300), conclusion failure:

#StepConclusion
1-28everything before itsuccess
29Changeset-family gate self-testsfailure
30-42everything after itskipped

and in the same run, Check Changeset job 93196809939: skipped. Nothing else in the repository could have caught it.

The failure text:

✗ check-empty-changeset --self-test -- 3 failure(s)
- consumer: exactly one live `grep -qxF 'allow-major'` read is expected in the workflow; found 0. ...
- consumer: the live allow-major read must honour both skip-changeset reads ...
- consumer: the live allow-major read must write `allow=true` exactly once and only after the label
was really observed (found 0 at [], grep at -1) ...

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:

$ git show origin/main:.github/workflows/pr-automation.yml | grep -n "grep -qxF 'allow-major'"
801: if grep -qxF 'allow-major' <<<"$LABELS"; then
$ git log --oneline -S "grep -qxF 'allow-major'" origin/main -- .github/workflows/pr-automation.yml
70949571c fix(ci): allow-major 改为求值时刻实时读标签,红 run 不再永久红 (#5620) (#6876)

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, and pr-automation.yml is byte-identical to origin/main.

Not a stale-artifact green either (AGENTS.md §9): these are .mjs files run directly by node, no dist/ 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:

AblationPredictedObserved
delete the lint.yml stepRED, "exactly once (found 0)"exit 1, that exact message
condition the step on a skip-changeset labelRED, the no-if: pinexit 1, 2 failures (the no-if: pin and the "lint.yml must not read skip-changeset" pin)
swap the step for pnpm check:empty-changeset (the full check)RED, stray-scan + countexit 1, both, naming #6129's false-RED direction
drop the adr-0087 half from the pnpm scriptRED, the adr halfexit 1, that assertion
add paths: to lint.yml's pull_request: triggerRED, the paths pinexit 1, that assertion
restoreGREENexit 0, 64 assertions

Residual, recorded rather than implied

The new assertion is run by the step it pins, so a PR that deletes that step and carries skip-changeset is 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

CommandResult
pnpm check:changeset-gate-self-testsPASS — 64 + 107 assertions
pnpm check:workflow-status-functionsPASS — real YAML parse, 23 workflows / 42 jobs / 24 job-level if: (proves the edited lint.yml still parses)
pnpm check:node-versionPASS — 26 setup-node steps, all Node 22
pnpm check:nul-bytesPASS — 6420 tracked files, no raw control bytes
node scripts/check-type-check-coverage.mjs --self-testPASS — it reads lint.yml and the root manifest; unaffected
13 further lint-job gates (doc-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)all PASS
pnpm exec eslint scripts/check-empty-changeset.mjs --no-inline-configPASS

Boundaries

⛔ Untouched on the final tree: docs/adr/**, content/docs/releases/**, pr-automation.yml (byte-identical to origin/main), the changeset-check job and its chunks.length === 5 invariant, 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: #6923check-changeset-no-major.mjs has no --self-test, and its enforcing half is unexecuted on every run today because Changesets is in pre-mode.


Generated by Claude Code

…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
@vercel

vercelBot commented Aug 9, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 9, 2026 4:25am

Request Review

@os-project-manageros-project-manager added skip-changeset PR has no user-facing published change; bypasses the changeset gate and removed size/m labels Aug 9, 2026 — with Claude
@github-actionsgithub-actionsBot added ci/cd dependencies Pull requests that update a dependency file labels Aug 9, 2026
…#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
@os-project-managerClaude

Copy link
Copy Markdown
CollaboratorAuthor

Note on the red ESLint run at 56f7018 (run 31294323300) — it was deliberate and it is already reverted. Recording it here because the run stays in this PR's history.

56f701810 is titled TEMP ABLATION (reverted in the next commit) and its entire diff is one character class in .github/workflows/pr-automation.yml:

- if grep -qxF 'allow-major' <<<"$LABELS"; then
+ if grep -qF 'allow-major' <<<"$LABELS"; then

That is the reverse verification the card asks for: a green step proves only that it ran, not that it can fail. 0290e7747 reverts it, and pr-automation.yml is now byte-identical to origin/main.

It is not drift on main — the two probes, either of which settles it:

$ git show origin/main:.github/workflows/pr-automation.yml | grep -n "grep -qxF 'allow-major'"
801: if grep -qxF 'allow-major' <<<"$LABELS"; then
$ git log --oneline -S "grep -qxF 'allow-major'" origin/main -- .github/workflows/pr-automation.yml
70949571c fix(ci): allow-major 改为求值时刻实时读标签,红 run 不再永久红 (#5620) (#6876)

The read is intact on main, and it was introduced by PR #6876 — the specimen this card is built on. So the loop closes on itself: an assertion #6876 shipped without ever executing on its own CI is now shown to be live, and to catch a regression to the very line #6876 added, on a PR carrying the same skip-changeset label that hid it. Check Changeset was skipped on that same run (job 93196809939), so nothing else in the repository could have caught it.

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

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

Labels

ci/cddependenciesPull requests that update a dependency filesize/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@os-project-manager@claude