test(ci): fixture the no-major changeset guard and wire it into the exemption-free self-test step (#6923) - #7008
Merged
os-project-manager merged 1 commit intoAug 9, 2026
Conversation
…xemption-free self-test step (#6923) `scripts/check-changeset-no-major.mjs` was the one member of the changeset-gate family with no `--self-test`, which is why PR #6917 could not wire it into `lint.yml` alongside the other two. Refactor it into a pure `judge()` plus a pure `render()` (verdict -> lines + exit code), add 70 fixtures, and add the script to `check:changeset-gate-self-tests`. The fixtures were measured against `@changesets/parse@0.4.3` rather than reasoned about, and that measurement found a real divergence: the parser required its fence on line 1, so a changeset opening with a blank line declared nothing to this guard while changesets honoured its `major` and would have promoted the whole lockstep group. Both sibling parsers already skipped leading blanks, and all three carry a comment claiming the three read the same block -- so this was also the one place that comment was false. Fixed, and pinned. The enforcing half is still unexecuted on CI: `.changeset/pre.json` is `"mode": "pre"`, so the real scan takes the exemption branch on every run. What changed is that `enforce` is now produced by fixtures on every PR, in a job with no label exemption. Fixtured is not executed, and the header says so. Also corrects `check-empty-changeset.mjs`, whose consumer assertion stated in its own message that this script is "deliberately absent: it has no `--self-test` to run" -- true when #6917 wrote it, false as of this commit. 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
|
os-project-manager
marked this pull request as ready for review
August 9, 2026 10:21
os-project-manager
deleted the
claude/issue-6923-changeset-no-major-self-test
branch
August 9, 2026 10:38
This was referenced Aug 9, 2026
Merged
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>
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.
Fixes #6923
scripts/check-changeset-no-major.mjswas the one member of the changeset-gate family with no--self-test, which is why PR #6917 could not wire it intolint.ymlalongside the other two. This adds one, following the idiom ofcheck-empty-changeset.mjsandcheck-adr-0087-registration.mjs(pure exported judge, fixtures that drive the real decision, missing-input-is-a-failure), then uses those fixtures to measure the enforcing half instead of reasoning about it.Premise re-verified on
origin/mainfirstBoth halves of the card still held at
d3e53f2d8:grep -n "self-test\|selfTest" scripts/check-changeset-no-major.mjs— no output..changeset/pre.jsonis"mode": "pre", so the real scan takes the RC exemption branch and exits 0 on every run.Which half the fixtures exercise, and what is still unexecuted
Stating this plainly because it is the thing most easily overstated.
judge({ changesets, pre })and a purerender(verdict), so theenforceverdict — the offenders report andprocess.exit(1)— is now produced and asserted on every PR, message and exit code alike..changeset/pre.jsonis unchanged and still"mode": "pre"(this seat performs no release actions, release workflow: publish pushes tags + npm but its version commit never reaches main — twice now (rc.3 c6a52d3, rc.4 a10cbc77); landing the commit must be part of the publish lane #6170), so every real CI invocation still exits 0 through the exemption branch. Fixtured is not executed. What changed is only that the branch is now driven by fixtures, in a job with no PR-level label exemption. The script's header says this in the same words so the next reader does not read one as the other.The measurement that changed the diff
The dialect fixtures are measured against
@changesets/parse@0.4.3— the version this repo resolves — rather than written from the regex. That found a real divergence, so this PR is not fixtures-only:@changesets/parse"@objectstack/spec": major'@objectstack/spec': majordocs: major(unquoted)"@objectstack/spec": MAJOR"@objectstack/spec": major # keepThe parser required its fence on line 1 (
if (lines[0]?.trim() !== '---') return []), so a changeset opening with one blank line declared nothing to this guard while changesets honoured itsmajorand would have promoted the entire lockstep group. Both sibling parsers already skipped leading blanks, and all three carry a comment saying the three read the same block — so this was also the one place that comment was false. One line, aligned with the siblings; it can only ever add offenders, and today it adds only::noticelines.The trailing-comment row is a family-wide gap in a shared regex with a different consequence in each of the three scripts, so it is filed (#7004) rather than fixed here. The fixture pins the current behaviour with a message telling the next author to flip it, not delete it.
Reverse verification — predictions written before running
Ten ablations, each with its expected failure count stated up front. Nine matched exactly; one did not, and is reported as it happened rather than reshaped.
pre?.mode === 'pre'becomes!== 'exit'check:changeset-gate-self-testscheck-empty-changeset)pr-automation.yml.changeset/README.mdlint:so the slice anchor goes staleR2 diverged and the divergence is worth recording. The prediction was five assertion failures. What happened was a
TypeError: Cannot read properties of null (reading 'tag')— the loosened test letspre === nullreach the exempt branch, which then dereferencespre.tag. Still red, but by crashing before any assertion ran, so it proved less than intended. R2b repeats it withpre?.tagand produces exactly the predicted five:no pre.json at all,pre.json that did not parse,pre.json with no mode key,mode: some future spelling,mode: PRE (wrong case)— withmode: exitcorrectly staying green.R7 is the vacuous-green trap this family keeps hitting, checked deliberately after reading
check-required-contexts.mjs(#6983). The wiring block sliceslint.ymlby anchor; a stale anchor yields an empty slice that every assertion below would pass against. R7 renames the job, and the guard fires by name (the lint: job could not be sliced out of lint.yml — its anchors went stale) rather than passing silently.Wiring
check:changeset-gate-self-testsgains a third entry. Two halves land in deliberately different places:lint.yml's ESLint job, which has no PR-level exemption — what 观察单:changeset 家族三个 check-*.mjs 的 --self-test 只在可被 skip-changeset 整体豁免的 job 里跑,改这些脚本的 PR 恰好豁免自己的自检 #6509 / PR fix(ci): run the changeset family's --self-test in lint.yml, out of reach of skip-changeset (#6509) #6917 built the step for;pr-automation.yml, because itsallow-majorandskip-changesetexemptions are deliberate. Moving it intolint.ymlwould silently revoke the escape hatch this script's own error message prescribes. Asserted in both directions.check-empty-changeset.mjsis edited for one reason: its consumer assertion stated in its own message that this script is "deliberately absent: it has no--self-testto run". That was true when #6917 wrote it and false as of this commit, so leaving it would make the repo assert something false. Replaced with an assertion that this script IS wired, and its--self-test-on-every-invocation regex extended to cover the new entry (R4b/R4c confirm both are load-bearing).What was measured and deliberately NOT acted on
Per the card's boundary — fixtures may document the enforcing half; changing release-mode semantics is not this PR's to do.
.changeset/pre.jsonis untouched.The enforcing half reads the whole
.changesetstock with no branch point, unlike its diff-scoped sibling. Measured atd3e53f2d8: 1552 changesets on disk, 171 of them declaringmajor(222 package entries), still present because pre-modechangeset versionrecords consumed changesets inpre.json(1279) rather than deleting them.changeset pre exitrewrites the mode to"exit"(@changesets/pre@2.0.2), so from that commit until the final Version PR merges, every unlabelled PR would fail this guard listing 171 files it never touched, withallow-majoras the only route out. That is #6129's direction by another route and it decides what the guard means, so it is filed (#7005) for triage, not improvised here.Findings filed (unassigned, out of this PR's scope)
check-empty-changeset(a valid changeset reads as empty-frontmatter). Measured against the real parsers by extracting their source, not by re-implementing them.check-changeset-no-major.mjsjudges the whole.changesetstock, not the PR's diff — atchangeset pre exitit reds every unrelated PR (171 files measured) #7005 — the stock-scoped enforcement described above..changeset/makescheck-changeset-no-major.mjsprint a tick and exit 0 (#4690 shape) #7006 —finding: an unreadable.changeset/still exits 0 (check:react-declaration-parity 是唯一没接进任何 workflow 的源码审计门禁,且无 MANIFEST 时静默 skip 退出 0 —— 它现在永远不可能红 #4690 shape). Not changed here, but now distinguishable (no-changeset-dirvsclean), which is the prerequisite for changing it.Verification
No changeset: this PR touches
scripts/and the rootpackage.jsononly and publishes nothing, so it carriesskip-changeset.Generated by Claude Code