ci(changeset): 改了发版包 src/ 却没带 changeset 的 PR 一律失败,空 frontmatter 为显式豁免 (#3387) - #3769
Merged
Merged
Conversation
…#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>
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
yinlianghui
marked this pull request as ready for review
August 8, 2026 13:16
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 #3387
合并后 objectstack#4904(源单)即可解除阻塞 —— 它把「哪些前端改动发版」的判据统一到「读本仓声明的 changeset」,而本 PR 补上的正是该判据赖以成立的前提。
问题
objectstack#4731 / #4843 已把发版判据统一成读本仓声明的
.changeset/*.md,平台侧直接把它们写进自己的发布记录。这个判据只在一个前提下成立 —— 改了发版包源码就必须带一个 changeset —— 而此前没有任何门禁保证它。三条实测后果:19716b5bffix(charts): name the slices(plugin-charts / plugin-dashboard)5e7ef1141fix(i18n): resolve qualified view ids0e50440(#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 是一等通过写法,不是变通。要的是「声明一次」,不是强制发版:
因此也没有为
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 个包目录(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.yml的pnpm 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,拿另一个提交做完比对并打印自信的绿灯:「你指的 base 不存在」和「你没指 base」是两件不同的事,只有后者可以靠猜回答。同族的
check-i18n-en-drift.mjs仍是跌落写法 —— 本 PR 不动它,已另开 #3766。验证
历史回放(最强的一项证据 —— 门禁在真实历史上重现了 issue 记录的全部三条):
回放最近 80 个非 merge 提交:10 红 / 27 绿(改了守护面且已声明)/ 43 不适用。10 红里 5 个只动了
src/下的测试文件(一行空 frontmatter 即可),另 5 个动了非测试源码,其中两条是用户可见修复且任何发布记录都查不到 ——918888a30fix(fields): echo stored date values in the sub-grid's native date cells、dcff16e06fix(cli,create-plugin),即本类问题在已记录的三条之后又发生了两次。反向验证(先预判方向再跑):
--diff-filter=Agit diff失败吞成空列表第一项的预判错了一次并已改正,值得单独说:原先声称覆盖该 filter 的 "pending" 用例在去掉 filter 后依然全绿 —— 早提交的 changeset 本就落在 diff range 之外,那条用例钉的是 range 而非 filter,注释却说它钉的是 filter。补了两个真正触达 filter 的 fixture;其中「删除待发 changeset」经测量由两道独立防线各自挡住(filter,以及「在 head 读不到的文件声明不了任何东西」),注释按实测改写为「钉结果而非钉某一道防线」。
门禁与测试(全部在共享锁下、
--maxWorkers=2、堆上限 4096):写脚本时踩到并已消灭两个字节级陷阱,均记录在源码注释里:
-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/)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)。CONTRIBUTING.md与AGENTS.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.mddocuments 5 workflows that do not exist and omits 9 that do — including a changeset gate and askip-changesetlabel neither of which is real #3724)。两处都改到最小,并指向本地自查命令。没有 changeset
CI 配置 + 仓库级脚本 + 文档,不改任何已发布包源码 —— 与 #3722 / #3744 同例(两者也都没带)。本门禁自己也这么判(见上)。
顺手记录的两项越界发现(均已开单,本 PR 不修)
--base/OS_I18N_DRIFT_BASE解析不出时会静默跌落到别的提交,拿错基准比对并报绿 #3766 ——check-i18n-en-drift.mjs的显式 base 同款跌落缺陷(未打标签,等分诊定级)。scripts/README.md整篇在讲两个不存在的脚本,且无任何门禁/链接覆盖(finding,不带pm:queue)。Generated by Claude Code