Skip to content

ci(changeset): 改了发版包 src/ 却没带 changeset 的 PR 一律失败,空 frontmatter 为显式豁免 (#3387) - #3769

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-3387-changeset-presence-gate
Aug 8, 2026
Merged

ci(changeset): 改了发版包 src/ 却没带 changeset 的 PR 一律失败,空 frontmatter 为显式豁免 (#3387)#3769
yinlianghui merged 1 commit into
mainfrom
claude/issue-3387-changeset-presence-gate

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes #3387

合并后 objectstack#4904(源单)即可解除阻塞 —— 它把「哪些前端改动发版」的判据统一到「读本仓声明的 changeset」,而本 PR 补上的正是该判据赖以成立的前提。

问题

objectstack#4731 / #4843 已把发版判据统一成读本仓声明的 .changeset/*.md,平台侧直接把它们写进自己的发布记录。这个判据只在一个前提下成立 —— 改了发版包源码就必须带一个 changeset —— 而此前没有任何门禁保证它。三条实测后果:

提交 内容 changeset
19716b5bf fix(charts): name the slices(plugin-charts / plugin-dashboard)
5e7ef1141 fix(i18n): resolve qualified view ids
0e50440 (#3518) fix(form),26 个文件跨五个包 + 十个语言包

三条都是用户可见修复,都搭着下一次发版出去,在 CHANGELOG、版本号、平台发布记录里一处都查不到。第三条落在 objectstack#4843 把这件事变得「可听见」之后 —— 可听见没让它停下,只有门禁能。

做法

新增正向触发门禁 .github/workflows/changeset-presence.yml,跑 scripts/check-changeset-presence.mjs:改动落在发版包的 src/ 上时,本次改动必须新增一个 .changeset/*.md

没有加宽 changeset-guard.yml 的 paths。 它的 paths: ['.changeset/**'] 是刻意的反向触发,并写在自己的文件头里:ci.yml/lint.yml 都把 .changeset/** 列进 paths-ignore,只加 changeset 的 PR 不会启动任何别的 workflow,那个 guard 就是为看见这种 PR 而存在。而忘了写 changeset 的 PR 按定义不碰 .changeset/** —— 唯一能发现它的检查恰好是唯一不会跑的检查。加宽会毁掉它原本服务的场景。两个门禁并存、方向相反:一个管已有声明的级别,一个管声明是否存在

几处判断,连同得出它的测量

空 frontmatter 是一等通过写法,不是变通。要的是「声明一次」,不是强制发版:

---
---

Test-only change to the grid column resolver; no published behaviour changes.

因此也没有src/ 下的测试文件开豁免口子 —— 教门禁认哪些文件「不算」正是漏洞的藏身处。顺带一个实测反例:f1310e40ftest(...) 前缀却同时改了非测试源码,提交信息的前缀不可信,文件清单才可信。

守护面是推导出来的,不是写死的 glob。 issue 提的字面 glob 只覆盖 packages/ 下一层;而 @object-ui/consoleapps/console —— 本仓最常改的已发布包,也正是平台 bump-objectui.sh 替它写 changeset 的那个包 —— 会被整整漏掉。改为读 .changeset/config.jsonfixed 组:发版覆盖谁就守谁,ignore 的(@object-ui/site、examples)不守。今天推导出 40 个包目录(39 个 packages/* + apps/console)。既不在 fixed 也不在 ignore 的包,其源码改动响亮失败而不是被当作「不发版」;分类本身由 check-changeset-fixed.mjs 负责,两个门禁合起来是「每个包都被分类,且每个被分类为发版的包的源码都被声明」。

触发器上不加任何 path 过滤,这是与 dispatch 建议形状的一处刻意偏离。trigger 上的 paths 会跳过整个 workflow(GitHub 没有 per-job path filter),不匹配的 PR 根本不会创建这个 check;而一个从不上报的必需 check 不会让 PR 失败,只会让它永远 pending,在合并队列里要等 ruleset 的 60 分钟超时 —— 这正是 #3523 的后半段(#3509 实测一个纯 docs PR 启动了零个 check)。所以本门禁在每个 PR 上都上报、由脚本读 diff 决定,并因此可以被设为必需。过滤器还会成为脚本守护面的第二份副本,和它自由漂移 —— 这也是 docs-links.yml 文件头拒绝 paths: content/** 的同一条理由。

订阅 merge_group 既然可被设为必需,就必须在队列构建上报告,否则只会把队列挂到超时。已加进 merge-queue-reporting.test.ts 的名单(第五条)。脚本不需要为此写任何 per-event 分支:它按「与目标分支的 merge-base」解析基准,队列构建上没有 GITHUB_BASE_REF、也没有 github.event.pull_request,自然落到 merge-base with origin/main —— 正是队列建组所基于的提交。ci.ymlpnpm check:i18n-drift 今天就是这样在这个事件上解析基准的。

push 到 main 不订阅:改动已落地,没有还能写的声明,失败只会把 main 染红在下一位提交者头上。workflow_dispatch 也不订阅:手动跑没有可判的 revision range,而本门禁宁可响亮失败也不肯自己编一个;本地入口是直接 node scripts/check-changeset-presence.mjs(默认「本分支 vs merge-base」,并读工作树 —— 未提交、甚至未 git add 的 changeset 也算,pnpm changeset 刚写完就能自查)。

每一项缺失输入都响亮失败(#4690 / objectstack#4928):base 解析不出、git diff 报错、.changeset/ 目录不存在、包未分类,全部红。方向和 ci.yml 里的过滤门禁相反:那些决定要不要跑活,「判断不了」就跑;这里判断本身就是活,「判断不了」就失败。两者都拒绝在什么都没看的情况下报绿。

写测过程中被自己的测试抓出的一个真实缺陷

resolveBaseRef 初稿把显式 --base 只当作候选链第一环,于是一个本地 clone 里不存在的 sha 会静默跌落merge-base with main,拿另一个提交做完比对并打印自信的绿灯:

# 缺陷版
$ node scripts/check-changeset-presence.mjs --base 0123456789abcdef0123456789abcdef01234567
Compared the working tree with b1204af0a (merge-base with origin/main): ...
✅  No source of a released package changed in this range, so no changeset is owed.
exit=0
# 修好后
❌  Cannot resolve the commit to compare against ...  tried: --base 0123… (unresolved)
    That base was named EXPLICITLY, so it is not guessed around: ...
exit=1

「你指的 base 不存在」和「你没指 base」是两件不同的事,只有后者可以靠猜回答。同族的 check-i18n-en-drift.mjs 仍是跌落写法 —— 本 PR 不动它,已另开 #3766

验证

历史回放(最强的一项证据 —— 门禁在真实历史上重现了 issue 记录的全部三条):

19716b5bf -> exit 1      # 并点名 @object-ui/plugin-charts / plugin-dashboard,5 个文件
5e7ef1141 -> exit 1      # @object-ui/i18n,2 个文件
0e50440   -> exit 1      # 6 个包,26 个文件 —— 与分诊追评记录的 26 完全一致
e473b6c29 -> exit 0      # 带了 changeset 的提交,正常通过

回放最近 80 个非 merge 提交:10 红 / 27 绿(改了守护面且已声明)/ 43 不适用。10 红里 5 个只动了 src/ 下的测试文件(一行空 frontmatter 即可),另 5 个动了非测试源码,其中两条是用户可见修复且任何发布记录都查不到 —— 918888a30 fix(fields): echo stored date values in the sub-grid's native date cellsdcff16e06 fix(cli,create-plugin),即本类问题在已记录的三条之后又发生了两次

反向验证(先预判方向再跑):

拆掉的机制 预判 实测
去掉 --diff-filter=A 「编辑他人待发 changeset」用例转红 ✅ 1 failed / 30 passed
恢复 base 跌落写法 显式 base 用例转红,exit 0→1 ✅ 1 failed / 30 passed,exit 实测 0→1
git diff 失败吞成空列表 diff 失败用例转红 ✅ 1 failed / 30 passed

第一项的预判错了一次并已改正,值得单独说:原先声称覆盖该 filter 的 "pending" 用例在去掉 filter 后依然全绿 —— 早提交的 changeset 本就落在 diff range 之外,那条用例钉的是 range 而非 filter,注释却说它钉的是 filter。补了两个真正触达 filter 的 fixture;其中「删除待发 changeset」经测量由两道独立防线各自挡住(filter,以及「在 head 读不到的文件声明不了任何东西」),注释按实测改写为「钉结果而非钉某一道防线」。

门禁与测试(全部在共享锁下、--maxWorkers=2、堆上限 4096):

pnpm exec vitest run scripts/            -> 21 files / 400 tests passed
pnpm run type-check:scripts              -> 0 errors   (初版被它抓到一处未收窄的联合类型)
node scripts/check-control-bytes.mjs     -> OK (3700 files) + 手工 grep 全部改动文件:干净
node scripts/check-doc-links.mjs         -> Links are valid across 7 scan roots
node scripts/check-changeset-no-major.mjs / check-changeset-fixed.mjs -> OK
pnpm exec eslint (两个新文件)              -> exit 0

写脚本时踩到并已消灭两个字节级陷阱,均记录在源码注释里:-z 的分隔符被写文件工具实体化成了真的 NUL 字节(#4890 同款,改用 check-control-bytes.mjs 的写法 String.fromCharCode(NUL));以及注释里的 glob 星号后跟斜杠会提前闭合 /** */ 块(tsconfig.scripts.json 文件头记过这一条)。

自指冒烟测试(dispatch 第 5 条):本 PR 自己不碰任何守护面,门禁在自己的改动上判为「不欠 changeset」并通过 —— 实测 7 个文件、0 个落在守护面内。远端 CI 上的 Changeset Declaration 应当报告为绿(而非 skipped —— 它没有 path 过滤)。

文件面的两处扩张(相对 dispatch 的 .github/workflows/ + scripts/)

  1. content/docs/guide/ci-cd-pipeline.md —— 机械强制,不是选择。 ci-cd-pipeline-doc.test.ts 双向钉住该页与 .github/workflows/:新增 workflow 而不在该页建节,pnpm test 直接红。同时该页原有一段「Nothing in CI requires a pull request to add a changeset」会被本 PR 变成假话,已改写(顺带说清三个名字相近的检查各管什么,以及为什么豁免是空 frontmatter 而不是一个 label)。
  2. CONTRIBUTING.mdAGENTS.md 各一处 —— 文档不能和门禁对着说。 前者原写「DON'T create a changeset for … apps / 测试改动」,后者原写「纯 bug 修复不需要」;而这条旧判据正是放走上面三条修复的那一条。这页自己反复引用的教训就是:把 CI 实际强制内容说错的文档比没有文档更糟(Docs:ci-cd-pipeline.md 的 Performance Budget / Size Check 两节与实际工作流不符(预算数字差 5.8 倍、记录了一个不存在的工作流) #3197 / Docs:ci-cd-pipeline.md 的工作流清单与 ci.yml 一节仍与实际不符(11 vs 12、两个工作流没被记录、五个任务名里三个不存在) #3212 / .github/WORKFLOWS.md documents 5 workflows that do not exist and omits 9 that do — including a changeset gate and a skip-changeset label neither of which is real #3724)。两处都改到最小,并指向本地自查命令。

没有 changeset

CI 配置 + 仓库级脚本 + 文档,不改任何已发布包源码 —— 与 #3722 / #3744 同例(两者也都没带)。本门禁自己也这么判(见上)。

顺手记录的两项越界发现(均已开单,本 PR 不修)


Generated by Claude Code

…#3387)

objectstack#4731 / #4843 把「哪些前端改动发版」的判据统一成**读本仓声明的
`.changeset/*.md`**,而这个判据赖以成立的前提——改了发版包源码就必须带一个
changeset——此前由任何门禁保证。实测的后果:`19716b5bf` fix(charts)、
`5e7ef1141` fix(i18n)、`0e50440`(#3518,26 个文件跨五个包加十个语言包)
都改了已发布包的源码、都是用户可见修复、都没有 changeset,于是搭着下一次发版
出去,在 CHANGELOG、版本号、平台发布记录里一处都查不到。

新增正向触发门禁 `.github/workflows/changeset-presence.yml`
(`scripts/check-changeset-presence.mjs`):改动落在发版包的 `src/` 上时,
本次改动必须**新增**一个 `.changeset/*.md`。

⛔ 没有加宽 `changeset-guard.yml` 的 paths。它的 `paths: ['.changeset/**']`
是刻意的反向触发,并写在自己的文件头里:`ci.yml`/`lint.yml` 都把
`.changeset/**` 列进 `paths-ignore`,只加 changeset 的 PR 不会启动任何别的
workflow,那个 guard 就是为看见这种 PR 而存在的。而**忘了写 changeset 的 PR
按定义不碰 `.changeset/**`**——唯一能发现它的检查恰好是唯一不会跑的检查。
加宽会毁掉它原本要服务的场景,所以两个门禁并存、方向相反:一个管已有声明的
级别,一个管声明是否存在。

几处判断,连同得出它的测量:

- **空 frontmatter 是一等通过写法**,不是变通。要的是「声明一次」,不是强制
  发版;纯内部改动/只动测试写 `---` 紧跟 `---` 加一句理由即可,理由就留在仓
  库里。因此也**没有**为 `src/` 下的测试文件开豁免口子——教门禁认哪些文件
  「不算」正是漏洞的藏身处;顺带一个实测反例:`f1310e40f` 是 `test(...)` 前
  缀却同时改了非测试源码,提交信息的前缀并不可信,文件清单才可信。
- **守护面是推导出来的,不是写死的 glob。** issue 提的字面 glob 只覆盖
  `packages/` 下一层,而 `@object-ui/console` 在 `apps/console`——本仓最常改
  的已发布包,也正是平台 `bump-objectui.sh` 替它写 changeset 的那个包——会被
  整整漏掉。改为读 `.changeset/config.json` 的 `fixed` 组:发版覆盖谁,门禁
  就守谁,`ignore` 的(`@object-ui/site`、examples)不守。今天推导出 40 个包
  目录。既不在 `fixed` 也不在 `ignore` 的包,其源码改动**响亮失败**而不是被
  当成「不发版」,分类本身由 `check-changeset-fixed.mjs` 负责。
- **触发器上不加任何 path 过滤。** trigger 上的 `paths` 会跳过整个 workflow
  (GitHub 没有 per-job path filter),于是不匹配的 PR 根本不会**创建**这个
  check;而一个从不上报的必需 check 不会让 PR 失败,只会让它永远 pending,
  在合并队列里则要等 ruleset 的 60 分钟超时——这正是 #3523 的后半段。所以本
  门禁在每个 PR 上都上报、由脚本读 diff 决定,并因此**可以**被设为必需,同时
  订阅 `merge_group`(`merge-queue-reporting.test.ts` 的名单加了这一条)。
  过滤器还会成为脚本守护面的第二份副本,和它自由漂移。
- **push 到 main 不订阅**:改动已经落地,没有还能写的声明,失败只会把 main
  染红在下一位提交者头上。`workflow_dispatch` 也不订阅:手动跑没有可判的
  revision range,而本门禁宁可响亮失败也不肯自己编一个。
- **每一项缺失输入都响亮失败**(#4690 / objectstack#4928):base 解析不出、
  `git diff` 报错、`.changeset/` 目录不存在、包未分类,全部红。方向和
  `ci.yml` 里的过滤门禁**相反**:那些决定要不要跑活,「判断不了」就跑;这里
  判断本身就是活,「判断不了」就失败。两者都拒绝在什么都没看的情况下报绿。

自身写测过程中被自己的测试抓出一个真实缺陷并修掉:`resolveBaseRef` 原先把
显式 `--base` 只当作候选链的第一环,于是一个在本地 clone 里不存在的 sha 会
静默跌落到 `merge-base with main`,拿**另一个**提交做比较并打印自信的绿灯
(实测 exit 0;修好后 exit 1)。「你指的 base 不存在」和「你没指 base」是
两件不同的事,只有后者可以靠猜回答。(同族的
`check-i18n-en-drift.mjs` 仍是跌落写法,已另开单,本 PR 不动。)

反向验证(先预判方向再跑):去掉 `--diff-filter=A` → 「编辑他人待发
changeset」用例转红(1 failed / 30 passed);恢复上述 base 跌落 → 显式 base
用例转红且 exit 0→1;把 `git diff` 失败吞成空列表 → diff 失败用例转红。第一
项的预判**错了一次并已改正**:原先声称覆盖该 filter 的 "pending" 用例在去掉
filter 后依然全绿——早提交的 changeset 本就落在 diff range 之外,那条用例钉
的是 range 而非 filter。补了真正触达 filter 的两个 fixture,其中「删除待发
changeset」经测量由两道独立防线各自挡住,注释按实测改写。

三处文档会因本门禁变成假话,一并修正:`ci-cd-pipeline.md` 里
「Nothing in CI requires a pull request to add a changeset」(该页被
`ci-cd-pipeline-doc.test.ts` 双向钉住,新 workflow 本就必须在此建节)、
`CONTRIBUTING.md` 的「DON'T create a changeset for ... apps / 测试改动」、
以及 AGENTS.md 那句「纯 bug 修复不需要」——正是这条旧判据放走了上面三条修复。
这页自己的教训就是:一个把 CI 实际强制内容说错的文档比没有文档更糟。

无 changeset:CI 配置 + 仓库级脚本 + 文档,不改任何已发布包源码,与 #3722 /
#3744 同例。本 PR 也是自指的冒烟测试——门禁在自己的改动上判为「不欠 changeset」
并通过(实测 7 个文件,0 个落在守护面内)。

Fixes #3387

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

Co-authored-by: Claude <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 8, 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)
objectui Ignored Ignored Aug 8, 2026 1:08pm

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation configuration ci/cd tests labels Aug 8, 2026
@yinlianghui
yinlianghui marked this pull request as ready for review August 8, 2026 13:16
@yinlianghui
yinlianghui added this pull request to the merge queue Aug 8, 2026
Merged via the queue into main with commit a4f837c Aug 8, 2026
18 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-3387-changeset-presence-gate branch August 8, 2026 13:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cd configuration documentation Improvements or additions to documentation tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

门禁:PR 改动 packages/*/src/** 而未新增 .changeset/*.md 时失败(空 frontmatter 为显式豁免)

2 participants