Skip to content

fix(ci): 两处聚合门禁改数逐分片正面凭据 —— declared = enforced (#6082) - #6194

Merged
hotlong merged 2 commits into
mainfrom
claude/issue-6082-shard-positive-attestation
Aug 7, 2026
Merged

fix(ci): 两处聚合门禁改数逐分片正面凭据 —— declared = enforced (#6082)#6194
hotlong merged 2 commits into
mainfrom
claude/issue-6082-shard-positive-attestation

Conversation

@hotlong

@hotlong hotlong commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Fixes #6082

按 2026-08-07 02:07Z 维护者裁决(comment 5211251003)实施 方案 C:两处聚合门禁改为数逐分片正面凭据(declared = enforced)。⛔ 方案 A(白名单加 abandoned)已被否决,本 PR 不含它;方案 D 是管家台账面,与本 PR 无码面交集。

一、被替换的缺陷

test-gate / dogfood-gate 原本只读一个标量 needs.*.result(矩阵聚合读数),命中 success|skipped|cancelled 放行、其余落 *) 判红。一个标量装不下三个分片的判决,前一轮 dev 的实测(comment 5208599210)给出两条反例:

# 实测 run 事实
1 31120902911 runner 饥饿下分片被逐个丢弃(runner_id: 0started_at == created_atsteps 数组根本不存在 ⇒ 零测试执行),聚合读到未文档化的 abandoned ⇒ 假红,31 秒后 #6010 被踢出队列。该 run 并非 moot —— 队列仍在实时消费它的判决,所以放绿 = 对零测试执行发布 Test Core: success,正是 #4928 要挡的形状。
2 31114735713 分片 Test Core (3/3) job 级 conclusion: failure 在场,同 run 聚合门禁却读到 abandoned聚合里的任何生命周期态都会吞掉兄弟分片的真实判决 —— 问题从来不是「少了一个词」。

二、机制选型:制品(artifacts),而非 job outputs

每个真实分片 job 以if: 的最后两步发布凭据;门禁下载本 run 的同名制品并要求凭据集合 == 矩阵声明的花名册

  • 为什么不用 job outputs:GitHub 明文记载矩阵 job 的 outputs 互相覆盖(消费者看到的是最后完成的那条腿),needs.*.outputs.* 在结构上装不下三条腿的凭据。社区常用的「空值不覆盖」是未文档化的合并副作用 —— 把合并判决押在未指定的 runner 细节上,恰恰是本单要修的那类错误(abandoned 本身就是未文档化枚举漏出)。⇒ 否决。
  • 为什么不用 actions/cache:同属「每条腿可写、下游可读」的通道,但按本仓自身的理由否决 —— 10 GB 缓存池已被精打细算(见 ci.yml Restore Turbo cache 注释:PR 侧写入曾驱逐 main 的 turbo 种子,Build Core 51s → 4m30s 是实测事故)。
  • 权限面:同 run 下载制品无需任何新权限(actions: read 只用于跨 run / 跨仓),已核对 actions/download-artifact@v7 的 README 与 action.yml(pattern / merge-multiple / github-token 的语义);两个门禁的 permissions: contents: read 原样未动
  • 未被调度的分片:凭据由分片 job 自己发布,一个从未被派到 runner 的 job 不会产生任何东西 ⇒ 计数天然短缺 ⇒ 判红。这正是选型要求的性质。

凭据两步为什么必须是最后两步且不带 if::不带 if: 的步骤只在此前每一步都成功时才运行,而它们之后没有任何步骤能在凭据已计入通过之后再把 job 翻红。check:shard-attestation 静态强制这两条性质。

三、期望 N 的推导(#4928 语义不回退)

逐腿推导,不读 filter 的 output:

该腿的聚合 result 期望 N 判据
skipped 0 filter 合法地判定本族无事可做 ⇒ 花名册归零。但只在 filter 自身 success(或 cancelled)时放行 —— #4928 守卫逐字保留。
cancelled 0(且允许已完成腿的凭据存在) #3668:生命周期态而非分片判决;真败对 cancelled 支配性(run 30271824408 实测),放绿不掩盖回归。
其余(success / abandoned / 任何未来漏出值) 矩阵声明的分片数 凭据集合必须逐一相等

即「filter 说跳过 ⇒ 期望 N 相应归零、skipped-by-filter 仍是通过」被保留,且这条推导比读 output 更硬:它不可能与实际发生的事情不一致。

被声明的 failure 仍是一票否决,即便花名册齐全 —— 一条腿可以先发凭据、再被 post-step(缓存保存的 post action 之类)翻红。计数是必要条件,不是充分条件。其余取值一律只由计数决定,这正是它对「下一个未文档化生命周期值」免疫、而不需要再往词表里加词的原因。

四、支配性实验(#3668 先例形)

可判定的核心抽到 scripts/check-shard-attestation.mjs(家族惯例:--self-test + 静态扫描;check:stall-guard 是同形先例 —— 既在 CI 里真跑、又自带自测)。纯 YAML + 不可测 shell 通不过本 PR 自己在实施的「declared = enforced」标准,故抽脚本。

四向实验通过真实 --verify CLI(不是纯函数)驱动,方向在跑之前先行预测:

### (i)   all 3 declared shards attested   [aggregate=success filter=success] expected: PASS
    + test-1-of-3  (run 31120902911, attempt 1)
    + test-2-of-3 / + test-3-of-3
    attested 3 / 3 declared shard(s)
Test Core gate satisfied: all 3 declared shard(s) published a positive attestation.
>>> PASS (exit 0) — as predicted

### (ii)  PIN abandoned-not-moot: shard 3 discarded, runner_id 0, zero tests   [aggregate=abandoned] expected: FAIL
    + test-1-of-3 / + test-2-of-3 / - test-3-of-3  MISSING
::error::Test Core: 1 of 3 declared shard(s) of test published no positive attestation (test-3-of-3).
>>> FAIL (exit 1) — as predicted

### (iii) one shard genuinely failed   [aggregate=failure] expected: FAIL
::error::… published no positive attestation (test-3-of-3).
::error::Test Core: leg test reported result 'failure' — a declared negative is never overridden by a full roster.
>>> FAIL (exit 1) — as predicted

### (iii-b) PIN real-failure-swallowed: shard 2 conclusion=failure, aggregate reads abandoned   expected: FAIL
    + test-1-of-3 / - test-2-of-3  MISSING / - test-3-of-3  MISSING
::error::Test Core: 2 of 3 declared shard(s) … (test-2-of-3, test-3-of-3).
>>> FAIL (exit 1) — as predicted

### (iv)  family skipped by the filter (#4928 legitimate half)   [aggregate=skipped filter=success] expected: PASS
    satisfied (skipped by the filter — #4928; expected attestations: 0)
>>> PASS (exit 0) — as predicted

### (iv-b) #4928 guard: skipped while filter failed   [aggregate=skipped filter=failure] expected: FAIL
::error::Test Core: leg test was skipped while the filter job did not succeed (filter result: failure). Refusing to report a pass over zero runs — see #4928.
>>> FAIL (exit 1) — as predicted

### (v)   #3668 superseded run cancelled   expected: PASS
    satisfied (cancelled — run-lifecycle state, #3668; expected attestations: 0)
>>> PASS (exit 0) — as predicted

### (vi)  full roster + declared failure (post-step red)   expected: FAIL
    attested 3 / 3 declared shard(s)
::error::Test Core: leg test reported result 'failure' — a declared negative is never overridden by a full roster.
>>> FAIL (exit 1) — as predicted

八向全部与预测一致。同样的四向 + 本单两条反例钉 + #4928 守卫三种坏 filter 取值,已作为命名用例进 --self-test(共 49 条断言),脚本内注释直接引 #6082 与两个 run 号。

逆向的诚实说明:本 PR 不会#6010 那类被踢的 PR 转绿 —— 分片确实零执行,判红是正确的真负例,这一点裁决已明确(治驱逐是已批的 D,归管家台账)。C 的收益是另外两件:①「兄弟分片被丢弃 ⇒ 聚合吞任意红分片」这个更宽缺陷在结构上消失;②「分片没跑却报绿」从「靠词表没写对」变成「凭据不存在,数不出来」。

五、静态漂移守卫(pnpm check:shard-attestation,已接入 lint.yml)

GitHub 无法在 strategy.matrix 与下游 job 的步骤之间共享同一个字面量(env 上下文在 strategy 中不可用),所以两处字面量在这里对账,而不是靠记。扫描 .github/workflows/ci.yml 断言:

六、必需检查名稳定 + always-report 存活(#5617 / #3622)

  • 名字:git diffname:if: 两个键一行未动;解析后复核:test-gate => name: "Test Core" | if: "always()"dogfood-gate => name: "Dogfood Regression Gate" | if: "always()",分片 job 名 Test Core (${{ matrix.shard }}/3) / Dogfood Regression Gate (${{ matrix.shard }}/3) / Dogfood Verify CLI 同样逐字保留。
  • always-report:两个门禁仍是 if: always() 的纯 job(未改成 if: !cancelled()),因此在每个 PR 与 merge_group 上必定发布上下文。新增的 checkout / download 步骤只可能让该上下文变红(fail-closed),不可能让它消失 —— 而 chore(ci): cut PR wall-clock ~9.5min → ~5-6min — dedupe dogfood, shard it, cache lint's build #3622 的死锁来自上下文缺席,不是来自它是红的。静态守卫把 if: always() 本身也钉住了。

七、merge_group 一致性

设计中没有任何一处依赖 github.event_name:凭据步骤、上传/下载、门禁判据在 pull_request / merge_group / push 上完全同形。唯一与事件相关的既有步骤是 Save Turbo cache (main only)(if: always() && github.event_name == 'push'),它位于凭据两步之前;在 PR / merge_group 上它是 skipped(不是 failed),而隐式 success() 只关心「此前有没有步骤失败」,所以凭据步骤照常运行。制品是 run 级资源,与触发事件无关。

八、验证

✓ check-shard-attestation --self-test: 49 assertions (dominance experiment + both #6082 counter-examples + the #4928 guard).
✓ check-shard-attestation: 2 aggregate gate(s) count 3 declared leg(s) across 3 attesting job(s).

✓ check-workflow-status-functions --self-test: 34 assertions over temp fixture roots (real scan() path)
check-workflow-status-functions: OK (scanned 22 workflow file(s), 41 job(s), 24 job-level if: expression(s);
  9 read needs.*.outputs.*, all naming a status function).

✓ check-nul-bytes --self-test: 56 assertions over a temp git repo (real scan() path)
check-nul-bytes: OK (scanned 5920 tracked text file(s); no raw ASCII control bytes).

eslint scripts/check-shard-attestation.mjs --no-inline-config  → exit 0
yaml parse: ci.yml OK (10 jobs) / lint.yml OK (2 jobs) / package.json OK

check:workflow-status-functions 与本改动无交集:其脚本头明确把 needs.*.result / needs.*.conclusion 划在范围外(「Those ARE status reads … This gate is about the silent compression of status into data」),而门禁读的正是 result;本 PR 也没有新增任何 job 级 if:needs.*.outputs.*

九、无 changeset

.github/workflows/** + scripts/** + 根 package.json(private: true)脚本一行,不发布任何包 ⇒ 打 skip-changeset 标签,不写空 changeset。


Generated by Claude Code

`test-gate` / `dogfood-gate` 原本只读一个 `needs.<matrix>.result`:命中
`success|skipped|cancelled` 放行,其余落 `*)` 判红。一个标量装不下三个分片
的判决,实测两个缺陷:

- run 31120902911:runner 饥饿下分片被逐个丢弃(runner_id 0、无 steps、零
  测试执行),聚合读到未文档化的 `abandoned` ⇒ 假红,31 秒后 PR #6010 被踢
  出合并队列。直接把 `abandoned` 加进白名单(方案 A)已被维护者否决:该 run
  并非 moot,队列仍在实时消费它的判决,放绿等于对零测试执行发布
  `Test Core: success` —— 正是 #4928 要挡的形状。
- run 31114735713:分片 `Test Core (3/3)` job 级 conclusion=failure,同 run
  聚合门禁却读到 `abandoned` —— 聚合里的任何生命周期态都会吞掉兄弟分片的真
  实判决。所以问题从来不是「少了一个词」。

改为数正面凭据:每个真实分片 job 以「无 `if:` 的最后两步」发布
`shard-attest-<job>-<n>-of-<N>` 制品(「我跑了且我通过了」),门禁下载本 run
的同名制品并要求凭据集合 == 矩阵声明的花名册。从未被调度的分片发不出凭据,
失败的分片同样发不出 —— 吞噬面在结构上消失。`cancelled`(#3668 生命周期态,
真败对其有支配性)与 filter-`skipped`(#4928,期望 N 归零)语义原样保留;
被声明的 `failure` 仍是一票否决,因为 post-step 可以在凭据发布之后翻红。

选制品而非 job `outputs`:GitHub 文档明确矩阵 outputs 互相覆盖(消费者看到
最后完成的那条腿),靠「空值不覆盖」这一未文档化合并行为搭门禁,等于重犯本
单要修的错。同 run 下载制品不需要 `contents: read` 之外的权限,门禁权限块
未变;`actions/cache` 这条同类通道按本仓自身理由否决(10 GB 池已被 turbo 种
子紧张占用)。

可判定的核心抽到 `scripts/check-shard-attestation.mjs`(家族惯例:
`--self-test` + 静态扫描),49 条断言含 #3668 形的支配性实验四向、本单两条
实测反例各一钉、#4928 守卫不回退。静态扫描把门禁花名册钉死在矩阵声明上,并
强制凭据两步永远是 job 的最后两步且不带 `if:`。

分片 job 名与 `Test Core` / `Dogfood Regression Gate` 两个必需检查上下文名、
两处 `if: always()` 全部逐字未动。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BDmDsu2575gDxeMCxXhDE3
@vercel

vercel Bot commented Aug 7, 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 7, 2026 10:40am

Request Review

@github-actions github-actions Bot added the size/l label Aug 7, 2026
@hotlong hotlong added skip-changeset PR has no user-facing published change; bypasses the changeset gate and removed size/l labels Aug 7, 2026 — with Claude
@github-actions github-actions Bot added ci/cd dependencies Pull requests that update a dependency file labels Aug 7, 2026
`Dogfood Regression Gate gate satisfied` —— 门禁名本身以 Gate 结尾,模板再拼
一个 "gate" 就重了。改为 `<门禁名>: satisfied — …`。纯日志文案,判据未动;
49 条自测与八向支配性实验重跑全绿。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BDmDsu2575gDxeMCxXhDE3
@github-actions github-actions Bot added the size/l label Aug 7, 2026
@hotlong
hotlong marked this pull request as ready for review August 7, 2026 10:51
@hotlong
hotlong added this pull request to the merge queue Aug 7, 2026
Merged via the queue into main with commit 42e0aa7 Aug 7, 2026
24 checks passed
@hotlong
hotlong deleted the claude/issue-6082-shard-positive-attestation branch August 7, 2026 11:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cd dependencies Pull requests that update a dependency file size/l skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants