Skip to content

fix(scripts): digest 的回落读取不再刷一屏 git fatal:,改说一句可读事实 (#6175) - #6335

Merged
hotlong merged 1 commit into
mainfrom
claude/issue-6175-digest-readat-quiet-fallback
Aug 7, 2026
Merged

fix(scripts): digest 的回落读取不再刷一屏 git fatal:,改说一句可读事实 (#6175)#6335
hotlong merged 1 commit into
mainfrom
claude/issue-6175-digest-readat-quiet-fallback

Conversation

@hotlong

@hotlong hotlong commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Fixes #6175

readAt() 的回落路径本身是对的 —— 坏的是它把一次完整的结果打成了一次失败的样子。本 PR 落地分诊 07:57Z 同意的 B + C 组合:B 消掉误导,C 把「发生了回落」从噪声变成一句可读的事实。

前提复核(#6289 合并后)

一处派发单事实更正

派发单称重放区间 f5bc4c78be76...f995a452d2cato 是 release commit。实测不是:

sha 真实 subject
f995a452d2ca ci(scripts): type-check scripts/ via a standalone tsconfig.scripts.json (#3494)
7dfbeb704e1e chore: release packages (#3248) ← 真正的 release commit

7dfbeb704e1e 正是 issue 正文自己观察到的那次 pin bump 的 to。所以主重放改用 issue 自己的区间 f995a452d2ca..7dfbeb704e1e(回落),并把派发单给的 f5bc4c78be76..f995a452d2ca(回落)留作零回落对照组 —— 比原计划多出一个方向的证据。

实现(仅 scripts/objectui-changeset-digest.mjs)

B — 第一次尝试静默,但只静默「成功的回落」

git() 增加 captureStderr 选项(默认关):打开时用 stdio: ['ignore', 'pipe', 'pipe'] 把子进程 stderr 收进抛出的 error,而不是接到我们自己的 stderr。默认关是有意的 —— 失败即失败的调用(例如 main() 里那次 git cat-file -e 端点探测)应当继续把 git 的诊断直接打出来。

readAt() 因此变成:第一次尝试捕获 stderr;失败则回落到「添加它的那个 commit」,同样捕获;两次都失败时把两次的诊断按尝试点名重新打出来再继续抛错:

✗ objectui-changeset-digest: cannot read .changeset/never-existed.md — neither at `to` nor at the commit that added it.
  at `to` (9b4f60aa1055f38a27a159d1ab2779fae2735d2e):
    fatal: path '.changeset/never-existed.md' does not exist in '9b4f60aa1055…'
  at the commit that added it (9b4f60aa1055f38a27a159d1ab2779fae2735d2e):
    fatal: path '.changeset/never-existed.md' does not exist in '9b4f60aa1055…'

安静是「回落成功」挣来的,永不外借给真正的失败 —— 彻底失败比改前更响(多了一行点名摘要和两段按尝试标注的诊断,退出码不变)。

C — 回落发生时,在 accounting 行旁说一句事实

classifyRange() 统计 absentAtTo(实际发生回落的 changeset 数,entries 已按 path 去重,故一条只记一次),经 buildDigest() 透传到 CLI,在 accounting 行之后打一句、且仅在大于 0 时打:

→ 9 of 9 changeset(s) added in this range no longer exist at 7dfbeb704e1e — a release consumes the changesets it ships; each was read from the commit that added it, so the account above is complete.

两个刻意的落点选择:

  1. 落在 stderr,不落在 changeset 正文。 正文记录的是「这个区间发布了什么」,而「工具怎么读到这个文件」不是发布事实;需要这句话的是盯着这次运行的操作者,也正是当初被那九行 fatal: 误导的那个读者。
  2. 措辞不做主观断言。 不去读 to 的 subject 判断它是不是 release commit —— 那正是 bump-objectui.sh 的 pin changeset 只收 feat|fix 且静默截断到 40 条 —— 破坏性 refactor! 进不了前端发布记录 #4731 从这个文件里清掉的 subject 推断。只陈述实测事实(N 条在 to 处不存在、各自从添加它的 commit 读到),再用一句通用机制解释。

真实区间重放(改前 / 改后)

主重放 f995a452d2ca..7dfbeb704e1e(to = release commit,9 次回落)

改前 改后
stdout sha256 539eb31c7b0674b9… 539eb31c7b0674b9… 逐字节不变(cmp exit 0)
stderr fatal: 行数 9 0
stderr accounting 行 → 9 releasing changeset(s), 0 breaking (0 declared major, 0 annotated), 0 release-nothing, 20 commit(s) without a changeset 同上,逐字不变
stderr 新增 一行 C 句(→ 9 of 9 changeset(s) … the account above is complete.)
退出码 0 0

对照组 f5bc4c78be76..f995a452d2ca(0 次回落):stdout stderr 双流逐字节不变(cmp 两次 exit 0)—— 没有回落就没有新行,C 不制造新噪声。

反向验证(先申报,后执行)

两个变异体都先申报预期方向再跑,实测逐条对上:

M1 —— 撤掉 B(第一次尝试恢复继承 stderr),保留全部新断言

M2 —— 删掉 C 的输出,其余不动

  • 申报:#6175 the fallback SAYS SO — once, with the real count 转红。并且明确申报另外两条不会转红,理由是结构性的,不是运气:a range that crossed no release gains NO new line 是一条否定式断言,把 C 整个删掉恰好平凡地满足它;the fallback is COUNTED 读的是返回值上的 absentAtTo 字段而不是打印出来的那行。
  • 实测:恰好 1 红,正是申报那条;那两条如申报保持绿。

这一条与派发单模板不同,如实记录:派发单写的是「删掉 C ⇒ (a/c 相关断言转红」,实测只有 (a) 侧那条肯定式断言会红。一条否定式断言在构造上无法察觉「它所禁止的东西被删掉了」—— 这正是那条 CLI 子进程肯定式断言必须单独存在的原因,两条并不冗余。

self-test

--self-test#6289 的 37 条 → 45 条,新增 8 条独立成组,⛔ 未改 #4731 / #6099 既有 fixture 的任何精确计数(新建第三个临时仓库 objectui-released:两条 changeset 各自被一个 commit 添加,再由一个 chore: release packages commit 一并删除,范围 base..release 同时含增与删)。

其中 3 条通过 spawnSync 子进程跑真实 CLI:git 的 fatal: 落在真实 stderr 上,进程内断言根本看不见它。彻底失败那条通过新导出的 readAt 驱动 —— classifyRange 到不了那个分支(它的 sha 来自 --diff-filter=A,路径必然存在),而一条测试进不去的错误路径,正是会悄悄烂成静默的那条。

门禁(均在 git add 之后跑)

门禁 EXIT
pnpm check:objectui-changeset(digest 45 条 + objectui-range 自测) 0
pnpm exec eslint scripts/objectui-changeset-digest.mjs --no-inline-config 0
pnpm check:nul-bytes 0(56 断言自测 + 扫 5978 个跟踪文本文件)
控制字节自扫 grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]' grep exit 1(无命中 = 干净)

不在本 PR 里


Generated by Claude Code

`readAt()` 在 `to` 端点读不到 changeset 时会回落到「添加它的那个 commit」,
这条路径本身是对的 —— 范围内的 release commit 消费掉了当时挂着的全部
changeset,回落正是把它们留在发布记录里的机制。但 `execFileSync` 默认把子进程
stderr 直接接到我们自己的 stderr 上,于是第一次 `git show` 失败时 git 的
`fatal: path … does not exist in …` 在 `catch` 跑起来之前就已经打出去了:
真实 pin bump `f995a452d2ca..7dfbeb704e1e`(#6159 / PR #6173)上是九行
`fatal:` 之后接一行「全部成功」。

这正是本文件已经写死的那条原则的镜像面。#4731 写的是「a degraded list and a
complete one must never look alike」,这里发生的是**一个完整的列表长得像一次
失败**,而且它指向错误的处置:下一个读者的第一反应是「digest 挂了,改用
`--no-changeset` 手写」—— 那条路才会真的丢掉 9 条 releasing changeset。

- B:第一次尝试改为**捕获** stderr(`stdio: ['ignore','pipe','pipe']`)而不是
  继承;两次都失败时把两次的诊断按尝试点名重新打出来并继续抛错 —— 安静是
  「回落成功」挣来的,永不外借给真正的失败。
- C:发生过回落时,在 accounting 行旁打一句事实(`absentAtTo`),0 次回落时
  不打 —— 不制造新噪声。这句落在 stderr 而不是 changeset 正文:正文记录这个
  区间发布了什么,而「工具怎么读到这个文件」不是发布事实。

self-test 新增 8 条断言独立成组(#4731/#6099 既有 fixture 的精确计数一字未动),
其中三条通过**子进程**跑真实 CLI —— git 的 `fatal:` 落在真实 stderr 上,
进程内断言看不见它。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BDmDsu2575gDxeMCxXhDE3
@vercel

vercel Bot commented Aug 7, 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 7, 2026 2:05pm

Request Review

@github-actions github-actions Bot added the size/m label Aug 7, 2026
@hotlong hotlong added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 7, 2026 — with Claude
@hotlong
hotlong marked this pull request as ready for review August 7, 2026 14:32
@hotlong
hotlong added this pull request to the merge queue Aug 7, 2026
Merged via the queue into main with commit a585374 Aug 7, 2026
24 of 25 checks passed
@hotlong
hotlong deleted the claude/issue-6175-digest-readat-quiet-fallback branch August 7, 2026 14:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants