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-manager os-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

Probe Result
grep -rln over .github/workflows/ for all three script names pr-automation.yml only, for all three
counter-probe: grep -c "run: pnpm check:" .github/workflows/lint.yml 42 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 assertions run 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):

# Step Conclusion
12 Reject an empty-frontmatter changeset added by this PR skipped
13 Require an ADR-0087 disposition on a declared-breaking changeset skipped

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:

# Step Conclusion Duration
29 Changeset-family gate self-tests success 3s

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:

# Step Conclusion
1-28 everything before it success
29 Changeset-family gate self-tests failure
30-42 everything after it skipped

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:

Ablation Predicted Observed
delete the lint.yml step RED, "exactly once (found 0)" exit 1, that exact message
condition the step on a skip-changeset label RED, the no-if: pin exit 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 + count exit 1, both, naming #6129's false-RED direction
drop the adr-0087 half from the pnpm script RED, the adr half exit 1, that assertion
add paths: to lint.yml's pull_request: trigger RED, the paths pin exit 1, that assertion
restore GREEN exit 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

Command Result
pnpm check:changeset-gate-self-tests PASS — 64 + 107 assertions
pnpm check:workflow-status-functions PASS — real YAML parse, 23 workflows / 42 jobs / 24 job-level if: (proves the edited lint.yml still parses)
pnpm check:node-version PASS — 26 setup-node steps, all Node 22
pnpm check:nul-bytes PASS — 6420 tracked files, no raw control bytes
node scripts/check-type-check-coverage.mjs --self-test PASS — 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-config PASS

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

vercel Bot commented Aug 9, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 9, 2026 4:25am

Request Review

@github-actions github-actions Bot added the size/m label Aug 9, 2026
@os-project-manager os-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-actions github-actions Bot 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
@github-actions github-actions Bot added the size/m label Aug 9, 2026
…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

Copy link
Copy Markdown
Collaborator Author

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

@os-project-manager
os-project-manager marked this pull request as ready for review August 9, 2026 06:54
@os-project-manager
os-project-manager added this pull request to the merge queue Aug 9, 2026
Merged via the queue into main with commit 6ec55ef Aug 9, 2026
27 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-6509-self-test-lint-wiring branch August 9, 2026 07:09
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Aug 17, 2026
…6984) (objectstack-ai#7104)

`scan()` 原本只枚举 `git ls-files`,也就是索引。一个刚写出来、还没
`git add` 的文件根本不在扫描集里,门禁照样 exit 0 并打印它惯常的成功
行 —— 读起来是「这棵树没有裸控制字节」,而不是「你刚写的那个文件没被
看过」。

这个洞的形状恰好等于本门禁自己的事故源:每一例裸字节都来自编辑工具在
作者「正在写关于这个字节的内容」时把转义落成真字节(objectstack-ai#4763 / objectstack-ai#4890 /
PR objectstack-ai#5140 / objectstack-ai#5460 的 issue 正文),而落点必然是一个**新文件**。于是
agent 指令要求的那次「push 前跑一遍」,正是最可能在看一个索引枚举够不
到的文件的那次运行。objectstack-ai#6984 实测过:一个未跟踪文件里躺着裸 0x1b,
AGENTS.md 的 self-scan 找得到,本门禁绿,唯一的差别是索引。

- 扫描集改为「索引 + `--others --exclude-standard`」,即 git 既不跟踪
  也不忽略的工作区文件。
- 成功行改为同时报出两半:`scanned N text file(s) -- N tracked,
  M untracked-not-ignored`。M 为 0 时**照样打印**,否则 CI 的绿和
  objectstack-ai#6984 之前那种绿会渲染成同一句话,而后者正是本单要消掉的假信心。
- 未跟踪的命中在报错行上标 `[untracked]`——「还没 add」是作者拿到
  一个被拒文件后第一个需要知道的事实。
- `--list` 逐条列出未跟踪的那一半路径。

三项实测,不是断言:

1. `--exclude-standard` 让本地垃圾留在外面。本仓库在完整 `pnpm install`
   之后,未跟踪路径 76122 条,过 `--exclude-standard` 之后 0 条;它是剪
   枝而不是遍历,16ms vs 索引枚举的 5ms。EXCLUDED 仍叠加在上面,所以有
   人把 node_modules 取消忽略也点不红。
2. CI 里是 no-op:workflow 检出的是一个 commit,全树受跟踪,未跟踪那一
   半恒为空。`--self-test` 用「把样本 stage 掉再扫一遍」证明,而不是在
   注释里声称。
3. 反向:回退 `enumerate()` 的未跟踪那一半,6 条新断言立刻转红。

`--self-test` 断言数 48 → 75。临时仓库额外把 `core.excludesFile` 指向
一个空文件,否则开发者的全局 ignore 会让「EXCLUDED 仍然生效」那条样本
因为另一个原因通过。

工具链改动,不涉及任何 package,不发版 —— 走 `skip-changeset`,与近期
scripts/ + workflow-only PR 的做法一致(objectstack-ai#7048 / objectstack-ai#7008 / objectstack-ai#6983 / objectstack-ai#6917)。


Claude-Session: https://claude.ai/code/session_01F8q5J1MQyocgtNspb15fSn

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cd dependencies Pull requests that update a dependency file size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants