Files
Unlucky-Launcher/packaging/README.md
T
worker-updatee2e 68a1f90828 fix(m169): 复审 r2 —— 守门归属改成「列表项结构判定 + 默认拒绝」,堵掉无名步骤/无名 job/flow 步骤的继承放行
r2 复审(reviewer-updatee2e-r2,p2-4items;被抓出一个真洞,mission 翻回 active)逐条落地。

P2-1(本轮核心,真洞)allowlist 的归属原来按「最近一行 `- name:`」切块,于是任何不写成
`- name: <x>` 的步骤都继承上一块的名字;被放行的 `update-e2e` 恰好是 ci.yml 里最后一个
`- name:`,所以追加到文件末尾的一切都落在它的块里。复审三条真实注入(无名 `- run:`、无名步骤的
整个 job、flow 风格步骤)当时全绿。现在:
- `split_items()` 按 YAML **列表项**切块(每个 `- ` 起一项;比当前项深的行归当前项;同级或更浅的
  非列表行**结束**当前项)。项名 = 该项内**同级**的 `name:` 键(键序无关;`run: |` 块里的
  `name: dummy` 不算;`run: echo "name: x"` 也不算)。
- **默认拒绝**:落不到列表项上的行(`jobs:` 之前的孤儿行、job 级 `run:` 这类项外键)带 feature 即红;
  没有 `name:` 的项带 feature 即红。不再有「继承上一块」这回事。
- 三条复审注入 + 本轮新增三条(`jobs:` 之前的孤儿行、`run: |` 里夹带并伪造 name、shell 变量拼接)
  都进了自测向量;在**真仓库文件**上重跑也是全红。

P2-2(同族误杀):
- 键序无关:允许的那一步把 `run:` 写在 `name:` 之前仍绿(旧实现会报到上一块的名字上)。
- `cargo hack check --all-targets` 不再被误判成 `--all`(正则收紧到 `--all(?![-\w])`)。
- Cargo 的 `update-test-source = [ … ]` **跨行声明**不再被误判成「写进了别的 feature 列表」
  (`scan_manifest` 增加 `in_declaration` 状态)。

P2-3 `Fixture::run` 的第二条断言比错对象:原来比夹具字段 `self.dest`,手写
`fx.run(&["apply","--dest","<测试二进制>","--json"])` 两条都过。现在从 `args` 里解析真实 `--dest`
值(含 `--dest=` 形式)再 canonicalize 比较;夹具字段那份作为额外一道保留。

P2-4 `release-guard.yml` 的步骤级注释还停在 r1 的扫描面:改成「含复合 action / packaging 递归 /
Makefile 等 + 归属按列表项判 + 唯一允许 ci.yml 的 update-e2e 一步」。

自测数据搬到 `packaging/tests/guard_vectors.json`(基线 8 个文件 + 26 条向量:21 红 / 6 绿),
脚本本身从 379 行降到 373 行(守门仍 ≤400);README §14 补上归属规则与「抓不住」清单。

验证(本机,全部 exit 0):
- 守门正式扫描绿;`--self-test` 21 条注入全红 + 6 条基线/放行绿;向量文件缺失时 exit 1(不静默)。
- **真仓注入**(在真 ci.yml / release-build.yml / cli/Cargo.toml 上改完即还原):复审三条(无名步骤 /
  无名 job / flow 步骤)全红;本轮新增三条(孤儿行 / `run: |` 夹带 / shell 拼接)全红;
  三条合法写法(键序、`--all-targets`、跨行声明)全绿;还原后基线绿。
- E2E:debug 6 绿、release 6 绿(`running 3 tests` ×2,0.26s / 0.23s,非 0 measured)。
- `cargo clippy -p unlucky-cli --all-targets --features update-test-source -- -D warnings` 干净;
  `cargo fmt --all -- --check` 干净;全仓 400 行扫描无超限(守门 373 / 向量 194 / ci.yml 317)。

约束遵守:未 push;`PUBLISH_SLOTS` 只列 `windows-x64`(未动);release 侧仍无该 feature;跨卷 bug 未碰。
⚠ 仍未验:CI 里 E2E 真跑起来本机验不了(只证明 workflow 写对 + 守门放行,首跑要等推送后看 Actions)。
2026-11-08 11:12:11 +08:00

