Files
Unlucky-Launcher/.github/workflows/release-publish.yml
T
AskaEth e3ee20bdcd feat(packaging): M151 发布流水线生成更新清单 manifest.json(D50)
清单挂 GitHub Release 资产,固定文件名 manifest.json(D50)。字段形状的权威是 M146 的
crates/core/src/update/manifest.rs(ReleaseManifest,deny_unknown_fields + 逐字段校验),
生成器只负责把那套形状填对,不另立 schema。

同源,不双写:packaging/make_manifest.py 的每个 sha256 直接读 Release 自己的
SHA256SUMS.txt,size 取自同一批文件的 stat();产物名里的版本段必须等于 --version,
tag 与 Cargo.toml 漂了就没有产物可对,当场失败。CI 在「裁剪发布白名单后重算
SHA256SUMS」之后调它,所以清单描述的是真正要发的资产,不是构建出来的全部。
make_artifacts.py 只加 --manifest 开关,用子进程调同一份脚本(本地跑也产出
manifest.json),本文件继续是产物命名的唯一真相源。

只放启动器产物:DTO 的 Artifact 没有角色字段,Release::artifact_for() 取第一个
target 命中的产物,把 ull 并列写进去会让「覆盖哪个文件」取决于数组顺序;ull 照旧进
SHA256SUMS.txt 与 Release。

url 两种形态:默认裸文件名(DirSource 口径,本地/内网用);CI 传 --asset-base-url
拼绝对地址并钉 tag(不是 latest),清单里每条都指向与它自报版本相符的字节。

自验:--self-test 用 packaging/tests/manifest.golden.json 逐字节对拍(覆盖裸 exe 与
tar.gz 两种形态,并钉住「ull 不进清单」),再跑 14 条负例证明严格解析不是摆设;
发布步会先跑它再产文件。真 DTO 对拍已在本机用 M146 的 ReleaseManifest 实测:
golden 解析通过(2 个产物、min_supported、force 均正确落位),14 条篡改全部被拒。

