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
This commit is contained in:
1 parent
87ab8783a3
commit
e3ee20bdcd
6 files changed
+503
-3
No files matched your search
@@ -11,6 +11,7 @@ Release matrix (see `packaging/README.md` for the full contract):
|
||||
macOS x64 unlucky-launcher_<ver>_macos_x64.tar.gz
|
||||
every slot ull_<ver>_<platform>_<arch>[.exe] (bare CLI binary, no archive)
|
||||
all SHA256SUMS.txt
|
||||
all manifest.json (only with --manifest, see below)
|
||||
|
||||
Naming rule (user's call, 2026-11-05): `<name>_<version>_<platform>_<arch>`, underscore
|
||||
separated, and **no implementation detail in the file name** — the asset never says which UI
|
||||
@@ -31,6 +32,14 @@ carries LICENSE / NOTICE / README.md anyway, so a bare `ull` loses nothing.
|
||||
The launcher binary also answers every CLI subcommand, so `ull` is not a second
|
||||
interface — it is the version of it that links no graphics stack at all.
|
||||
|
||||
`--manifest` additionally writes `manifest.json` (the update manifest, D50) into the same
|
||||
output directory. The generation itself lives in `packaging/make_manifest.py`, invoked as a
|
||||
subprocess: this file stays the single source of truth for **naming** (`asset_stem()`), that
|
||||
one is the single writer of the **manifest**, and it derives every sha256/size from the
|
||||
`SHA256SUMS.txt` this script just wrote — one set of facts, no second bookkeeping. CI does not
|
||||
use this flag: `release-publish.yml` recomputes `SHA256SUMS.txt` after pruning the publish
|
||||
whitelist and generates the manifest from *that*, so the manifest always describes what ships.
|
||||
|
||||
Slot and build directory are overridable because a cross build's binaries live in
|
||||
`target/<triple>/release` and the build host (x64) is not the artifact (arm64): CI
|
||||
passes both from its matrix, and the executable header is read back so a binary can
|
||||
@@ -209,6 +218,15 @@ def write_sums(out_dir: Path, assets: list[Path]) -> Path:
|
||||
return sums
|
||||
|
||||
|
||||
def write_manifest(out_dir: Path, version: str) -> Path:
|
||||
"""`manifest.json`(D50):跑的是 CI 发布侧同一份脚本,事实全部来自刚写完的 SHA256SUMS.txt。"""
|
||||
script = REPO_ROOT / "packaging" / "make_manifest.py"
|
||||
proc = subprocess.run([sys.executable, str(script), "--dist", str(out_dir), "--version", version])
|
||||
if proc.returncode != 0:
|
||||
fail(f"manifest generation failed ({script.name} exited {proc.returncode})")
|
||||
return out_dir / "manifest.json"
|
||||
|
||||
|
||||
def asset_stem(name: str, version: str, slot: str) -> str:
|
||||
"""`<name>_<version>_<platform>_<arch>` — 产物命名的唯一真相源。
|
||||
|
||||
@@ -267,6 +285,7 @@ def main() -> int:
|
||||
parser.add_argument("--slot", choices=list(SLOTS), help="matrix slot; defaults to the one derived from the host")
|
||||
parser.add_argument("--build-dir", default=DEFAULT_BUILD_DIR, help=f"directory holding the built binaries (default: {DEFAULT_BUILD_DIR})")
|
||||
parser.add_argument("--no-smoke", action="store_true", help="skip the `ull --version` smoke run")
|
||||
parser.add_argument("--manifest", action="store_true", help="also write manifest.json (update manifest, D50)")
|
||||
args = parser.parse_args()
|
||||
|
||||
version = workspace_version()
|
||||
@@ -310,6 +329,9 @@ def main() -> int:
|
||||
size_mb = item.stat().st_size / (1024 * 1024)
|
||||
print(f"artifact: {item.name} {size_mb:.2f} MiB sha256={sha256(item)}")
|
||||
print(f"sums: {sums.name}")
|
||||
if args.manifest:
|
||||
manifest = write_manifest(out_dir, version)
|
||||
print(f"manifest: {manifest.name}")
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user