140 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 打包与发布工程(packaging/)
单一真相源:**单文件产物矩阵 + 发布契约**。`.github/workflows/release.yml` 按本文实现,
`packaging/make_artifacts.py` 是本地与 CI **共用**的打包脚本(同一份代码 → 同一套目录布局)。
本目录只放发布工程资产(脚本 / 说明 / 发布说明模板),不参与任何 crate 的编译。
## 索引:§1–§12 各在哪份文件(M145 拆分)
拆分原因:本文件与 `.github/workflows/release.yml` 都已是 400 行整,下一处改动必撞全仓行数红线(`guards.yml` 的
`file-size`,单文件 ≤400 行)。按主题分开后每份都留出余量,**内容一字未改、只搬家**;**节号沿用原编号**,
所以全仓那些「`packaging/README.md` §4 / §7.4 / §9」式引用不必改动 —— 按下表找承载文件即可。
| 节 | 主题 | 承载文件 |
|----|------|---------|
| §1 | 产物矩阵 | `packaging/docs/artifacts-and-version.md` |
| §2 | 版本来源(唯一真相源) | `packaging/docs/artifacts-and-version.md` |
| §3 | 怎么构建(本地 / CI) | `packaging/docs/build-and-ci.md` |
| §4 | 每平台运行时依赖 | `packaging/docs/runtime-deps.md` |
| §5 | 软渲染兜底 | `packaging/docs/runtime-deps.md` |
| §6 | Cargo 构建档位 | `packaging/docs/profiles-and-measurements.md` |
| §7 | 实测(本机,不是估算) | `packaging/docs/profiles-and-measurements.md` |
| §8 | 发行合规 | `packaging/docs/compliance-support-boundaries.md` |
| §9 | 支持矩阵(对外口径) | `packaging/docs/compliance-support-boundaries.md` |
| §10 | 已知边界 / 后续 | `packaging/docs/compliance-support-boundaries.md` |
| §11 | CI 运行时冒烟(Linux 真渲染一帧) | 本文下方(细节在 `packaging/ci-runtime-smoke.md`) |
| §12 | 文件清单 | 本文下方 |
本文件仍是**契约的唯一真相源入口**:`.github/workflows/release.yml`(及拆出的 `release-build.yml` /
`release-publish.yml`)的注释、`make_artifacts.py` 的说明、根 `README.md` / `README.zh-CN.md` 与
`packaging/release-notes.md` 都按「`packaging/README.md` §N」引用。改契约时先改上表对应的承载文件,
再看一眼 §12 的行数预算 —— 任何一份超过 400 行都要继续按主题拆。
## 11. CI 运行时冒烟(Linux 真渲染一帧)
**细节与依据在 [`packaging/ci-runtime-smoke.md`](ci-runtime-smoke.md)**(本节再增长也往那里写,两个文件都必须 ≤400 行)。
`ci.yml` 的 `runtime-smoke` job 在 `ubuntu-latest` 上补第一份**真实图形环境**证据:装 `xvfb` + 软渲染依赖 +
字体,用 `xvfb-run` 起 GUI 进程渲出一帧 PNG,再对**像素本身**断言(退出码 0 / 尺寸 / 非空白,artifact
`runtime-smoke-screenshot`)。
要点三条:
- 只看退出码不够:「合法 PNG + 退出码 0」不等于画面有内容;「完全没字体」与「非软渲染」都是**硬失败**(panic /
报错,不产生 PNG;review 实测)。
- 闸门下限 ≈「至少画出一条标题栏」(review 实测切片仍 pass):证明「场景被真实渲染过」,**不证明界面元素齐全,
更不证明视觉正确**——视觉靠人眼看 artifact。
- 只覆盖 Linux x64;macOS 是 dispatch-only 探针(无实测证据),Windows 已有本机人眼验收。
**本地复跑**:`bash packaging/linux-smoke-docker.sh . D:/tmp/ull-smoke`——脚本逐步复刻这个 job(同样的 apt 依赖、
`--bin unlucky-launcher-iced`、同样的 `xvfb-run … --software-renderer --screenshot --data-dir`);输出目录别写 `/tmp`,
Git Bash 下会静默落进 Docker VM(脚本内部用 `cygpath -m` 归一)。
**M131 真跑过一次**(本机 Docker / Debian 12 容器 / 2026-11-05):`exit=0`,截图 1180×760、`[ok] 截图非空白`。
## 12. 文件清单
| 文件 | 作用 | 行数上限 |
|------|------|---------|
| `packaging/README.md` | 本文:索引 + §11 / §12 / §13(契约入口;§1–§10 见 `docs/`) | ≤400 |
| `packaging/docs/artifacts-and-version.md` | §1 产物矩阵 + §2 版本来源 | ≤400 |
| `packaging/docs/build-and-ci.md` | §3 怎么构建(本地 / CI) | ≤400 |
| `packaging/docs/runtime-deps.md` | §4 每平台运行时依赖 + §5 软渲染兜底 | ≤400 |
| `packaging/docs/profiles-and-measurements.md` | §6 Cargo 构建档位 + §7 实测 | ≤400 |
| `packaging/docs/compliance-support-boundaries.md` | §8 发行合规 + §9 支持矩阵 + §10 已知边界 | ≤400 |
| `packaging/make_artifacts.py` | 打包与校验(本地 + CI 共用) | ≤400 |
| `packaging/release-notes.md` | GitHub Release 说明模板(`{{VERSION}}` / `{{SUMS}}` 占位) | ≤400 |
| `packaging/check_screenshot.py` | 截图非空白判定(CI 门禁 + 本地手动跑,零第三方依赖) | ≤400 |
| `packaging/check_publish_slots.py` | 发布名单守卫(M164):名单 ⇄ 产物两个方向;`--self-test` 注入 8 条回归 | ≤400 |
| `packaging/check_test_source_off.py` | 测试通道守门(M169):workflow / 打包脚本 / `Cargo.toml default` 都不许打开 `update-test-source`;归属按 `jobs.*.steps[]` 列表项判、默认拒绝;`--self-test` 按 `tests/guard_vectors.json` 注入 | ≤400 |
| `packaging/tests/guard_vectors.json` | 守门自测向量(基线 + 26 条:21 红 / 6 绿),数据与代码分开 | ≤400 |
| `packaging/ci-runtime-smoke.md` | §11 的细节:断言口径/阈值依据/失效模式/复跑 | ≤400 |
| `packaging/linux-smoke-docker.sh` | 本地复跑 `runtime-smoke`(Docker + xvfb) | ≤400 |
| `packaging/linux-check-docker.sh` | 本地复跑 `ci.yml` 的两条 ubuntu 门禁(clippy / test,Docker) | ≤400 |
| `.github/workflows/release.yml` | tag 触发 → 选槽位(`plan`)→ 调下面两个可复用工作流 | ≤400 |
| `.github/workflows/release-build.yml` | `workflow_call`:单槽位构建(体检 / cargo / 打包 / 上传 artifact) | ≤400 |
| `packaging/make_manifest.py` | 更新清单 `manifest.json` 的生成与自验(D50,见 §13) | ≤400 |
| `packaging/tests/manifest.golden.json` | 清单 golden 样例:`make_manifest.py --self-test` 逐字节对拍 | ≤400 |
| `.github/workflows/release-publish.yml` | `workflow_call`:发布(`PUBLISH_SLOTS` 裁剪 / SHA256SUMS / 清单 / Release) | ≤400 |
## 13. 更新清单 `manifest.json`(D50)
清单作为**固定文件名**的 Release 资产挂着;客户端按 `releases/latest/download/manifest.json` 取(D50)。
字段形状的唯一权威是 `crates/core/src/update/manifest.rs` 的 `ReleaseManifest`(`deny_unknown_fields` +
逐字段校验:semver、`size > 0`、摘要长度/字符集、`target`/`format` 取值表)——生成器只负责把那套形状填对。
- **生成器**:`packaging/make_manifest.py`(零第三方依赖)。CI 在 `release-publish.yml` 里**重算
`SHA256SUMS.txt` 之后**调它,所以清单描述的是「真正要发的资产」,不是「构建出来的全部」。
- **同源,不双写**:每个 `sha256` 直接读 `SHA256SUMS.txt`,`size` 取自同一批文件的 `stat()`;产物名里的版本段
必须等于 `--version`(tag 与 `Cargo.toml` 漂了就没有产物可对,当场失败)。`--self-test` 用
`packaging/tests/manifest.golden.json` 逐字节对拍,并跑 14 条负例证明严格解析不是摆设。
- **只放启动器产物**:`ull` 不进清单。DTO 的 `Artifact` 没有角色字段,`Release::artifact_for()` 取「第一个
target 命中的产物」,把 CLI 并列写进去会让「该覆盖哪个文件」取决于数组顺序(`ull` 照旧进 `SHA256SUMS.txt`)。
- **`url` 两种形态**:默认裸文件名(相对源根,`DirSource` 口径,本地/内网用);CI 传 `--asset-base-url`
拼绝对地址,且**钉 tag 不钉 latest**——清单里每条都指向与它自报版本相符的字节。
- `min_supported` / `notes` 只是**可填而不默认填**:前者是「收回老版本支持」的发布策略(未拍板,写错会强制升级),
后者在 Release 正文里已有,两处各写一份迟早漂。
- 本地复跑:`python3 packaging/make_artifacts.py --manifest` → 与 CI 同一段代码,产出 `dist/manifest.json`。
**D51 定案(2026-11-06,方案甲)+ M163 语义修正(2026-11-07)**:GitHub 的 `releases/latest/download/*`
只指向**非 prerelease** 的最新 Release。而自更新清单固定取 `releases/latest/download/manifest.json`,所以
「新版本必须被标成稳定版,latest 才会指过去」——M163 之前 push tag 默认打 `--prerelease`,v0.2.2 因此至今
`releases/latest` 404、更新通道取不到清单。现在的语义(唯一真相源 `packaging/release_marker.py`):
| 触发 | create(新建 Release) | edit(Release 已存在) |
| --- | --- | --- |
| `push` tag `v*`(含其 Re-run) | **稳定版**(不带标记) | 不碰标记 |
| `workflow_dispatch` + `prerelease=true` | 预发布 | 显式改成预发布 |
| `workflow_dispatch` + `prerelease=false`(默认) | 稳定版 | 显式改成稳定版 |
`v0.2.2` 那个 Release **保留为历史预发布不动**,稳定通道以 `v0.2.3` 为起点。发版 checklist 第六件 =
确认 prerelease 标记与通道一致(`gh release view <tag> --json isPrerelease`),守门是
`packaging/check_release_semantics.py`(CI:`.github/workflows/release-guard.yml`)。
## 14. 更新链路的测试通道(`update-test-source`,M169)
「发现新版本 → 下载 → 长度 + SHA256 双检 → 就位」这条链要能在**发布档(release)**上验收,就得让
二进制能指向本地更新源;而这条能力同时是「离任意本地目录最近的一条路」(终点是替换用户盘上的 exe),
所以它必须是**编译期**的,不是运行期设置项:
- **默认关**:`update-test-source` 是 `crates/core` / `cli` / `presenter` / `app-iced` 各一条 feature,
默认不启用。官方发布构建不带它 ⇒ `core::update::job::default_source` 里那段分支**根本编不进来**
(README 的「release builds do not contain that path at all」就是这么成立的)。
- **验收构建**:`cargo build --release --features update-test-source`,再配
`ULL_UPDATE_DIR=<root>` + `ULL_UPDATE_TEST_SOURCE=1`(两档缺一不可)。`ull update check --json` 会
如实写 `data.source: "file:<root>"`、`data.test_source: true`,人读输出另有一行点名。
- **守门**:`packaging/check_test_source_off.py`(CI:`.github/workflows/release-guard.yml`,两步:
`--self-test` + 正式扫描)扫 `.github/workflows/**`(含复合 action)、`packaging/**` 的递归脚本、
`Makefile`/`justfile`/`Taskfile` 与各 `Cargo.toml` 的 `default`。**唯一允许**出现该 feature 的地方是
`.github/workflows/ci.yml` 里名为 `update-e2e` 的那一步,且该步必须带 `--test update_e2e_test`
(文件名 / 步骤名 / 参数任一不符即红,见脚本的 `ALLOWED_STEP`)。
归属按 **`jobs.*.steps[]` 的列表项**判(不是「最近一行 `- name:`」):落不到具名步骤上的行——`jobs:`
之前的孤儿行、项外键、没有 `name:` 的步骤、flow 风格步骤——**一律判红**(默认拒绝)。自测向量在
`packaging/tests/guard_vectors.json`(26 条:21 条注入必须红 + 6 条基线/合法写法必须绿)。
抓不住(如实记):变量间接拼接(`FEAT="update-""test-source"`)、feature 改名后接回发布、`build.rs`
里 spawn cargo、以及「把别的步骤**改名**成 `update-e2e` 并带 `--test update_e2e_test` 却干别的事」。
- 验收本体:`cargo test --release -p unlucky-cli --features update-test-source --test update_e2e_test
--test update_e2e_tamper_test`(正路 + 幂等 / 篡改摘要 + 篡改包体 + 截断)。CI 里同一个命令挂在
`ci.yml` 的 `update-e2e` job(**只在 Windows**:夹具的「真形状」是 Windows 裸 `.exe` +
`portable-exe`;Linux/macOS 的真实产物是 `.tar.gz` + `portable-zip`,在本模块会被直接拒)。
**不验**真 GitHub 下载、GUI 点击与重启替换(需人工)。