未决(报告带利弊,未自行改发布默认值):releases/latest/download/* 只指向非 prerelease
的最新 Release,而发布默认 prerelease=true。

Refs: D50, M146
2026-11-06 22:15:58 +08:00

178 lines
10 KiB
YAML
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.
name: Release / publish(可复用)
# Release 流水线的**发布侧**(M145 拆分:`release.yml` 顶到 400 行红线)。本文件 = 拆分前 `release.yml` 的 `release`
# job **逐字搬家**:裁剪白名单外的槽位产物、重算全量 `SHA256SUMS.txt`、渲染 `packaging/release-notes.md` 模板、
# `gh release create/edit` 的同 tag 幂等,全部照旧。留在调用方 `release.yml` 的只有三件事:`needs: build`、
# tag 门禁 `if: always() && !cancelled() && github.ref_type == 'tag'`、以及 `permissions: contents: write` 的授予。
# 与拆分前的三处等价改写(都只涉及"从哪里读",不涉及行为):
# ① `secrets.GITHUB_TOKEN` → 本文件声明的入参 secret `PUBLISH_TOKEN`(调用方透传 `secrets.GITHUB_TOKEN`)。
# 可复用工作流**拿不到**调用方未显式传入的 secret,写 `secrets.GITHUB_TOKEN` 会静默为空 → gh 401。
# ② `inputs.prerelease`(原为 `workflow_dispatch` 的输入)→ `github.event.inputs.prerelease`(**字符串**形态的同一个
# 值:`push` 触发时不存在 ⇒ 空串,dispatch 时是 `"true"`/`"false"`),下面 `case "${PRERELEASE:-}"` 的三条分支与
# 拆分前完全一致(空值 = 不碰该标记)。为什么不用 `workflow_call` 入参转一道:dispatch 的 `inputs.prerelease` 是
# **boolean**,而「空值 vs true/false」这条三态语义只有字符串能表达 —— 布尔入参无法取空,硬塞进字符串入参则是
# 类型不符(`uses` 的 `with:` 会按被调方声明的类型校验;actionlint 当场报 `bool value cannot be assigned`)。
# 取 event 上的字符串形态既保住三态,又不需要任何类型转换。
# ③ `PUBLISH_SLOTS` / `runs-on` / `permissions` 的**内容**未动,只是被调方必须自己声明(被调方不继承调用方的
# workflow 级 `env`;权限取调用方与本文件的交集 —— 两边都声明 `contents: write` 才拿得到写权)。
on:
workflow_call:
secrets:
PUBLISH_TOKEN:
description: "调用方透传 `secrets.GITHUB_TOKEN`(本 job 已声明 contents: write,`gh release` 用它发版)"
required: true
# 权限取交集:调用方 release.yml 的 release job 与本文件都声明 contents: write,交集才是 write(不声明则被收窄成 read)。
permissions:
contents: write
jobs:
release:
name: GitHub Release
runs-on: ubuntu-latest
permissions:
contents: write
env:
# ── 发布白名单:唯一决定「哪几个槽位进 Release」的开关(空格分隔,键同 matrix.slot)──
# **M142 起只有 `windows-x64`**:arm64 连试四轮未走通,而下面齐全性检查是「名单内少一档就 fail」——名字挂着
# arm64 就等于 x64 编成功也发不出去。故把它移出名单:它照留在矩阵里跑取证(诊断注解是将来修它的唯一证据),
# **红了不挡发布**。**可逆**:arm64 修好后把键加回这一行即可(齐全性检查与 SHA256SUMS 自动跟着扩),再同步
# README §1/§9 与 release-notes.md 两张表。四档 Linux/macOS 仍未验证,默认连构建都不跑(M139)。
PUBLISH_SLOTS: "windows-x64"
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
path: dist
merge-multiple: true
- name: 裁掉白名单外的槽位产物(构建取证 ≠ 发布)
shell: bash
run: |
set -euo pipefail
# 槽位键 `windows-x64` 落到文件名是 `windows_x64`(asset_stem),故取文件名里的「平台_架构」两段比白名单。
keep=" "
for slot in $PUBLISH_SLOTS; do keep+="${slot//-/_} "; done
for f in dist/*; do
base=${f##*/}
[ "$base" = "SHA256SUMS.txt" ] && continue
platform_arch=${base%.exe}; platform_arch=${platform_arch%.tar.gz}; platform_arch=${platform_arch#*_*_}
case "$keep" in
*" $platform_arch "*)
echo "publish: $base"
;;
*)
echo "drop: $base($platform_arch 不在发布白名单:四档未验证 / arm64 待修;仍留在 dist-<slot> artifact 里取证)"
rm -f "$f"
;;
esac
done
- name: 发布白名单齐全性 + 重算全量 SHA256SUMS
shell: bash
run: |
set -euo pipefail
ls -l dist
# 白名单内槽位必须**全齐**才发版(宁可不出 Release,也不出少一档的 Release)。每个槽位都要 GUI 与 CLI 两个
# 资产——只按「槽位名出现过」判会被其中一个掩盖;比较用下划线形态(资产名自 D49 起是 `..._windows_x64.exe`)。
# M142 后白名单只剩 x64 一档:arm64 缺产物不再拦发布,但 x64 自己缺 GUI 或 CLI 照样在这里 fail。
for slot in $PUBLISH_SLOTS; do
stem=${slot//-/_}
for prefix in unlucky-launcher ull; do
if ! compgen -G "dist/${prefix}_*_${stem}*" >/dev/null; then
echo "::error::发布白名单缺产物:$prefix / $slot"
exit 1
fi
done
done
# 各槽位各写一份 SHA256SUMS.txt,合并后只有一份是真的 —— 在 Release 侧按已发布的资产重算。
cd dist
rm -f SHA256SUMS.txt
sha256sum unlucky-launcher_* ull_* > SHA256SUMS.txt
sha256sum -c SHA256SUMS.txt
cat SHA256SUMS.txt
- name: 生成更新清单 manifest.json(D50:固定名挂 Release 资产)
shell: bash
env:
TAG: ${{ github.ref_name }}
RELEASE_BASE: ${{ github.server_url }}/${{ github.repository }}/releases/download
run: |
set -euo pipefail
# 先自验再产文件:golden 对拍 + 「DTO 严格解析」镜像(未知字段 / 摘要长度 / size=0 / 版本重复
# 这些在发布步当场炸,而不是变成用户机器上的「查不到更新」)。零第三方依赖,毫秒级。
python3 packaging/make_manifest.py --self-test
# 事实来源是上一步**重算**出来的 dist/SHA256SUMS.txt:清单不另算一份摘要,两者不可能不一致。
# url 钉死 tag(不是 latest):清单里每条都指向与它自报版本相符的字节。
# min_supported / notes 刻意先不写:前者是「收回老版本支持」的发布策略(未拍板,写错会强制升级),
# 后者在 Release 正文里已有,两处各写一份迟早漂。
python3 packaging/make_manifest.py \
--dist dist \
--version "$TAG" \
--channel stable \
--asset-base-url "$RELEASE_BASE/$TAG" \
--released-at "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
- name: 生成 Release 说明(支持矩阵模板)
shell: bash
env:
TAG: ${{ github.ref_name }}
run: |
set -euo pipefail
python3 - <<'PY'
import os, pathlib
version = os.environ["TAG"].lstrip("v")
template = pathlib.Path("packaging/release-notes.md").read_text(encoding="utf-8")
sums = pathlib.Path("dist/SHA256SUMS.txt").read_text(encoding="utf-8").strip()
body = template.replace("{{VERSION}}", version).replace("{{SUMS}}", sums)
pathlib.Path("release-body.md").write_text(body, encoding="utf-8", newline="\n")
print(body[:400])
PY
- name: 创建或更新 GitHub Release(同 tag 重跑幂等)
env:
GH_TOKEN: ${{ secrets.PUBLISH_TOKEN }}
TAG: ${{ github.ref_name }}
PRERELEASE: ${{ github.event.inputs.prerelease }}
shell: bash
run: |
set -euo pipefail
base_flags=(--title "Unlucky Launcher $TAG" --notes-file release-body.md)
# pre-release 只在**显式要求**时断言:PRERELEASE=true/false 来自 tag ref 上的 dispatch 输入;空值 = push
# 触发(或它的 Re-run),此时**不碰**该标记(否则会把用户手动提升为 stable 的 Release 静默翻回 pre-release)。
# 首次 create 仍守「pre-release 默认开」老契约:空值按 true 处理。
create_mark=()
edit_mark=()
case "${PRERELEASE:-}" in
true)
create_mark=(--prerelease)
edit_mark=(--prerelease)
;;
false)
edit_mark=(--prerelease=false)
;;
"")
create_mark=(--prerelease)
;;
*)
echo "::error::非预期的 github.event.inputs.prerelease 取值:$PRERELEASE"
exit 1
;;
esac
# Windows 单 exe 装不下 LICENSE/NOTICE/README.md,故三份与产物并列挂 Release(条款随包);`ull` 是独立资产。
# manifest.json 是更新清单(D50):固定文件名,客户端问 `releases/latest/download/manifest.json`。
# ⚠️ 未决(M151 报告带利弊,未自行改默认值):GitHub 的 `releases/latest/download/*` 只指向**非
# prerelease** 的最新 Release,而下面首次 create 默认勾 `--prerelease`。矛盾的两个方案——
# 甲:稳定发布时显式 dispatch `prerelease=false`(latest 随之生效,客户端地址不变);
# 乙:客户端不用 latest,改按 tag 查 API 再取 assets(发布默认值不用动,客户端多一次 API 调用)。
assets=(dist/unlucky-launcher_* dist/ull_* dist/manifest.json dist/SHA256SUMS.txt LICENSE NOTICE README.md)
if gh release view "$TAG" >/dev/null 2>&1; then
# 同 tag 重跑(补丁/重编):Release 已存在 → 覆盖资产 + 刷新说明,再 `create` 会因已存在而失败。
gh release upload "$TAG" "${assets[@]}" --clobber
gh release edit "$TAG" "${base_flags[@]}" "${edit_mark[@]}"
else
gh release create "$TAG" "${assets[@]}" "${base_flags[@]}" "${create_mark[@]}"
fi