fix(ci): Check Changeset 的 skip-changeset 判定加一次「结算读」,首跑不再结构性必红 (#6378) - #6429
Conversation
Check Changeset 只有一次活标签读取,实测在 PR 创建后约 +10s 触发(PR #6426 的 opened 跑 31204438874:PR 17:53:57Z 建立,读标签步骤 17:54:07Z 起跑), 而 skip-changeset 标签只能在 PR 建立之后才打得上,实测落地在 +10..45s。 两者恒定重叠,于是走标签路线的 PR 首跑几乎必红,今日一天复现 22 次。 本次把「计数」与「判定」拆开,并在两者之间插入第二次活标签读取(结算读): * 计数步骤只产出 added,不再自己下红结论; * 结算读只在 `fast-path 无标签 且 added == 0` 时运行 —— 也就是只向 「本来就要变红」的 PR 收取等待成本。写了 changeset 的 PR 完全跳过该步, 既不等待也不多花一次 API 配额; * 窗口自 PR 创建时刻起算 120s(约为实测最坏标签延迟 40s 的三倍),因此 synchronize / labeled / rerun 重放时截止点早已过去,只读一次、不睡眠。 门禁没有被放宽:结算读只有在「真的看见标签」时才写 skip=true,其余所有出口 (无 PR 号、标签读不到、窗口关闭、重试上限)一律写 skip=false 即强制执行。 无 changeset 且无 skip-changeset 的 PR 仍然 exit 1。 check-empty-changeset.mjs 的 CONSUMER 断言同步补齐(36 -> 43 条),钉住 「豁免只能由真实标签建立、等待只能向将红的 PR 收取、每个判定步骤必须同时 认两次读取、失败必须仍是失败」;七条消融全部转红,无空绿。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BDmDsu2575gDxeMCxXhDE3
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 31207566794 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
Queue steward — yielding (队列管家让行), no re-queue action takenThis PR was kicked out of the merge queue by run 31207566794 (generation Signature (from the full log archive, not a tail): Attribution — not this PR's diff. The failing test lives in Disposition: the PR re-entered the queue at 18:48:43Z as generation This signature is not yet in the #5810 ledger — a ledger-upgrade petition has been filed on the anchor issue (the ledger is human-upgrade-only; this seat only petitions). Generated by Claude Code |
Fixes #6378
1. 根因复核:时序证据
立单说的是「job 起跑那一刻就读标签」。复核后需要修正一处措辞——
Check Changeset早在 #5580 就已经不再读事件负载,而是有一次活标签读取。问题不在于它读的是快照,而在于它读得太早,且只读一次。实测取自 PR #6426 的
opened跑(run 31204438874,job 92951930923):而
skip-changeset只能在 PR 建立之后才打得上,立单实测其落地在 +10..45s(#6310 约 +15s,#6358 约 +35..45s)。一次 +10s 的读取恒定落在竞态窗口内——这就是「结构性必红」的机制,也是为什么 #5542 打了标签仍红、#5650 标签早到 41s 就绿:那是运行器启动快慢在决定判决。这里还顺带证伪了一个我自己的工作假设:我原本打算「把等待藏在 install 后面,成本为零」。实测否掉了——整个 job 只有 43s,走到判定步骤时距 PR 创建也才 +45s。这个仓库里没有一个足够慢的步骤可以藏住等待。所以等待不能靠藏,只能靠条件化。
2. 并发修复检查(Operational note 8,两次)
git log origin/main -- .github/workflows/pr-automation.yml81a4a67(#6342)、613c515(#6192)、b030c9a(#6059)、d97f2a2。均已在 main,无在飞并发修复git log 5faa23c..origin/main -- (本单文件面)5faa23c推进到7618ee8,但该区间内没有任何提交触及本单文件面,零冲突另核对在飞 PR 列表(#6426 / #6424 / #6423 / #6389 / #6279 / #6208),无一触及
pr-automation.yml。3. 方向选择
选定:方向 1 的条件化变体——保留原有的快路径读取,在计数之后、判定之前插入第二次活标签读取(「结算读」),并且这次读取只在该 PR 本来就要变红时才发生。
拆成三步:
Count the changesets this PR adds(原判定步骤,改为只产出added,不再自己下结论)Settle the skip-changeset window——if:为「快路径无标签」并且「added 等于 0」Require a changeset (or the skip-changeset label)——纯判定关键在第 2 步的
if:。写了 changeset 的 PR 完全跳过该步:不等一秒,不花一次 API 配额。等待只向「本来就要被判红」的 PR 收取,而把一个红推迟至多一个窗口,代价是零。这正面回答了立单对方向 1 的成本质疑(「把等待成本转嫁给每次运行」),而不是回避它。窗口自 PR 创建时刻起算 120s(约实测最坏标签延迟 40s 的三倍),轮询间隔 10s。锚在创建时刻使它自动失效:
synchronize、labeled、任何rerun_failed_jobs重放时created_at早已过去,截止点已在身后,循环只读一次、不睡眠。被否决的方向
方向 2(改由
labeled事件唯一判定)——漏洞假设已证伪,即:漏洞是真的,方向不可取。 立单要求先确认「一个从头到尾没有任何标签动作的 PR 是否仍会被判到」。答案是不会:GitHub 的labeled事件只在标签实际写入时触发,一个没有任何标签动作的 PR 不产生该事件。而本仓恰恰存在这种形状——.github/labeler.yml无匹配路径、且 diff 小到不触发 size 标签的 PR,全程零标签动作。若把唯一判定移到labeled,这类「真的忘了写 changeset」的 PR 将一次都不会被判。这是本单最危险的一条,故弃。方向 3(推迟到 ready-for-review / 首次 synchronize) 弃:本仓流程是「PR 先 DRAFT、验收后翻 ready」,但一个建立后直接翻 ready 且再无推送的 PR 不产生
synchronize;且ready_for_review未在on:里,加它等于改触发面,还要牵动 required 集的语义(本单禁项)。方向 4(标签缺失时输出 neutral 而非 failure) 弃:这是把门禁降级成提示,直接违反本单硬约束。
4. ⛔ 硬约束:门禁没有被放宽
结算读只有在真的看见标签时才写
skip=true。其余每一个出口——无 PR 号、标签读不到、窗口关闭、重试上限——一律写skip=false,即强制执行,与 #4690 的既定方向一致。无 changeset 且无skip-changeset的 PR 仍然exit 1。5. 反向验证(先申报,后执行)
声明 A(竞态消失)— 极性:肯定式,会红
申报:本 PR 自身走 skip-changeset 路线,
pull_request事件取 merge ref 版本的 workflow,故本 PR 即活冒烟——首跑就该绿,不需要 rerun。实测结果(活体,首跑,已达成):本 PR 建立于
18:22:52Z,skip-changeset于约18:23:2xZ打上。opened那一跑的Check Changeset(run 31206681669 / job 92959379121)首跑success,18:23:01Z 起 18:23:44Z 止,未做任何 rerun。job 日志:关键在于结算读步骤当时确实执行了——而它的
if:只在「快路径没看见标签」且「added 为 0」时才成立。也就是说:约18:23:05Z的快路径读取没读到标签(标签尚未落地),是18:23:42Z的结算读接住了它。换成改动前的 workflow,这一跑必红。 竞态在它自己的修复 PR 上被当场复现、当场吃掉。附带一格实测:
Auto Label于18:23:38Z完成其整组 PUT(新增ci/cd),标签读回为["ci/cd", "skip-changeset"]——本次 #5533 式覆写没有发生,结算读在该 PUT 之后仍读到标签。日志里的
shell: /usr/bin/bash -e {0}也实测确认了本文件注释的说法:无 pipefail。声明 B(门没被放宽)— 极性:肯定式,会红
申报:构造「无 changeset 且无 skip-changeset」的形状,必须红。
实测(不是推理):把 workflow 里两个新步骤的
run:原样抽出,用桩gh在bash -e -o pipefail下真跑,11 条全过:其中第 4 条正是本单的竞态:标签在第 3 次读取时才出现,结算读仍判为豁免。第 2、3 条与末尾
added=0得rc=1合起来就是硬约束的实测证据。(注:桩测跑在
-o pipefail下,比 GitHub 默认更严;实际 workflow 无shell:键,默认bash -e无 pipefail——这一点在核对计数步骤时反过来咬了我一次,我最初的本地探针误加了 pipefail,导致空 diff 时grep -v返回 1 而整条管道失败。已按真实语义重跑。)声明 C(既有 CONSUMER 断言未变陈旧)— 极性:肯定式,会红
申报:
check-empty-changeset.mjs里直接读 workflow 的既有断言,改后必须仍然真实反映 workflow 现状;不能只是「还绿」。风险点是真实存在的:我把
Check for a changeset added by this PR改名为Count the changesets this PR adds并移动了计数行。若既有断言锚在步骤名上,它现在就已陈旧。实测:把 #6129 重新植入改名后的计数步骤(
$MERGE_BASE换回$PINNED_BASE_SHA),自测立刻转红:⇒ 既有断言锚在 diff 拼写而非步骤名上,穿过了我的重构仍然咬得住,不是「看不见我的改动」。
空绿自查(消融七条,全部转红)
新增断言若不会因构造被拿掉而转红,就是幻检。逐条消融实测:
-qxF改-qF)exit 0continue-on-error零空绿。 自测条数 36 改为 43。
6. 门禁 EXIT 表
node scripts/check-empty-changeset.mjs --self-test✓ 43 assertions over real temp git repos (real scan() path)pnpm check:workflow-status-functions✓ 34 assertions;OK (22 workflow files, 41 jobs, 24 job-level if:; 9 read needs.*.outputs.*, all naming a status function)parsed 22 workflow files, 0 errorsrun:块bash -nBAD COUNT 0pnpm check:nul-bytes✓ 56 assertions;OK (scanned 6067 tracked text files)grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]'npx eslint scripts/check-empty-changeset.mjs另:用真实 git 跑通计数步骤——本分支
added=0,回退 40 个提交为基准时added=26。CI 侧(本 PR):ESLint
success、TypeScript Type Checksuccess、Check Changeset首跑success。7. 不在本 PR 里
content/docs/releases/**。Resolve the diff base仍只带快路径守卫:它的exit 1是「git 基准不可用」,不是 changeset 判决,本就不受标签豁免——如实记录而非顺手扩大。CONSUMER 断言按「跑check-*.mjs或发出 no-changeset 错误」来圈定判定步骤,该步骤被有意排除在外。opened那跑的红不会自行清除(labeled那跑会绿,但绿不清红)。这是 Check Changeset 从事件载荷读skip-changeset标签:开 PR 后 5 秒内加标签,首个 run 永久红(重跑复用载荷)—— 一日三例 #5580 就记录过的方向,本 PR 把窗口从「约 10s」放宽到「120s」,未声称消灭它。allow-major那半仍读冻结负载(pr-automation.yml 的 allow-major 步骤仍从事件载荷读标签:#5580 同款竞态的孪生体(pre-mode 期间休眠) #5620),照旧不在本单范围。skip-changeset标签路线。