Skip to content

fix(ci): Check Changeset 实时读 skip-changeset 标签,首个 run 不再永久红 (#5580) - #5625

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-5580-changeset-label-live-read
Aug 5, 2026
Merged

fix(ci): Check Changeset 实时读 skip-changeset 标签,首个 run 不再永久红 (#5580)#5625
os-zhuang merged 1 commit into
mainfrom
claude/issue-5580-changeset-label-live-read

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #5580

症状

.github/workflows/pr-automation.yml 的 changeset-check job 从事件载荷读标签:

if: >-
  !contains(github.event.pull_request.labels.*.name, 'skip-changeset')

github.event.pull_request.labels 是事件触发那一刻的快照。于是开 PR 后数秒内补
skip-changeset 标签时:

  1. opened 事件的 run 看不见标签 → 走计数路径 → 无 changeset → ;
  2. 标签落上后 labeled 事件的新 run → skipped(判定正确);
  3. 但首个红 run 永久红:rerun_failed_jobs 复用原事件载荷(pm-dispatch
    Operational notes 5),重跑多少次都看不见后来的标签。

一日三例:#5467(该门禁自己的修复 PR)、#5501#5577。每例的直接成本是一个需要人或
agent 停下来「认签名解释掉」的红检查 —— merge-queue-triage、PM 复核、事件订阅三方
会各自撞一次。

修法

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

重跑因此收敛:载荷仍是旧的,但实时读每次都重新查询。

四个设计决定

  • 容忍方向朝着执行。 标签读不到(API 报错、无 PR 号)判为 skip=false,即照常
    执行守卫。读不到输入的门什么也没验证,据此发豁免正是 check:react-declaration-parity 是唯一没接进任何 workflow 的源码审计门禁,且无 MANIFEST 时静默 skip 退出 0 —— 它现在永远不可能红 #4690 反模式(静默跳过、
    exit 0、看起来像「无违规」)。失败以 ::warning:: 明说,由计数步骤定论。
  • 实时读放在 checkout 之前。 标签在位时其后全部步骤跳过,整个 job 只花一次 API
    调用 —— 收敛到实时状态比它替掉的那个 stale 红更便宜。
  • 精确整行匹配(grep -qxF,here-string 而非管道)。被替换的 contains(数组, 'skip-changeset') 是数组元素精确匹配,子串匹配会让 skip-changeset-audit 这类
    标签新获豁免。here-string(与 release.yml 一致)让 grep -q 不进管道:-q 首个
    命中即关闭管道,写入端可能吃 SIGPIPE,在 set -o pipefail 下会把判定翻成 false。
    该步骤未写 shell:,两种方言都已实测。
  • 每步重复 guard,而非新增一个 gate job。 job 级 if: 读不到自己 job 的步骤;
    另起一个 gate job 会给本已在与检查列表噪音作战的仓库再添一行检查和一个全新的变红
    途径,而把标签判定塞进计数步骤里则要先付 checkout + install 才发现该 PR 豁免。

边界

验证

结果
pnpm check:workflow-status-functions(#5343/PR #5477 门禁) OK,22 workflow / 39 job / 19 个 job 级 if:;self-test 34 断言通过
pnpm check:nul-bytes OK,5533 个文件无裸控制字节;self-test 48 断言通过
pnpm check:node-version OK,23 个 setup-node 步骤仍全在 Node 22
YAML 结构解析 7 个步骤中 6 个带 guard(实时读步骤自身不带,它必须总是执行);BASE_SHA env 与计数命令逐字未变
步骤脚本分支矩阵 14/14 通过(见下)
前后决策真值表 7 格中仅 2 格改变,与事前预测一致(见下)

分支矩阵(从 YAML 抽出真实 run: 脚本,stub gh,两种 shell 方言)

跑的是 workflow 里那段脚本本身,不是重敲的副本。

场景 期望 bash -e bash -eo pipefail
载荷 stale、标签实时在位 —— #5580 本体 exempt PASS PASS
实时无该标签 enforce PASS PASS
完全没有标签 enforce PASS PASS
API 失败(HTTP 403) enforce + warning PASS PASS
无 PR 号(gh 一次都没调用) enforce + warning PASS PASS
近似标签名(skip-changeset-audit / no-skip-changeset) enforce PASS PASS
401 个标签含目标(pipefail 压力) exempt PASS PASS

反向验证

GitHub 表达式语言无法在本地执行,所以前后决策函数是建模的(shell 那一半是真跑
的)。方向先预测后运行:应当只有一格改变。

场景 载荷 实时 BEFORE AFTER
开 PR 前就带标签,仍在 has has skipped skipped .
开 PR 后约 5s 补标签 —— 本 bug none has enforce exempt YES
该红 run 的 rerun_failed_jobs(同一份冻结载荷) none has enforce exempt YES
全程无标签、无 changeset none none enforce enforce .
开 PR 后移除标签(载荷仍带) has none skipped skipped .
该移除触发的 unlabeled 事件 none none enforce enforce .
标签实时在位但 labels API 读不到 none n/a enforce enforce .

实际改变 2 格,而非 1 —— 因为「首 run」与「它的重跑」是同一个缺陷的两次,预测里写的
就是这两格;其余 5 格逐格不变。

保留 fast-path 留下的唯一反向 stale 格已如实记在文件里:标签在开 PR 后被移除时
本 run 仍短路,即凭快照放行。该格自愈 —— 移除标签必然触发 unlabeled 事件,它起的
run 两处都看不到标签而照常执行。#5580 那个方向没有这种救援:labeled run 的绿不会
清掉 opened run 的红。这是保留 fast-path 换来常规路径零成本的代价,值得,但下一个
读者应当看见它,所以写进了注释而不是只写在这里。

本 PR 自身


Generated by Claude Code

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

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

- **容忍方向朝着执行**:标签读不到(API 报错、无 PR 号)判为 `skip=false`,即
  照常执行守卫。读不到输入的门什么也没验证,据此发豁免正是 #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 两处都看不到
  标签而照常执行;#5580 那个方向没有这种救援(`labeled` run 的绿不会清掉
  `opened` run 的红)。文件内注释写明了这笔交换。

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

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

Co-authored-by: Claude <noreply@anthropic.com>
@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 8:48pm

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 自己成了活体验证:重跑从红收敛到绿

PR body 里那条是预测,现在有实测了 —— 而且撞的正是 #5580 的核心格。

同一个 run(31045832140,event pull_request,即 opened)、同一个 head SHA
(57279e9bd)、两次 attempt:

attempt 时刻 Check Changeset 为什么
1 20:48:39 起 failure(job 92440984751) 实时读发生在 ~20:48:45,那时 skip-changeset 还没落上 → skip=false → checkout + install → 计数 0 → 红(20:49:28)
2 20:49:55 起(rerun_failed_jobs) success(job 92441341603,5 秒) 载荷仍是那份冻结的、不含标签的 opened 载荷,但实时读这次看见了标签

attempt 2 的实时读步骤日志(逐字):

env:
  GH_TOKEN: ***
  PR_NUMBER: 5625
Labels on PR #5625 right now: ci/cd
size/s
skip-changeset
[notice] 'skip-changeset' is on PR #5625 (read live, not from the event payload),
         so this PR declares no release of its own and the changeset check is exempt.

这正是 #5580 判定为不可能的那一步。 旧逻辑下 attempt 2 读的还是同一份冻结载荷,
必然再红一次,而且永远红 —— 一日三例都卡在这里。

两个顺带被实测确认的点:

  • 权限够用:gh api repos/.../pulls/5625 在该 job 现有的 pull-requests 权限下
    直接成功,没有拓宽任何 scope,也没走 issues/{n}/labels(那个端点的权限映射另算)。
  • 豁免路径真的只花一次 API 调用:attempt 2 的日志归档里只有 Set up job、实时读、
    Complete job 三个步骤文件 —— checkout / setup-node / corepack / install / 计数 /
    no-major 六步全部 skipped,没有产生日志。整个 job 5 秒完成。

残留的那一半,如实说明

attempt 1 仍然红,这不是缺陷而是本修法的边界:实时读把读取时刻从「事件创建」推到
「job 启动」,但它仍是一个时点读 —— 标签在该时点之后才落上,这一 run 依旧会红。
修掉的是永久那一半:重跑现在会收敛。要连 attempt 1 都绿,得让 job 等待标签或
在计数失败后重读一次,那是另一种设计(会把「作者根本没打算加标签」的正常红也拖慢),
不在 #5580 范围内。


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review August 5, 2026 20:56
@os-zhuang
os-zhuang enabled auto-merge August 5, 2026 20:56
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 5, 2026
Merged via the queue into main with commit a9c7e6b Aug 5, 2026
23 of 25 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-5580-changeset-label-live-read branch August 5, 2026 21:11
os-zhuang pushed a commit that referenced this pull request Aug 5, 2026
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 从事件载荷读 skip-changeset 标签:开 PR 后 5 秒内加标签,首个 run 永久红(重跑复用载荷)—— 一日三例

2 participants