Uh oh!
There was an error while loading. Please reload this page.
fix(release-tooling): say which changeset copy was printed, or refuse - #10844
Conversation
Section 2 of collect-release-notes.sh asked two history questions that a shallow checkout answers wrongly and silently, and printed the answers under normal headings. The list: `git log --diff-filter=D PREV..NEW` looks safe because both endpoints are named, but an endpoint that RESOLVES does not make the range WALKABLE. A clone holding PREV as its own shallow island walks down from NEW, stops at its graft and exits 0, so every changeset consumed below the floor is absent from the list. Measured on the fixture in this file's --self-test: complete clone lists 2, depth-12 island clone lists 1, both exit 0. It now routes through the same `git-history.mjs ensure --no-fetch` predicate section 4 already uses, and withholds the list rather than printing it short. The bodies: every body now names the object it was read from, and the `|| git show "<prev>:<path>"` fallback is deleted rather than guarded. That copy is the pre-edit prose for any changeset revised during the dev cycle, and it was printed with nothing to say so. It was also not the safety net it read as — a changeset consumed this cycle usually did not exist at PREV_REF, so the fallback failed too and took the script down mid-output under `set -e`. The route the filing card predicted (the fallback firing because the deleting commit sits below the floor) does NOT reproduce, and is structurally closed: a file reaches the list only when its deleting commit is above the graft, and such a commit always has its parent present. At the boundary git reports the tree as ADDED, never DELETED, so the file drops out of the list instead of reaching the fallback. Measured at clone depths 5/8/9/10/11/12/15/25. The harm it named is real by another route, which the self-test now pins directly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt
✅ ACCEPT — reviewer of record: |
Uh oh!
There was an error while loading. Please reload this page.
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 32490658877 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
Fixes#10509
Section 2 of
collect-release-notes.shprinted the body of each consumed changeset through a|| git show "${PREV_REF}:${f}"fallback, with nothing in the output saying which copy was printed. This makes every body name the object it was read from, and withholds a body it cannot read rather than substituting a different one.The card's predicted mechanism does NOT reproduce — measured, not reasoned
The card (and this PR's own branch name) says the fallback fires because the deleting commit sits below a shallow floor. That route is structurally closed, and I probed it head-on rather than arguing it:
A file reaches the consumed-list only when its deleting commit is above the graft, and such a commit always has its parent present — so
del~1resolves for every file that is in the list. At the graft boundary itself git reports the whole tree as ADDED, never DELETED, so the file drops out of the list instead of reaching the fallback.Swept over 11 clone depths on the fixture, counting files whose
del~1is unresolvable:del~1unresolvableZero at every depth — the fallback cannot fire from a shallow floor.
The harm the card named is real, by a different route in the same section
The same sweep reproduces the defect that is there, in the column beside it: the list silently shrinks with depth — 0 files at depth 5, 1 at depth 12, 2 at depth 25 — every one of them exit 0, under the same heading.
git log --diff-filter=D PREV..NEWlooks safe because both endpoints are named, but an endpoint that RESOLVES does not make the range WALKABLE (the sibling range-question point in #9450). A clone holding PREV as its own shallow island — the shapegit fetch --shallow-sinceandgit fetch --depth=1 origin TAGboth produce — walks down from NEW, stops at its graft, and exits 0.Worse at the empty end: when the floor sits above every deletion, the stock
_None found — is NEW_REF past the 'chore: version packages' commit?_line printed a plausible wrong diagnosis, sending the operator to check their arguments instead of their clone.So section 2 is now withheld on the same terms as section 4, through the same predicate —
scripts/pm/git-history.mjs ensure --no-fetch, reused rather than re-implemented, per the shared-predicate/per-call-site-verdict ruling from PR #10506.And the fallback is deleted rather than guarded. It was never the safety net it read as: the
PREV_REFcopy is the pre-edit prose for any changeset revised during the cycle, and a changeset consumed this cycle usually did not exist atPREV_REFat all — so it failed too and took the script down mid-output underset -e.The range guard asks about an instant, not an ancestry
changeset_range_guardasks the horizon predicate whether the floor predatesdate(PREV), becausePREV..NEWis contained in[date(PREV), date(NEW)]. Deliberately notmerge-base --is-ancestor: that would also refuse PREV tagged off a side line, which is a legitimate release shape rather than a truncated clone.Ablations — every case shown to fail without its guard
Mutation confirmed on disk by marker counts before/after, never by an editor's exit code; each leg restored and re-verified.
PREV_REFfallbackBody WITHHELD1→0, fallback line 0→1SPANS-PREV pre-edit prose.if false; then0→1_source:label_source:2→1git status --porcelain= 0 filesThe first ablation is the card's harm reproduced directly: with the fallback back in place, the release page is written from prose that was rewritten before it shipped, under a normal heading, with nothing to say so.
The wiring pins are assembled from two adjacent literals so they cannot match their own source line — the phantom-tick defect PR #10506 measured on this same file.
Date shapes
Untouched and left correct: the self-test's window edges are already complete instants (
2026-06-20T00:00:00Z), never bare dates. The--self-testruns green includinggit-history.mjs's own case pinning that, so the p0 from #10807 (landed as PR #10811) is not regressed here.Gates
Derived with
node scripts/pm/dispatch-gates.mjs(no path arguments), run on the final commit67afcb7e67:check-cross-package-test-inputsOK: 13 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.check:nul-bytescheck-nul-bytes: OK (scanned 6257 text file(s) ... no raw ASCII control bytes).collect-release-notes.sh --self-test(thelint.ymlstep covering this file)collect-release-notes --self-test: all cases passed.(21 cases)git-history.mjs --self-test(samelint.ymlstep)git-history --self-test: all cases passed.check-engine-split-ratio --self-test(same step)check-engine-split-ratio --self-test: all cases passed.check:parse-guardreadsscripts/**but scans TypeScript parses; this diff is bash and contains no TS parse, so it is irrelevant by content rather than merely unmatched.Not done here, deliberately
scripts/**— no published package, nothing to release.skip-changesetapplied by publish surface, matching the precedent set by PR fix(pm-tooling): refuse windowed history questions a shallow clone cannot see all of #10506 on this same script..changeset/**, the changeset scripts,cut-rc.yml,release.yml,pr-automation.ymland rootpackage.json—scripts/collect-release-notes.shis not among them.Generated by Claude Code