pr-automation.yml 的 Check Changeset 门禁,在本 PR 一个 changeset 都没加 的情况下会转绿,条件只是「本 PR 的 base.sha 被记下之后,main 又合进了带 changeset 的别人的 PR」。本仓一天合 ~18 个 PR,所以任何一个在飞久一点的 PR 都能白拿这张免检票。
发现于 PR #6117 (#6095 的实现单)。不是猜的,是同一个 PR 上前后两跑的实测对照。
机制
.github/workflows/pr-automation.yml 的计数步骤:
ADDED=$(git diff --name-only --diff-filter=A "$BASE_SHA" HEAD -- '.changeset/*.md' | grep -v '/README\.md$' | wc -l)
两个输入各自都合理,合在一起就漏:
BASE_SHA = github.event.pull_request.base.sha,它在 PR 创建时 被钉住,后续 synchronize 不跟着 main 往前走。
HEAD 是 actions/checkout@v7 在 pull_request 事件上默认签出的 merge ref (refs/pull/N/merge),也就是「本 PR head 与当前 main tip 的合并」—— 它含有 main 上最新 的一切。
于是 BASE_SHA 与 merge ref 之间的 main 漂移,整段都被 --diff-filter=A 记成「本 PR 新增的文件」。别人 PR 带进 main 的 .changeset/*.md,在本 PR 眼里就是本 PR 加的 changeset。
实测对照(PR #6117 ,同一个 PR,同一份 diff,零 changeset)
PR #6117 只改 pnpm-workspace.yaml + pnpm-lock.yaml,零 package.json 改动,.changeset/ 一个文件都没加。它本该一直红到有人打 skip-changeset。
跑
head
时间
Check for a changeset added by this PR
说明
1
bb4b42623
02:22Z
failure
那一刻 base.sha(6513c1749)恰好就是 main tip,漂移为 0 → ADDED=0 → 正确判红
2
680617108
02:39Z
success
17 分钟里 main 合进了 2 个带 changeset 的 PR → merge ref 里多出 2 个文件 → ADDED=2 → 假绿
第二跑之后 Reject an empty-frontmatter changeset 与 Guard against accidental major bumps 两步也跟着跑绿(job step 列表可见),即整条链都被这张假票放行。
冒名顶替的两个文件,本地可复现(6513c1749 是 PR #6117 的 base.sha):
$ git diff --name-only --diff-filter=A 6513c1749 origin/main -- '.changeset/*.md'
.changeset/last-admin-guard-permission-set-row.md
.changeset/seed-autonumber-read-outage.md
两个都不是 PR #6117 写的,一个字都不是。
危害
门禁的判定与 PR 内容无关,只与「你的 base 落后多少」有关。 同一份 diff,早跑红、晚跑绿;rerun 也不是幂等的 —— 它替 PR 作者做了一次跟 PR 内容无关的掷骰子。
这条门禁守的是「改了已发布包却不发版」这类事故(objectui 可以在改动已发布包源码的同时不带 changeset —— 这些修复不会出现在任何发布记录里 #4904 是同一族的另一半)。它一旦可以被 main 的合并节奏顺手关掉,改了发布包却没 changeset 的 PR 就能安静合入 —— 后果要到发版时才看得见。
方向上比 Check Changeset 从事件载荷读 skip-changeset 标签:开 PR 后 5 秒内加标签,首个 run 永久红(重跑复用载荷)—— 一日三例 #5580 更糟:Check Changeset 从事件载荷读 skip-changeset 标签:开 PR 后 5 秒内加标签,首个 run 永久红(重跑复用载荷)—— 一日三例 #5580 是「标签晚到 → 假红」,可以 rerun 收敛,是保守 失效;这条是假绿 ,没有任何后续步骤会把它纠回来。
它同时让 skip-changeset 这个明示豁免变得可选:真正需要打标的 PR 只要多等一会儿就自动绿了,打不打标没人知道。fix(deps): OSV override 选择器上界改到 major 边界,以后只挪 target #6117 就是活体标本 —— 它确实 需要 skip-changeset,而门禁已经不再要求它了。
可能的修法(未实现,留给裁决)
用 merge-base 代替钉死的 base.sha:git merge-base origin/$BASE_REF HEAD,只数这个 PR 自己那侧引入的文件。
或让 checkout 取 PR head ref (ref: ${{ github.event.pull_request.head.sha }})再对 merge-base 求差 —— 但这会改变本 job 其它步骤看到的树,需要一并核对 check-empty-changeset.mjs --base 与 check-changeset-no-major.mjs(两者也吃同一个 BASE_SHA,大概率有同款偏差)。
这两条选项都会改变门禁的判定面,且 check-empty-changeset.mjs 有自己的 --self-test,应当在同一单里补上「main 漂移不得影响计数」这条双向断言;⛔ 不要只改 workflow 而不动 self-test,否则这个漏洞下次会以另一种形状回来。
参考
本单由 #6095 的实现 agent 在跑 CI 时旁落发现,按 Prime Directive #10 单开、不认领、不在原 PR 里顺手修。
Generated by Claude Code
pr-automation.yml的 Check Changeset 门禁,在本 PR 一个 changeset 都没加的情况下会转绿,条件只是「本 PR 的base.sha被记下之后,main 又合进了带 changeset 的别人的 PR」。本仓一天合 ~18 个 PR,所以任何一个在飞久一点的 PR 都能白拿这张免检票。发现于 PR #6117(#6095 的实现单)。不是猜的,是同一个 PR 上前后两跑的实测对照。
机制
.github/workflows/pr-automation.yml的计数步骤:两个输入各自都合理,合在一起就漏:
BASE_SHA=github.event.pull_request.base.sha,它在 PR 创建时被钉住,后续synchronize不跟着 main 往前走。HEAD是actions/checkout@v7在pull_request事件上默认签出的 merge ref(refs/pull/N/merge),也就是「本 PR head 与当前 main tip 的合并」—— 它含有 main 上最新的一切。于是
BASE_SHA与 merge ref 之间的 main 漂移,整段都被--diff-filter=A记成「本 PR 新增的文件」。别人 PR 带进 main 的.changeset/*.md,在本 PR 眼里就是本 PR 加的 changeset。实测对照(PR #6117,同一个 PR,同一份 diff,零 changeset)
PR #6117 只改
pnpm-workspace.yaml+pnpm-lock.yaml,零package.json改动,.changeset/一个文件都没加。它本该一直红到有人打skip-changeset。Check for a changeset added by this PRbb4b42623base.sha(6513c1749)恰好就是 main tip,漂移为 0 →ADDED=0→ 正确判红680617108ADDED=2→ 假绿第二跑之后
Reject an empty-frontmatter changeset与Guard against accidental major bumps两步也跟着跑绿(job step 列表可见),即整条链都被这张假票放行。冒名顶替的两个文件,本地可复现(
6513c1749是 PR #6117 的base.sha):两个都不是 PR #6117 写的,一个字都不是。
危害
rerun也不是幂等的 —— 它替 PR 作者做了一次跟 PR 内容无关的掷骰子。skip-changeset标签:开 PR 后 5 秒内加标签,首个 run 永久红(重跑复用载荷)—— 一日三例 #5580 更糟:Check Changeset 从事件载荷读skip-changeset标签:开 PR 后 5 秒内加标签,首个 run 永久红(重跑复用载荷)—— 一日三例 #5580 是「标签晚到 → 假红」,可以 rerun 收敛,是保守失效;这条是假绿,没有任何后续步骤会把它纠回来。skip-changeset这个明示豁免变得可选:真正需要打标的 PR 只要多等一会儿就自动绿了,打不打标没人知道。fix(deps): OSV override 选择器上界改到 major 边界,以后只挪 target #6117 就是活体标本 —— 它确实需要skip-changeset,而门禁已经不再要求它了。可能的修法(未实现,留给裁决)
base.sha:git merge-base origin/$BASE_REF HEAD,只数这个 PR 自己那侧引入的文件。ref: ${{ github.event.pull_request.head.sha }})再对 merge-base 求差 —— 但这会改变本 job 其它步骤看到的树,需要一并核对check-empty-changeset.mjs --base与check-changeset-no-major.mjs(两者也吃同一个BASE_SHA,大概率有同款偏差)。check-empty-changeset.mjs有自己的--self-test,应当在同一单里补上「main 漂移不得影响计数」这条双向断言;⛔ 不要只改 workflow 而不动 self-test,否则这个漏洞下次会以另一种形状回来。参考
92750785815(红)与92753255340(绿)skip-changeset标签:开 PR 后 5 秒内加标签,首个 run 永久红(重跑复用载荷)—— 一日三例 #5580(标签竞态,已修,方向相反)、pr-automation.yml 的 allow-major 步骤仍从事件载荷读标签:#5580 同款竞态的孪生体(pre-mode 期间休眠) #5620(allow-major仍读载荷,孪生体)、objectui 可以在改动已发布包源码的同时不带 changeset —— 这些修复不会出现在任何发布记录里 #4904(objectui 侧「改发布包不带 changeset」)、空 frontmatter changeset 相对skip-changeset标签零收益、单向风险 —— 「禁止空 changeset 进 .changeset/」的决策证据(#5292 结案后无处存放) #5471 / 空 changeset 会静默卡死已 version 的发布:Release run 全绿,但 npm 和 Docker 什么都没发(17.0.0-rc.2 现在就卡着) #4898(空 changeset 路线).github/workflows/pr-automation.yml的Check for a changeset added by this PR步骤本单由 #6095 的实现 agent 在跑 CI 时旁落发现,按 Prime Directive #10 单开、不认领、不在原 PR 里顺手修。
Generated by Claude Code