feat(ci)+test(packaging): GPU 证据接进 CI:自测无条件跑,无 GPU 显式跳过(跳过≠通过)

新增 `.github/workflows/gpu-evidence.yml`:`selftest` 无条件跑 `--self-test` 与三个退出码
(0 过 / 1 判据不过 / 2 用法错,用真 CLI 走一遍);`probe` 只探测可用产物与硬件适配器;
`evidence` 的 job 级 `if:` 要求 `needs.probe.outputs.available == 'true'`,所以没产物时它在
Actions 里是灰色 skipped(不是一个绿色 ✓),原因写进 job summary 并打 `::warning::`。
新开文件而不是并进 `guards.yml`:那份已 399/400 行,`release-build.yml` 正好 400。

新增 `packaging/tests/gpu_shot_ci_selftest.py`:把接线口径变成机器判据(自测步不许有 `if:`、
真判据必须挂门、跳过必须 summary+`跳过`+`::warning::`+`$GITHUB_OUTPUT`、禁 `continue-on-error`、
判据命令不许 `|| true`),并带 5 条变异向量证明每种「改绿」写法都会当场红。

`packaging/docs/gpu-evidence.md` §8 记账(含「CI 上从没真跑过」)与 `packaging/README.md` 索引更新。
This commit is contained in:
AskaEth committed 2026-11-11 17:53:53 +08:00
1 parent 9234c1eeae
commit 075769c6d2
4 files changed
+503 -3

No files matched your search

