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)。
140 lines
12 KiB
Markdown
140 lines
12 KiB
Markdown
# 打包与发布工程(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 点击与重启替换(需人工)。
|