Files
Unlucky-Launcher/packaging/README.md
T
worker-updatee2e 28bf104041 fix(m169): 复审 r1 五条 p2 —— 守门开一个极窄 allowlist 并把 E2E 真接进 CI
r1 复审(reviewer-updatee2e-r1,p2-5items,均不挡合并)逐条落地:

P2-3(本轮主产出)守门把「E2E 进 CI」这条路堵死了:`scan_enabling_text` 对 workflow 里任何该
feature 字面量判红,于是 `cargo test --release … --features update-test-source --test
update_e2e_test` 这种**合法调用**也红,自己登记的 follow-up 无法实现。现在:
- `ALLOWED_STEP = {workflow: .github/workflows/ci.yml, name: update-e2e,
  must_contain: --test update_e2e_test}`;按 `- name:` 把 workflow 切成步骤块再判,文件名 / 步骤名
  / 命令行**三者任一不符即红**(job 名不参与判据:改 job 名不该解锁新能力)。
- `ci.yml` **新增一个独立 job** `update-e2e`(windows-latest、timeout 45m、release 档;现有 job
  一行未动,纯追加 27 行)。只在 Windows:夹具的「真形状」是裸 `.exe` + `portable-exe`,Linux/macOS
  的真实产物是 `.tar.gz` + `portable-zip`(会被 `ensure_self_replaceable` 直接拒),在 ubuntu 上跑绿
  是假绿。不降 debug:`debug_assertions`/优化/panic 行为都不同。
- self-test 覆盖 allowlist 六向量(允许 / 换文件 / 换步骤名 / 缺 `--test` / 发布工作流 / ci 内另一步)。

P2-2 守门 8 条绕过:注释剥离改**引号感知**(`echo "#"; cargo …` 不再吃掉真命令);扫描面扩到
`.github/actions/**`、`packaging/**` 递归子目录、`Makefile|justfile|Taskfile` 第二构建入口;
`--all-features` 之外补 `cargo hack --feature-powerset|--each-feature|--all`。大小写/下划线变体按
复审结论写进「抓不住」表(cargo feature 名敏感,写错编不过,不算漏检)。

P2-1 GUI 人工验收命令根本跑不起来:`-p unlucky-launcher-iced` 是 bin 名,改成 package 名
`-p unlucky-app-iced`(app-iced / presenter 两处,并写明 `-p` 只认 package)。

P2-5 夹具缺「不给 --dest 就 panic」的结构性保险:`Fixture::run` 加两条断言——`apply` 必带
`--dest`、`--dest` 不许等于本进程 exe 的 canonical 路径。抓得住「以后有人手写 fx.run(&["apply"])」,
抓不住「绕开 Fixture 直接调 unlucky_cli::run」。

P2-4 夹具「真形状」只在 Windows 成立:写进 `tests/support/update_e2e.rs` 文件头(说明 Linux 上跑绿
是假绿、CI 因此只在 Windows),**不改断言**。另按复审 ⑤ 补一行:失败路径三例抓不住回滚/备份
(摘要失败发生在 install_staged 之前),回滚靠正路备份断言 + install_cases。

跨卷替换 bug 按 tower 指令**未动**(另开 mission)。

验证:守门正式扫描 + self-test 绿(14 红 / 3 绿);真仓注入 6 组(基线绿、步骤改名红、缺 --test 红、
搬进 release-build 红、改 --all-features 红、还原绿);E2E debug 6 绿、**release 6 绿**;
`cargo clippy -p unlucky-cli --all-targets --features update-test-source -- -D warnings` 干净;
`cargo fmt --all -- --check` 干净;全仓 400 行扫描无超限(守门脚本 379 行、ci.yml 317 行)。

⚠ 未验:CI 里 E2E 真的跑起来这件事本机验不了——只证明 workflow 写对 + 守门放行,首次真实运行要等
推送后看 Actions。
2026-11-08 11:12:11 +08:00

11 KiB
Raw Blame History

打包与发布工程(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(本节再增长也往那里写,两个文件都必须 ≤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;--self-test 注入 5 条回归 ≤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)。
  • 验收本体: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 点击与重启替换(需人工)。