清单挂 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
178 lines
10 KiB
YAML
178 lines
10 KiB
YAML
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
|