From 4927da5e1c00a8e6ab00bccd0492c0406d0439cc Mon Sep 17 00:00:00 2001 From: chengma Date: Mon, 13 Jul 2026 17:36:46 +0800 Subject: [PATCH] build: version release metadata filename --- docs/04-architecture.md | 2 +- docs/packaging.md | 8 ++++---- docs/tasks/T-625.md | 11 +++++++---- docs/update-check.md | 2 +- scripts/build_exe.ps1 | 7 ++++--- tests/test_packaging.py | 37 ++++++++++++++++++++++++++++++++++++- 6 files changed, 53 insertions(+), 14 deletions(-) diff --git a/docs/04-architecture.md b/docs/04-architecture.md index f47c5de..6e7b7cd 100644 --- a/docs/04-architecture.md +++ b/docs/04-architecture.md @@ -527,7 +527,7 @@ cmshopee/ ### 10.1 发布与自动升级边界 -- `app/version.py` 是应用版本唯一来源;`app/release_manifest.py` 生成 `package-manifest.json` 和 `release-metadata.json`,构建脚本不得自行维护另一套 hash 或路径规则。 +- `app/version.py` 是应用版本唯一来源;`app/release_manifest.py` 生成 `package-manifest.json` 和 `release-metadata.json`,构建脚本不得自行维护另一套 hash 或路径规则。版本化元数据与对应 zip 一一匹配,同版本可覆盖、其他历史版本保留,旧无版本文件在构建时清理。 - 发布包格式固定为 `cmshopee-portable-v1`,入口为 `cmshopee.exe`,程序依赖集中在 `_internal/`。manifest 覆盖除自身外的所有程序文件,并记录规范化相对路径、字节数和 SHA-256。 - 发布包只能包含程序根项目,`data/` 和 `.cmshopee-update/` 永远在替换边界之外。第一阶段预留签名字段,但 SHA-256 只负责传输完整性,不等同于发布者身份认证。 - T-615 只提供可验证发布契约;启动门禁仍保持 T-544 的人工下载行为,直到后续下载、独立更新器、事务替换和失败熔断任务全部接入。 diff --git a/docs/packaging.md b/docs/packaging.md index c886e3d..6d89ab3 100644 --- a/docs/packaging.md +++ b/docs/packaging.md @@ -88,7 +88,7 @@ release\ README.txt package-manifest.json 蝦皮圈優化助手0.1.0.zip - release-metadata.json + release-metadata0.1.0.json ``` 说明: @@ -99,7 +99,7 @@ release\ - `version.txt` 使用 ASCII/UTF-8 无 BOM 写入 `APP_VERSION`,用于人工排查和未来更新机制读取。 - `README.txt` 用中文写明启动方式、不要放入 `Program Files`、保留 `data\`、升级时覆盖程序文件但不覆盖 `data\`。 - `package-manifest.json` 记录包格式、版本、入口、更新器协议、允许替换根项目,以及除清单自身外每个程序文件的规范化路径、字节数和 SHA-256。 -- `release-metadata.json` 记录整个 zip 的准确字节数和 SHA-256,并提供服务端发布字段模板;它不放进 zip。 +- `release-metadata.json` 记录对应版本整个 zip 的准确字节数和 SHA-256,并提供服务端发布字段模板;例如 `0.1.6` 生成 `release-metadata0.1.6.json`。它不放进 zip。 当前 PyInstaller 6.11.1 的 onedir 为集中依赖结构,所以 `release\蝦皮圈優化助手\` 必须包含 `cmshopee.exe` 和 `_internal\`,不得只复制单个 exe。 @@ -108,7 +108,7 @@ release\ 1. 修改 `app/version.py` 中的 `APP_VERSION`。 2. 运行语法检查和单元测试。 3. 运行 `powershell -ExecutionPolicy Bypass -File scripts\build_exe.ps1`。 -4. 检查 `version.txt`、GUI 标题栏、`package-manifest.json`、压缩包文件名和 `release-metadata.json` 的版本一致。 +4. 检查 `version.txt`、GUI 标题栏、`package-manifest.json`、压缩包文件名和 `release-metadata.json` 的版本一致。 5. 在无 Python 环境的 Windows 10/11 机器上解压 `蝦皮圈優化助手.zip` 并启动验证。 正式发布构建固定使用 Python 3.10。源码在 Python 3.7.9 上曾验证可运行,但发布包不再以 3.7.9 作为打包基线;脚本会直接调用 `py -3.10` 并校验返回版本必须是 `3.10.x`,避免同一个 `APP_VERSION` 在不同 Python 版本下打出不可比对的包。CI 仍可继续使用 Python 3.11 做自动测试。 @@ -172,7 +172,7 @@ T-544 第一版目标是**启动时检查是否必须升级**,但仍不做自 ### 3.5 自动升级发布基础(T-615) -`app/release_manifest.py` 是发布清单与 zip 元数据的唯一生成逻辑。构建脚本按以下顺序执行:组装 release 目录、拒绝用户数据和未知根项目、生成 `package-manifest.json`、压缩 zip、计算 zip hash/大小、生成 `release-metadata.json`。允许的根项目仅为 `cmshopee.exe`、`_internal/`、`version.txt`、`README.txt`、清单和后续独立更新器;明确禁止 `data/` 与 `.cmshopee-update/`。 +`app/release_manifest.py` 是发布清单与 zip 元数据的唯一生成逻辑。构建脚本按以下顺序执行:组装 release 目录、拒绝用户数据和未知根项目、生成 `package-manifest.json`、压缩 zip、计算 zip hash/大小、生成 `release-metadata.json`。同版本重打只覆盖当前版本元数据,保留其他版本文件,并清理历史无版本 `release-metadata.json`。允许的根项目仅为 `cmshopee.exe`、`_internal/`、`version.txt`、`README.txt`、清单和后续独立更新器;明确禁止 `data/` 与 `.cmshopee-update/`。 T-615 不改变客户端行为:T-544 仍只打开浏览器下载。客户端下载、解压校验、独立进程替换、回滚和重启分别由 T-616 至 T-619 实现。清单预留签名字段,但当前只验证完整性,不宣称已验证发布者身份。 diff --git a/docs/tasks/T-625.md b/docs/tasks/T-625.md index adf1624..4798151 100644 --- a/docs/tasks/T-625.md +++ b/docs/tasks/T-625.md @@ -3,7 +3,7 @@ id: T-625 title: 发布元数据文件名增加应用版本号 phase: 8 deps: [T-624] -status: TODO +status: DONE created: 2026-07-13 --- @@ -15,7 +15,7 @@ created: 2026-07-13 ## 方案 -1. 修改 `scripts/build_exe.ps1`,从已经读取的 `$appVersion` 生成 `$releaseMetadata = Join-Path $releaseRoot "release-metadata$appVersion.json"`;版本来源仍只有 `app/version.py`。 +1. 修改 `scripts/build_exe.ps1`,从已经读取的 `$appVersion` 生成 `$releaseMetadata = Join-Path $releaseRoot "release-metadata${appVersion}.json"`;版本来源仍只有 `app/version.py`。 2. 同版本重新打包前删除并重建对应的版本化元数据文件;保留 `release/` 下其他版本的 `release-metadata<版本>.json`,便于和历史 zip 一一对应。 3. 构建时清理历史无版本文件 `release/release-metadata.json`,避免运营误把旧文件当成本轮元数据;不得用通配符批量删除其他版本文件。 4. `app.release_manifest metadata --output` 继续接收脚本传入的完整路径,JSON 内容、zip SHA-256、`size_bytes` 和 `release.version` 生成逻辑不变;元数据仍不放进便携 zip。 @@ -31,7 +31,7 @@ created: 2026-07-13 ## 测试要求 -- 更新 `tests/test_packaging.py`,断言构建脚本使用 `release-metadata$appVersion.json`,不再把无版本文件作为当前输出,并且只清理明确的旧文件名和当前版本文件。 +- 更新 `tests/test_packaging.py`,断言构建脚本使用 `release-metadata${appVersion}.json`,不再把无版本文件作为当前输出,并且只清理明确的旧文件名和当前版本文件。 - 使用临时 zip 调用 `app.release_manifest metadata`,验证版本化输出文件名、JSON 版本、大小和 SHA-256。 - 运行 `python -m ruff check app tests main.py`、`python -m compileall app main.py`、`python -m unittest discover -s tests` 和 `git diff --check`。 - 条件允许时运行一次 `scripts/build_exe.ps1`,核对真实 release 目录;未运行必须在执行记录中说明。 @@ -45,4 +45,7 @@ created: 2026-07-13 ## 执行记录 -- 尚未执行。 +- 2026-07-13:构建脚本改为生成 `release-metadata${appVersion}.json`,同版本重打覆盖当前文件,保留其他版本化元数据,并只额外清理历史无版本 `release-metadata.json`。 +- 2026-07-13:补齐构建脚本静态约束和版本化输出文件测试,同步架构、版本接口与打包文档;发布元数据 JSON schema 和自动升级运行逻辑未改变。 +- 2026-07-13:独立干净 worktree 验证通过:Ruff、Python 3.10 compileall、完整 unittest 439 项及 `git diff --check` 全绿。 +- 2026-07-13:使用当前工作区 `APP_VERSION=0.1.5` 完成真实 Windows 打包,生成 `release-metadata0.1.5.json`;JSON 版本/zip文件名正确,zip大小 `53295265` 与 SHA-256 `187638dd14875d7039db4b16c570f66b1878909d850f3c8b69d50941706866bc` 均与实物一致。历史 `release-metadata0.1.6.json` 保留,旧无版本文件不存在,zip 内不含元数据文件。 diff --git a/docs/update-check.md b/docs/update-check.md index ad9b801..1adc162 100644 --- a/docs/update-check.md +++ b/docs/update-check.md @@ -55,7 +55,7 @@ T-617 已提供独立无控制台更新器和事务回滚能力:更新器从 ## 五、发版约定(服务端据此控制) -自动安装使用的全部字段必须放在同一个 `release` 对象内,不得把版本取自一个对象、hash 取自另一个对象。构建脚本生成的 `release/release-metadata.json` 提供版本、zip SHA-256 和准确大小;发布人员录入公网 HTTP/HTTPS `download_url`、强制策略和中文发布说明,不得手工改写 hash 或大小。正式发布仍应优先使用 HTTPS;HTTP 只用于外链暂时无法提供 HTTPS 的兼容期。 +自动安装使用的全部字段必须放在同一个 `release` 对象内,不得把版本取自一个对象、hash 取自另一个对象。构建脚本生成的 `release/release-metadata.json` 提供版本、zip SHA-256 和准确大小;发布人员应选择与待发布 zip 版本一致的元数据文件,再录入公网 HTTP/HTTPS `download_url`、强制策略和中文发布说明,不得手工改写 hash 或大小。正式发布仍应优先使用 HTTPS;HTTP 只用于外链暂时无法提供 HTTPS 的兼容期。 | 想要的效果 | 服务端返回 | | --- | --- | diff --git a/scripts/build_exe.ps1 b/scripts/build_exe.ps1 index 0833e40..b7ed86e 100644 --- a/scripts/build_exe.ps1 +++ b/scripts/build_exe.ps1 @@ -165,9 +165,10 @@ $releaseRoot = Join-Path $repoRoot "release" $releaseName = "$appName$appVersion" $releaseDir = Join-Path $releaseRoot $releaseName $portableZip = Join-Path $releaseRoot "$releaseName.zip" -$releaseMetadata = Join-Path $releaseRoot "release-metadata.json" +$releaseMetadata = Join-Path $releaseRoot "release-metadata${appVersion}.json" +$legacyReleaseMetadata = Join-Path $releaseRoot "release-metadata.json" -foreach ($path in @($releaseDir, $portableZip, $releaseMetadata)) { +foreach ($path in @($releaseDir, $portableZip, $releaseMetadata, $legacyReleaseMetadata)) { Assert-InProject -Path $path if (Test-Path -LiteralPath $path) { Remove-Item -LiteralPath $path -Recurse -Force @@ -202,7 +203,7 @@ if ($LASTEXITCODE -ne 0) { Compress-ArchiveWithRetry -SourcePath (Join-Path $releaseDir "*") -DestinationPath $portableZip & $pythonExe @pythonArgs -m app.release_manifest metadata --zip $portableZip --version $appVersion --output $releaseMetadata if ($LASTEXITCODE -ne 0) { - throw "生成 release-metadata.json 失败,退出码: $LASTEXITCODE" + throw "生成 release-metadata${appVersion}.json 失败,退出码: $LASTEXITCODE" } Write-Host "$(Decode-Utf8Base64 "5p6E5bu65a6M5oiQOg==") $exePath" diff --git a/tests/test_packaging.py b/tests/test_packaging.py index a1cfc82..2a06456 100644 --- a/tests/test_packaging.py +++ b/tests/test_packaging.py @@ -95,7 +95,23 @@ class PackagingTests(unittest.TestCase): self.assertIn('Assert-NoExcludedData -Root $releaseDir', script) self.assertIn('-m app.release_manifest manifest', script) self.assertIn('-m app.release_manifest metadata', script) - self.assertIn('release-metadata.json', script) + self.assertIn( + '$releaseMetadata = Join-Path $releaseRoot "release-metadata${appVersion}.json"', + script, + ) + self.assertIn( + '$legacyReleaseMetadata = Join-Path $releaseRoot "release-metadata.json"', + script, + ) + self.assertIn( + '@($releaseDir, $portableZip, $releaseMetadata, $legacyReleaseMetadata)', + script, + ) + self.assertNotIn( + '$releaseMetadata = Join-Path $releaseRoot "release-metadata.json"', + script, + ) + self.assertNotIn('release-metadata*.json', script) self.assertIn('cmshopee-updater.spec', script) self.assertIn('cmshopee-updater.exe', script) @@ -158,6 +174,25 @@ class PackagingTests(unittest.TestCase): ) self.assertEqual("cmshopee-portable-v1", release["package_format"]) + def test_release_metadata_can_be_written_to_versioned_filename(self): + with tempfile.TemporaryDirectory() as temp_dir: + zip_path = Path(temp_dir) / "蝦皮圈優化助手1.2.3.zip" + zip_path.write_bytes(b"portable-zip") + output = Path(temp_dir) / "release-metadata1.2.3.json" + + written = release_manifest.write_release_metadata(zip_path, "1.2.3", output) + metadata = json.loads(written.read_text(encoding="utf-8")) + + self.assertEqual(output, written) + self.assertEqual("release-metadata1.2.3.json", written.name) + self.assertEqual("1.2.3", metadata["release"]["version"]) + self.assertEqual(zip_path.name, metadata["release"]["download_filename"]) + self.assertEqual(zip_path.stat().st_size, metadata["release"]["size_bytes"]) + self.assertEqual( + hashlib.sha256(b"portable-zip").hexdigest(), + metadata["release"]["sha256"], + ) + if __name__ == "__main__": unittest.main()