fix(ci): diff-scope the no-major changeset guard against the merge base (#7005) - #7048
Merged
Merged
Conversation
…se (#7005) `check-changeset-no-major.mjs` read the whole `.changeset` directory with no branch point, so its verdict was a function of what main carried rather than of what the author wrote. Dormant while pre-mode holds; at `changeset pre exit` it would have failed every unlabelled PR in the repo, each listing 172 pending major changesets it never touched, with `allow-major` as the only route out. Judge only what the diff introduces, starting at `merge-base(base, head)` — the machinery `check-empty-changeset.mjs` already carries for #6129, reused rather than reinvented. The pending stock is exempt with no roster. 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 12:02
os-project-manager
enabled auto-merge
August 9, 2026 12:02
This was referenced Aug 9, 2026
This was referenced Aug 9, 2026
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>
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Aug 17, 2026
…tack-ai#7045) (objectstack-ai#7106) `check-empty-changeset.mjs` and `check-adr-0087-registration.mjs` both enumerated the diff with `--diff-filter=AM`. Git's rename detection is on by default, so a changeset renamed and modified in one commit reports as `R` and both filters dropped the row entirely: an emptied changeset, or a newly declared-breaking one with no ADR-0087 disposition, walked past its gate unseen. `--diff-filter=AMR`, reading the branch-point side at the PRE-RENAME path (field 2 of an `R` row, not field 3) — the pattern `check-changeset-no-major.mjs` landed first in PR objectstack-ai#7048 for the third member of the family. A pure rename compares equal and stays exempt, so moving a stock changeset still costs nothing. One thing the reference did not need: the base-side read is guarded by `isChangesetFile(basePath)`. Git pairs renames by CONTENT, so an `R` row can arrive as `.changeset/README.md -> .changeset/x.md`, and README declares nothing by definition — inheriting an exemption from it would have been free. Claude-Session: https://claude.ai/code/session_01F8q5J1MQyocgtNspb15fSn Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Aug 17, 2026
…ough renameRow (objectstack-ai#7342) Fixes objectstack-ai#7164 The rename-row control PR objectstack-ai#7048 added asserted only /^R\d/.test(raw.trim()) — that the diff output begins with an R row, not which two paths git paired. PR objectstack-ai#7157 brought the family's renameRow helper (whole-row match, pinning both paths) into this file for its own case; this control was the one spelling left behind. Re-spelled through renameRow(dir, base, '.changeset/old.md', '.changeset/new.md'), matching the shape check-empty-changeset.mjs and check-adr-0087-registration.mjs already use, and removed the now-unused raw binding. No behaviour change. Co-authored-by: Claude <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 #7005
What was wrong
scripts/check-changeset-no-major.mjsread the entire.changesetdirectory (readdirSync, no branch point) and failed if any pending changeset declared amajor. Its verdict was therefore a function of whatmaincarries, not of what the author wrote — #6129's direction reached by a different route: not a frozen base ref, but no base ref at all.Its sibling
check-empty-changeset.mjshas been diff-scoped since #6129 and carries the whole argument in a header note. This one never was, and nobody noticed because the enforcing half has never executed: the RC exemption stands the guard down for the entire pre-release window.Re-measured on
mainbefore building (the card quotedd3e53f2d8):d3e53f2d86dd3c251b.changeset/*.mdmajormajorentries.changeset/pre.jsonmodeprepreThe premise holds and has grown by one file. Pre-mode has not exited, so the situation is still pending rather than live — there is time, but the trigger is a scheduled event, not a hypothetical.
The sequence it was queued for:
changeset pre exitrewrites the mode to"exit"(@changesets/pre@2.0.2,changesets-pre.cjs.js:117) and that commit lands on main. Pre-modechangeset versiondoes not delete the changesets it consumes — it records them inpre.json.changesets(1279 of them) so the final release can re-apply them; only the post-exitchangeset versiondeletes them. So from that commit until the Version PR merges, every unlabelled PR in the repo would have gone red listing 172 files it never touched. The only route out is theallow-majorlabel, whose own error message says "a whole-stack major release is genuinely intended" — false for a PR fixing a typo.What changed
Judge only what the diff introduces, starting at
merge-base(base, head)— reusing the machinerycheck-empty-changeset.mjsalready carries rather than inventing a second one.judge()stays pure and now takesscan()'s output instead of the stock;render()is unchanged in shape.The rows, and the reason
Mis judged rather than skipped:AMMRRRow 2 removes the obvious bypass: editing a stock
minorchangeset's bump word tomajorintroduces a brand-new whole-stack major, and--diff-filter=Aalone sees nothing. Row 3 keeps the stock exempt when a PR legitimately edits an existing major changeset's prose.The comparison is per package, not per file, so adding
"@objectstack/cli": majorto a changeset that already declared"@objectstack/spec": majoris reported for@objectstack/clialone. Naming only what the PR introduced is the whole point of the card.The stock is exempt with no roster — "absent-or-non-major at the branch point" says it once, where a list of 172 names would be a high-water mark that rots on the first merge (the #5471 shape).
What this does not change: at
changeset pre exita whole-stack major really is intended, and the release's own Version PR carries it. That PR is exempt at the job level (changeset-checkskipschangeset-release/main), so the intended major still lands. What moves is who pays — the release, not the author of an unrelated PR.The
Rdivergence, measuredThis file now uses
--diff-filter=AMR; the two siblings useAM. Measured on git 2.43.0, renaming.changeset/old.mdto.changeset/new.mdwhile flipping its bump tomajorreports asR075 .changeset/old.md .changeset/new.mdand is dropped entirely byAM. Reading the base side at the pre-rename path closes it and costs nothing, because a pure rename compares equal and stays exempt. Both directions are fixtured with a control asserting git really did report a rename.The two siblings have the same hole in their own directions; filed as #7045 rather than fixed here, because their fixtures and messages are theirs.
The tradeoff, named rather than slipped in
This file used to have no dependency on
gitat all. It could run in a checkout with no history, beforepnpm install. It now shells out togit merge-base,git diffandgit show.That is a real reduction in where it can run, and it is the price of the fix: "what this PR introduces" is a claim about one side of a fork, and there is no way to evaluate it without the fork. The sibling already pays exactly this cost for exactly this reason, and both run in the same job, on the same checkout, one step apart. Zero third-party dependencies still holds —
node:child_processand thegitbinary are not new requirements for that job.A base that cannot be resolved, or that has no merge base with head, exits 1 rather than 0 (#4690).
Still unexecuted on CI, and that is stated rather than implied
.changeset/pre.jsonsays"mode": "pre", so the real scan still takes the exemption branch and exits 0 on every run. Theenforceverdict has never been produced by a CI invocation of this script and still is not after this PR. What it is, is fixtured — on every PR, in a job with no label exemption (check:changeset-gate-self-tests, lint.yml's ESLint job). Fixtured is not executed. #7008 set that precedent in this exact file and this PR keeps it.Verification
--self-test: 101 assertions, up from #7008's 70 (both counts measured, not estimated), all green, run throughpnpm check:changeset-gate-self-testsalongside both siblings.The card's acceptance criterion, on the real 172-file stock — driven through the shipped
scan()/judge()/render()withpreoverridden in memory only (.changeset/pre.jsonis never touched, #6170):A PR introducing no major is green with all 172 on disk and pre-mode exited. The other direction, same stock, same run:
One file named, out of 172 on disk. The fix narrows the gate; it does not disarm it.
Reverse verification — direction predicted before the run: RED, listing all 172, for a PR that introduces nothing.
main's stock-scopedjudge()was extracted and driven against the same tree:Confirmed in the predicted direction, and the mutation demonstrably applied: the same input yields 172 offenders on the old code and 0 on the new, so neither reading is a vacuous green. Every fixture follows the same discipline (
check-required-contexts.mjs#6983, and #7008's own self-test): each negative states which positive it differs from, and the#7005block asserts the real stock is non-empty before asserting a PR that introduces none of it is clean.#6129proper is fixtured on a realrefs/pull/N/mergeshape — a base branch that keeps moving after the fork, with two parents, because the defect lives in the difference between them. Three readings are asserted so the correction cannot be mistaken for a scan that finds nothing: from the merge base main's drift is invisible (0), from a frozen fork point it is blamed on this PR (1, the defect pinned), and the stock at HEAD contains it (1, which is exactly what the old gate read). The deletion half is fixtured too — the post-exitchangeset versionremoving every consumed changeset at once is precisely the "base branch that DELETES" case.An unrelated live instance turned up mid-task:
origin/mainmoved from08863dd18to6dd3c251bwhile this PR was being written, andgit diff origin/mainpromptly attributed four files from #7039 to this branch. Rebased.Pins flipped deliberately, not deleted
#7008 pinned current behaviour in several places. Two are now wrong on purpose:
The workflow wiring. It required the bare
node scripts/check-changeset-no-major.mjs. That spelling is the stock-scoped gate, so it is now the thing forbidden, and--base "$MERGE_BASE"is required. Three further assertions were added: exactly one--basecall site, every one of them handed$MERGE_BASEand never a pinned sha, and the step must setMERGE_BASEin its ownenv:—$MERGE_BASEis a shell variable, and a step that never receives it would scan against an empty base.That this is the only pin that moved was measured rather than assumed.
main's 70-assertion self-test, dropped into this worktree so it resolves the real repo root, fails on exactly one assertion and it is that one:69 of test(ci): fixture the no-major changeset guard and wire it into the exemption-free self-test step (#6923) #7008's 70 still hold unchanged.
[finding] An unreadable
.changeset/makescheck-changeset-no-major.mjsprint a tick and exit 0 (#4690 shape) #7006, dissolved rather than fixed. Theno-changeset-dirverdict rendered as exit 0 — a gate that could not read its input reporting "no violations", the check:react-declaration-parity 是唯一没接进任何 workflow 的源码审计门禁,且无 MANIFEST 时静默 skip 退出 0 —— 它现在永远不可能红 #4690 shape, pinned by test(ci): fixture the no-major changeset guard and wire it into the exemption-free self-test step (#6923) #7008 and filed as [finding] An unreadable.changeset/makescheck-changeset-no-major.mjsprint a tick and exit 0 (#4690 shape) #7006. Diff scoping removes the directory read from the enforcing path entirely, so there is no such verdict left to exit 0 from. Its replacement,unreadable-diff, exits 1. The assertion is flipped in place with a comment saying so, andreadChangesetssurvives only to serve--list. Commented on [finding] An unreadable.changeset/makescheck-changeset-no-major.mjsprint a tick and exit 0 (#4690 shape) #7006 rather than closing it — that call is triage's.--listis new, and it is where the stock view went. During an RC window it is how a curator sees what has accumulated; that used to be a side effect of every PR run, emitting 172::noticelines on PRs that introduced none of them, well past GitHub's 10-annotation cap.Gates run locally
pnpm check:changeset-gate-self-tests— green (65 + 107 + 101 assertions, all three family members)pnpm lint(eslint . --no-inline-config, repo-wide) — cleannode scripts/check-nul-bytes.mjs— OK, 6498 files; plus a targeted control-byte self-scan of both changed files, cleannode scripts/check-workflow-status-functions.mjs— the third script that readspr-automation.yml— green--self-test,--listNo changeset
scripts/**and.github/workflows/**only — this releases nothing, soskip-changesetapplies and no changeset is written. That is the route this gate's own sibling prescribes for exactly this shape of PR.Coordination
scripts/check-changeset-no-major.mjsis also #7004's file (the family-wide trailing-comment regex). This PR deliberately does not touchmajorPackagesInor the assertion pinning that gap, so the two changes should merge mechanically. #7004 had no in-flight edits to the file when this was written. #6923 landed as #7008 first, so the serialization triage asked for is already resolved.Generated by Claude Code