Skip to content

fix(ci): Check Changeset 的失败文案不再把「空 changeset」当作与标签等价的出路 (#5292) - #5467

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-5292-changeset-prescription
Aug 5, 2026
Merged

fix(ci): Check Changeset 的失败文案不再把「空 changeset」当作与标签等价的出路 (#5292)#5467
os-zhuang merged 1 commit into
mainfrom
claude/issue-5292-changeset-prescription

Conversation

@os-zhuang

@os-zhuang os-zhuang commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Fixes #5292

问题(前提已对 origin/main 复核)

origin/main @ 61fde5e44:

  • .github/workflows/pr-automation.yml:108 —— 失败文案仍写着 an empty changeset is fine for changes that release nothing;
  • 同文件 :102-104 —— 计数逻辑上方的注释把空 changeset 称作 on par with the skip-changeset label

两处都在,issue 的前提成立。

这两条出路在本闸门的计数里确实等价,但changesets/action 眼里不等价:

  • skip-changeset 标签 = 闸门层面的豁免,不产生任何输入;
  • 空 frontmatter 的 changeset = 喂给 action 的真实输入。待消费的 changeset 全为空时,action 走 hasChangesets && !hasNonEmptyChangesets 分支,打印 All changesets are empty; not creating PR,0 秒返回 —— 不建版本 PR、不发布,而 Release run 全绿。

这条机制不是本 PR 的推断,仓内已有记录:.github/workflows/release.yml:203-208 的恢复步骤注释原文写着 "changesets/action reaches its publish branch only with ZERO pending changesets, and an EMPTY changeset still counts ... and a GREEN run (#4898)"。

所以旧文案是一条主动的错误处方:开发者(或 agent)刚撞上红闸门的那一刻,它把一个已知会静默卡死发布的输入推荐给对方。

改动

只动文案与注释,不动计数逻辑(按分诊口径)。

失败时改为输出三条有序出路:

  1. 确实要发版 → pnpm changeset 并点名 package;
  2. 不发版(.github/.claude/docs/content/examples/、纯测试等)→ 首选 skip-changeset 标签,并说明它为什么安全(不产生 action 输入);
  3. 空 frontmatter changeset —— 依然合法、依然被计数,但降级为带警告的次选:点名它是 action 的真实输入、点名 #4898、点名「全空则 0 秒静默不发版且 Release 全绿」。

上方注释同步改写,去掉 on par with the skip-changeset label 这个错误等价,同时明确保留「仍然计数」这一事实。

完整对照写进 job log(作者用 gh run view --log-failed 或在 UI 里展开这一步时看到的就是它),::error:: 注解保留为 Checks 页签上的单行版本 —— 一行装不下两条出路的取舍。

验证:计数逻辑逐字未动

origin/main 与本分支的 run: 脚本从 YAML 里各自抽出,喂同一组 fixture 仓库跑,比对退出码:

fixture old exit new exit
A. PR 不加任何 changeset 1 1
B1. PR 加一个非空 changeset 0 0
B2. PR 加一个空 frontmatter changeset 0 0
C. PR 只加 .changeset/README.md 1 1

四个 fixture 退出码完全一致。B2 是关键的一格:空 changeset 仍然计数、仍然放行 —— 与今日先例(#5453 / #5454,两者都是带空 frontmatter changeset 过的这道门)保持一致。唯一的差异是 A / C 打印的文案。

关于反向验证方向的老实话:这里不存在「回退改动 → 测试转红」那种方向。本次修改按分诊口径刻意不碰判定,四个 fixture 的判定本就应当前后一致;能证明改对了的,是「退出码逐格相同」+「文案不再包含 an empty changeset is fine」这两条,而不是一次红绿翻转。仓内全域 grep 确认 empty changeset is fine 改后 0 处命中,且 skip-changeset 只在本文件出现,没有第二份需要同步的副本。

YAML 与 heredoc 也实测过:yaml.safe_load 解析通过,抽出的脚本里终止符 MSG 落在第 0 列(YAML 会剥掉块标量的公共缩进,所以终止符必须停在本块基准缩进 —— 这点已写进代码注释,防止后来者重新缩进时踩坑)。

门禁:node scripts/check-nul-bytes.mjsOK (scanned 5447 tracked text file(s) ... no raw NUL bytes);另按 AGENTS.md 的控制字符纪律对改动文件做了越界自扫 grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f]' → 无命中。

清点:.changeset/ 的空 frontmatter 残留(分诊范围 ②)

origin/main @ 61fde5e44 快照:

数量
.changeset/*.md(不含 README) 1065
非空 frontmatter 893
空 frontmatter 172

.changeset/pre.json 处于 mode: pre / tag: rc,记录了 860 个已被 changeset version 消费过的 changeset id。以此切分那 172 个空文件:

  • 140 个已记录在 pre.json —— 即已被某次 RC 版本运行消费过、按 pre 模式保留在盘上的残留。这正是 :95-101 注释描述的、也是本闸门改用 BASE_SHA diff 计数所要绕开的那堆残留。
  • 32 个未记录 —— 属于下一次 release run 的待消费输入。逐个归因:
    • 26 个在可见历史根提交 b49ccfdf 就已存在。该提交无父提交(仓库历史起点),git 无法再往前归因。
    • 6 个可归因,且其引入 PR 全部只碰非发布路径:
空 changeset 引入提交 该提交碰的路径
adr-0076-status-line-calibration.md 0c1660306 docs/
ci-filter-implicit-success-guard.md b8add3758 .github/
pm-dispatch-three-axis-decision-frame.md 3681d2a7d (#5453) .claude/
showcase-register-contact-views.md ed0d2aac0 (#5438) examples/
tenancy-modes-doc-posture-rewrite.md 23e9c9004 content/
widget-contract-theme-token-vocabulary.md 31b05372e (#5454) content/

结论:未发现无主残留。 每个可归因的待消费空 changeset 都来自一个只改非发布路径的 PR,即分诊评论所说的「正当存在的『releases nothing』声明」,按口径一个都没有删

当前 #4898 的 0 秒分支并未被触发:待消费的 changeset 里还有 173 个非空,所以 hasNonEmptyChangesets 为真。换句话说今天的残留是账面事实,不是正在燃烧的火。

一条顺手核实、写进了新文案的事实

新文案第 3 条里那句「空 changeset 也给不了你标签给不了的东西」不是修辞,是实测:

  • 取三个已被消费过的空 changeset(adr-0044-revise-service-owned-noteci-node-22-pinduplicate-fix-guard),把它们的正文原句拿去 grep 全仓 CHANGELOG.md各 0 处命中;
  • 对照组:任取一个非空 changeset 的正文首句 → 命中 packages/spec/CHANGELOG.mdpackages/cli/CHANGELOG.md 2 处

即:空 frontmatter 不点名任何 package,正文因此进不了任何 CHANGELOG。它相对标签没有任何正向收益,却多出「是 action 真实输入」这一项风险 —— 这正是把它降为次选、而不是并列的依据。

(顺带说明:这条事实同样加强了 issue 里那个「干脆禁止空 changeset 进 .changeset/」的备选方案。该方案按分诊口径明确不在本单,牵动发布机器语义,留给维护者裁定,本 PR 不做。)

本 PR 自身怎么过这道门

按 walking the talk,走新文案的首选出路:skip-changeset 标签(先例 PR #5290)。本 PR 只改 CI workflow 文案,不发布任何 package —— 正是新文案第 2 条描述的情形。故不附 changeset

skip-changeset 标签已随本 PR 一并加上(见 Labels),Check Changeset 因此对本 PR 豁免 —— 这正是新文案第 2 条推荐的走法。

…talled the release (#5292)

The gate's failure message offered "an empty changeset" and the
`skip-changeset` label as two equal ways out, and the comment above the
counter called them "on par". They are equal to THIS gate's counting, but
not downstream: the label is a gate-level exemption that produces no input
for changesets/action, while an empty-frontmatter changeset is a real input
to it. When every pending changeset is empty the action takes its
`hasChangesets && !hasNonEmptyChangesets` branch, prints "All changesets
are empty; not creating PR" and returns in 0 seconds -- no version PR, no
publish, Release run green. That is #4898, which stalled 17.0.0-rc.2.

The message was therefore an active mis-prescription: it handed a known
release-stalling input to whoever had just hit the wall.

Rewritten to three ranked routes, with the label preferred and the empty
changeset demoted to a warned last resort that names #4898. The counting
logic is untouched: an empty changeset still counts, so today's precedent
(#5453 / #5454, both merged behind empty changesets) stays valid.

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

vercel Bot commented Aug 5, 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 5, 2026 1:11pm

Request Review

@os-zhuang os-zhuang added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 5, 2026 — with Claude

Copy link
Copy Markdown
Contributor Author

附:新文案在本 PR 上的实跑证据(CI 自证)

本 PR 恰好把两条出路都跑了一遍,而且顺序刚好是「先撞门、再按新文案的首选出路脱困」——

1. 开 PR 时(标签尚未加上)→ Check Changeset 红,打印的正是新文案
run 31009072256 / job 92316255763,log 末段原文:

  3. An empty-frontmatter changeset also satisfies this gate and stays
     legal -- but it is a LAST RESORT, not the quick way past a red check.
     Unlike the label it is a REAL INPUT to changesets/action: when every
     pending changeset is empty, the action takes its
     "hasChangesets && !hasNonEmptyChangesets" branch, prints
     "All changesets are empty; not creating PR", and returns in 0 seconds
     -- no version PR, no publish, and the Release run still goes GREEN.
     That is #4898, which silently stalled 17.0.0-rc.2. It also buys you
     nothing the label does not: an empty changeset names no package, so
     its body reaches no CHANGELOG.

If you are unsure, take route 2. A wrong 'skip-changeset' label is caught by
review; a wrong empty changeset is caught by nobody.
##[error]This PR adds no changeset. If it releases nothing, apply the 'skip-changeset' label (preferred); otherwise run 'pnpm changeset' and name the packages. An empty-frontmatter changeset also passes this gate, but it is NOT equivalent to the label -- it is a real input to changesets/action, and an all-empty set stalls the release silently and greenly (#4898). Full comparison in this step's log.
##[error]Process completed with exit code 1.

这一跑同时证掉了两件本地无法证的事:heredoc 在 GitHub Actions 真实 runner 上的缩进逐字保真(终止符落位正确),::error:: 注解正常渲染成 ##[error]

2. 加上 skip-changeset 标签后 → Check Changeset skipped
run 31009087759 / job 92316311539 —— job 级 if: 豁免生效,闸门整个不跑。

也就是说:红 → 读文案 → 走第 2 条(标签)→ 绿。这条路径就是新文案推荐的那条,本 PR 自己走了一遍。旧文案在同样这一刻会把第 3 条(空 changeset)与第 2 条并列推荐给作者,而在一个改发布闸门的 PR 里种空 changeset,正是 #5292 指出的最难查的情形。


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review August 5, 2026 13:18
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 5, 2026
Merged via the queue into main with commit 99ea450 Aug 5, 2026
22 of 23 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-5292-changeset-prescription branch August 5, 2026 13:26
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Aug 6, 2026
…ack-ai#5580) (objectstack-ai#5625)

`github.event.pull_request.labels` 是事件触发那一刻的快照。开 PR 后数秒内补
`skip-changeset` 标签,`opened` 事件的 run 看不见它 → 走计数路径 → 无 changeset
→ 红;而 `rerun_failed_jobs` 复用同一份载荷(pm-dispatch Operational notes 5),
于是这个红 run 按构造无法被重跑成绿。一日三例:objectstack-ai#5467(本门禁自己的修复 PR)、
objectstack-ai#5501objectstack-ai#5577,每例都要一个人或 agent 停下来「认签名解释掉」。

job 内新增第一个步骤,用 `gh api repos/$REPO/pulls/$PR` 实时读回标签集,产出
`steps.labels.outputs.skip`;其后每个步骤按它决定是否执行。载荷读法按 issue 建议
保留为 fast-path —— 载荷已有标签就整个 job 跳过,常规路径依旧零 runner 成本。

- **容忍方向朝着执行**:标签读不到(API 报错、无 PR 号)判为 `skip=false`,即
  照常执行守卫。读不到输入的门什么也没验证,据此发豁免正是 objectstack-ai#4690 反模式(静默
  跳过、exit 0、看起来像「无违规」);失败以 `::warning::` 明说,由计数步骤定论。
- **实时读放在 checkout 之前**:标签在位时其后全部步骤跳过,整个 job 只花一次 API
  调用 —— 收敛到实时状态比它替掉的那个 stale 红更便宜。
- **精确整行匹配**(`grep -qxF`,here-string 而非管道):被替换的 `contains(数组,
  'skip-changeset')` 是数组元素精确匹配,子串匹配会让 `skip-changeset-audit`
  这类标签新获豁免;here-string 让 `grep -q` 不进管道,避免 `-q` 首个命中即关闭
  管道、写入端吃 SIGPIPE 在 `pipefail` 下把判定翻成 false。
- 保留 fast-path 留下唯一一个反向 stale 格:标签在开 PR 后被**移除**时本 run 仍
  短路。该格自愈 —— 移除标签必然触发 `unlabeled` 事件,它起的 run 两处都看不到
  标签而照常执行;objectstack-ai#5580 那个方向没有这种救援(`labeled` run 的绿不会清掉
  `opened` run 的红)。文件内注释写明了这笔交换。

⛔ 未动 `BASE_SHA` diff 计数逻辑与 objectstack-ai#5292/PR objectstack-ai#5467 的三段有序失败文案(heredoc
终结符仍在块基缩进);未动其他 job。`allow-major` 步骤的同款载荷读法按边界留在
原样 —— RC pre-mode 期间休眠(`check-changeset-no-major.mjs` 整体让位),已记为
objectstack-ai#5620。

验证:`check:workflow-status-functions` 与 `check:nul-bytes`(含各自 self-test)
全绿;从 YAML 抽出该步骤真实脚本,以 stub `gh` 在 `bash -e` 与 `bash -eo pipefail`
两种方言下跑 7 场景 × 2 = 14 例全通过(载荷 stale/标签实时在位、无标签、空标签、
API 失败、无 PR 号、近似标签名、401 个标签的 pipefail 压力);另建前后决策真值表,
7 格中仅「载荷无标签 + 实时有标签」的首 run 与其重跑两格改变(enforce → exempt),
与事前预测一致。

Fixes objectstack-ai#5580

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

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

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Check Changeset 的失败文案把「空 changeset」推荐为出路 —— 而那正是 #4898 静默卡死发布的输入

2 participants