+60 -2
View File
@@ -7,6 +7,9 @@
一句话:这套判据把「这张图是 GPU 档出的」从**人读日志**变成**可复跑的退出码**,但它证明的是
「像素由哪条光栅化路径产出」,**不是**「用户看到的窗口长这样」。第 4/5 节逐条划线。
**CI 接线(M253)在第 8 节**:`--self-test` 无条件跑,真实产物判据只在有可用产物时跑;
**无 GPU 时显式跳过并写明原因,跳过与通过分开写**(跳过 ≠ 通过)。
## 1. 怎么跑
```text
@@ -127,9 +130,64 @@ M246 本机实测是 **5.69% / 200**(gallery)与 **2.13% / 75**(shell)
## 7. 没验的部分(M249 自己)
- **没接进 CI**:本脚本目前**只手动跑**,`.github/workflows/**` 一行未动。接 CI 是另一条 mission
(需要 runner 上有真 GPU 或接受「跑不起来就跳过」,是个策略决定,不该顺手改 workflow)。
- **M249 当时没接进 CI**:那一轮 `.github/workflows/**` 一行未动(接 CI 是策略决定:runner 上有真 GPU,
还是接受「跑不起来就跳过」)。**M253 已接上**,见 §8;本节其余条目原样保留。
- **没在任何非本机环境跑过**:Windows + 上述单卡 + RDP 断开会话之外,Linux/macOS/其它 GPU 全没跑。
- **没改 Rust**:判据只读现有日志格式。若产品将来改了 `[shot]` / `Selected:` 的行格式,这里的正则
会同时失效(`--self-test` 用的是**按现格式手拼**的合成日志,抓不到格式漂移——它守判据,不守格式)。
- **没验 `--diff` 阈值化**:本文第 4 节引的是 M246 的读数,本 mission 没有重跑 `--diff`。
## 8. CI 接线(M253):无 GPU 时显式跳过,**跳过 ≠ 通过**
判据靠 **`.github/workflows/gpu-evidence.yml`** 跑(`push: main` / `pull_request` / `workflow_dispatch`)。
**为什么新开一个文件而不是并进 `guards.yml`**:`guards.yml` 已是 399 行、红线 400(`file-size` 那道门
就扫 `*.yml`),挂一个 job 必越线;`release-build.yml` 正好 400 行、动它得先拆。与 `release-guard.yml`
当初单独成文的理由逐字相同。
三个 job,各管一件事:
| job | 什么时候跑 | 它的结论代表什么 |
|-----|-----------|-----------------|
| `selftest` | **无条件**(不需要 GPU、不需要真产物) | 「尺子合格」:19 条向量 + 三个退出码 0/1/2 用真 CLI 走一遍 |
| `probe` | 无条件,只探测、不判定 | 工作区里有没有可判的产物;没有就把原因写进 job summary 并打 `::warning::` |
| `evidence` | `if: needs.probe.outputs.available == 'true'` | 「这张图真是 GPU 档出的」:`check_gpu_shot.py` 的退出码(0 过 / 1 判据不过 / 2 用法错)即结论 |
**跳过与通过分开写**:没有可用产物时 `evidence` job 的 `if:` 不成立 ⇒ 它在 Actions 里是**灰色
skipped**,不是一个绿色 ✓;同时 `probe` 打出(本地手模拟的原样输出,见 §8.1):
```text
GPU 证据:无硬件适配器(/dev/dri 下没有 renderD*,也没有可用的 nvidia-smi),且工作区里没有可用的样例产物
(约定路径要求 stderr 日志与同次运行的 PNG 同时存在;DevDocs/ 被 .gitignore 忽略,图不进仓库;event=push),
跳过真实产物判据(判据实现自测已过:那证明「尺子合格」,不证明「这张图是 GPU 出的」)
```
外加一条 `::warning::GPU 证据:跳过(…)—— 本次运行没有判过任何真实产物,不构成「GPU 证据已验证」`。
`available` 同时作为 job output 交给 `evidence`,所以「跳过」与「通过」在数据流上也是两条路。
四条路径的口径:
| 情形 | 结果 | 为什么这么定 |
|------|------|-------------|
| 工作区里有 `DevDocs/artifacts/gpu-probe/gpu-shot.stderr.log` + `gpu-shot.png`(约定成对) | `available=true` ⇒ `evidence` 真判 | 一次取证 = 一份干净的 stderr + 同次运行的那张图(§5 第 1/7 条);**只给一半仍按跳过**(半套产物不算可用) |
| `workflow_dispatch` 填了 `shot_log` / `shot_png` | 同上 | 显式点名 ⇒ 真判 |
| 点了名但**文件不存在**(或正例没给 PNG) | **红**(`::error::` + exit 1) | 「明确说要判、东西不在」若能退化成跳过,就又是一次假绿 —— 有意为之的输入缺失是错误,不是 skip |
| 托管 runner 常态(无 GPU + 无产物) | 跳过 + 原因 + 黄条 | 见上;托管 runner 本来就没有硬件适配器 |
**接线不许被改成「总是绿」**:`packaging/tests/gpu_shot_ci_selftest.py` 把这条口径变成机器判据 ——
自测步不许有 `if:`、真实判据必须挂在 `outputs.available == 'true'` 上、跳过那步必须同时写
`$GITHUB_STEP_SUMMARY` + `跳过` + `::warning::` + `$GITHUB_OUTPUT`、全文件不许 `continue-on-error`、
判据命令不许 `|| true`。它自带 5 条变异向量:把上面每种「改绿」写法注入临时副本,对应判据必须当场红。
### 8.1 这一节**没验**的部分
- **CI 上从没真跑过**:本 workflow 是本地写的,**一次 GitHub Actions 运行都没有**(未 push)。
本地做的是**手模拟**:把三个 step 的 `run:` 原文抽出来用 bash 跑(`GITHUB_OUTPUT` /
`GITHUB_STEP_SUMMARY` 指向临时文件),四条路径(跳过 / 约定产物 / dispatch 产物 / dispatch 缺失即红)
与 `evidence` 的 0/1/2 都按预期。**这不是 CI 实测**:`if:` 求值、job skip 在 UI 上的呈现、
`needs` 的输出传递在真 Actions 里仍未跑过。
- **没在真 GPU runner 上跑过**:`evidence` 的真判定只在本地用合成向量走过(0 与 1 都到达过),
「真产物 + 真 GPU runner」这个组合没实测。
- **没验「跳过是否满足 required status check」**:本 workflow 没挂成必需检查;GitHub 对 skipped job
与 required check 的交互口径没实测,别把这一节读成「跳过也能当门过」。
- **没接日常线 `dev`**:`dev-guards.yml` 复用的是 `guards.yml`,本文件不在其中;挂上去要多一次
`uses:`,那是新的成本决定,不在 M253 里顺手做。