build: version release metadata filename

This commit is contained in:
chengma
2026-07-13 17:36:46 +08:00
parent e79ec00bf6
commit 4927da5e1c
6 changed files with 53 additions and 14 deletions
+1 -1
View File
@@ -527,7 +527,7 @@ cmshopee/
### 10.1 发布与自动升级边界 ### 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<APP_VERSION>.json`,构建脚本不得自行维护另一套 hash 或路径规则。版本化元数据与对应 zip 一一匹配,同版本可覆盖、其他历史版本保留,旧无版本文件在构建时清理。
- 发布包格式固定为 `cmshopee-portable-v1`,入口为 `cmshopee.exe`,程序依赖集中在 `_internal/`。manifest 覆盖除自身外的所有程序文件,并记录规范化相对路径、字节数和 SHA-256。 - 发布包格式固定为 `cmshopee-portable-v1`,入口为 `cmshopee.exe`,程序依赖集中在 `_internal/`。manifest 覆盖除自身外的所有程序文件,并记录规范化相对路径、字节数和 SHA-256。
- 发布包只能包含程序根项目,`data/` 和 `.cmshopee-update/` 永远在替换边界之外。第一阶段预留签名字段,但 SHA-256 只负责传输完整性,不等同于发布者身份认证。 - 发布包只能包含程序根项目,`data/` 和 `.cmshopee-update/` 永远在替换边界之外。第一阶段预留签名字段,但 SHA-256 只负责传输完整性,不等同于发布者身份认证。
- T-615 只提供可验证发布契约;启动门禁仍保持 T-544 的人工下载行为,直到后续下载、独立更新器、事务替换和失败熔断任务全部接入。 - T-615 只提供可验证发布契约;启动门禁仍保持 T-544 的人工下载行为,直到后续下载、独立更新器、事务替换和失败熔断任务全部接入。
+4 -4
View File
@@ -88,7 +88,7 @@ release\
README.txt README.txt
package-manifest.json package-manifest.json
蝦皮圈優化助手0.1.0.zip 蝦皮圈優化助手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`,用于人工排查和未来更新机制读取。 - `version.txt` 使用 ASCII/UTF-8 无 BOM 写入 `APP_VERSION`,用于人工排查和未来更新机制读取。
- `README.txt` 用中文写明启动方式、不要放入 `Program Files`、保留 `data\`、升级时覆盖程序文件但不覆盖 `data\`。 - `README.txt` 用中文写明启动方式、不要放入 `Program Files`、保留 `data\`、升级时覆盖程序文件但不覆盖 `data\`。
- `package-manifest.json` 记录包格式、版本、入口、更新器协议、允许替换根项目,以及除清单自身外每个程序文件的规范化路径、字节数和 SHA-256。 - `package-manifest.json` 记录包格式、版本、入口、更新器协议、允许替换根项目,以及除清单自身外每个程序文件的规范化路径、字节数和 SHA-256。
- `release-metadata.json` 记录整个 zip 的准确字节数和 SHA-256,并提供服务端发布字段模板;它不放进 zip。 - `release-metadata<APP_VERSION>.json` 记录对应版本整个 zip 的准确字节数和 SHA-256,并提供服务端发布字段模板;例如 `0.1.6` 生成 `release-metadata0.1.6.json`。它不放进 zip。
当前 PyInstaller 6.11.1 的 onedir 为集中依赖结构,所以 `release\蝦皮圈優化助手<APP_VERSION>\` 必须包含 `cmshopee.exe` 和 `_internal\`,不得只复制单个 exe。 当前 PyInstaller 6.11.1 的 onedir 为集中依赖结构,所以 `release\蝦皮圈優化助手<APP_VERSION>\` 必须包含 `cmshopee.exe` 和 `_internal\`,不得只复制单个 exe。
@@ -108,7 +108,7 @@ release\
1. 修改 `app/version.py` 中的 `APP_VERSION`。 1. 修改 `app/version.py` 中的 `APP_VERSION`。
2. 运行语法检查和单元测试。 2. 运行语法检查和单元测试。
3. 运行 `powershell -ExecutionPolicy Bypass -File scripts\build_exe.ps1`。 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<APP_VERSION>.json` 的版本一致。
5. 在无 Python 环境的 Windows 10/11 机器上解压 `蝦皮圈優化助手<APP_VERSION>.zip` 并启动验证。 5. 在无 Python 环境的 Windows 10/11 机器上解压 `蝦皮圈優化助手<APP_VERSION>.zip` 并启动验证。
正式发布构建固定使用 Python 3.10。源码在 Python 3.7.9 上曾验证可运行,但发布包不再以 3.7.9 作为打包基线;脚本会直接调用 `py -3.10` 并校验返回版本必须是 `3.10.x`,避免同一个 `APP_VERSION` 在不同 Python 版本下打出不可比对的包。CI 仍可继续使用 Python 3.11 做自动测试。 正式发布构建固定使用 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) ### 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<APP_VERSION>.json`。同版本重打只覆盖当前版本元数据,保留其他版本文件,并清理历史无版本 `release-metadata.json`。允许的根项目仅为 `cmshopee.exe`、`_internal/`、`version.txt`、`README.txt`、清单和后续独立更新器;明确禁止 `data/` 与 `.cmshopee-update/`。
T-615 不改变客户端行为:T-544 仍只打开浏览器下载。客户端下载、解压校验、独立进程替换、回滚和重启分别由 T-616 至 T-619 实现。清单预留签名字段,但当前只验证完整性,不宣称已验证发布者身份。 T-615 不改变客户端行为:T-544 仍只打开浏览器下载。客户端下载、解压校验、独立进程替换、回滚和重启分别由 T-616 至 T-619 实现。清单预留签名字段,但当前只验证完整性,不宣称已验证发布者身份。
+7 -4
View File
@@ -3,7 +3,7 @@ id: T-625
title: 发布元数据文件名增加应用版本号 title: 发布元数据文件名增加应用版本号
phase: 8 phase: 8
deps: [T-624] deps: [T-624]
status: TODO status: DONE
created: 2026-07-13 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 一一对应。 2. 同版本重新打包前删除并重建对应的版本化元数据文件;保留 `release/` 下其他版本的 `release-metadata<版本>.json`,便于和历史 zip 一一对应。
3. 构建时清理历史无版本文件 `release/release-metadata.json`,避免运营误把旧文件当成本轮元数据;不得用通配符批量删除其他版本文件。 3. 构建时清理历史无版本文件 `release/release-metadata.json`,避免运营误把旧文件当成本轮元数据;不得用通配符批量删除其他版本文件。
4. `app.release_manifest metadata --output` 继续接收脚本传入的完整路径,JSON 内容、zip SHA-256、`size_bytes` 和 `release.version` 生成逻辑不变;元数据仍不放进便携 zip。 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。 - 使用临时 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`。 - 运行 `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 目录;未运行必须在执行记录中说明。 - 条件允许时运行一次 `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 内不含元数据文件。
+1 -1
View File
@@ -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<APP_VERSION>.json` 提供版本、zip SHA-256 和准确大小;发布人员应选择与待发布 zip 版本一致的元数据文件,再录入公网 HTTP/HTTPS `download_url`、强制策略和中文发布说明,不得手工改写 hash 或大小。正式发布仍应优先使用 HTTPS;HTTP 只用于外链暂时无法提供 HTTPS 的兼容期。
| 想要的效果 | 服务端返回 | | 想要的效果 | 服务端返回 |
| --- | --- | | --- | --- |
+4 -3
View File
@@ -165,9 +165,10 @@ $releaseRoot = Join-Path $repoRoot "release"
$releaseName = "$appName$appVersion" $releaseName = "$appName$appVersion"
$releaseDir = Join-Path $releaseRoot $releaseName $releaseDir = Join-Path $releaseRoot $releaseName
$portableZip = Join-Path $releaseRoot "$releaseName.zip" $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 Assert-InProject -Path $path
if (Test-Path -LiteralPath $path) { if (Test-Path -LiteralPath $path) {
Remove-Item -LiteralPath $path -Recurse -Force Remove-Item -LiteralPath $path -Recurse -Force
@@ -202,7 +203,7 @@ if ($LASTEXITCODE -ne 0) {
Compress-ArchiveWithRetry -SourcePath (Join-Path $releaseDir "*") -DestinationPath $portableZip Compress-ArchiveWithRetry -SourcePath (Join-Path $releaseDir "*") -DestinationPath $portableZip
& $pythonExe @pythonArgs -m app.release_manifest metadata --zip $portableZip --version $appVersion --output $releaseMetadata & $pythonExe @pythonArgs -m app.release_manifest metadata --zip $portableZip --version $appVersion --output $releaseMetadata
if ($LASTEXITCODE -ne 0) { if ($LASTEXITCODE -ne 0) {
throw "生成 release-metadata.json 失败,退出码: $LASTEXITCODE" throw "生成 release-metadata${appVersion}.json 失败,退出码: $LASTEXITCODE"
} }
Write-Host "$(Decode-Utf8Base64 "5p6E5bu65a6M5oiQOg==") $exePath" Write-Host "$(Decode-Utf8Base64 "5p6E5bu65a6M5oiQOg==") $exePath"
+36 -1
View File
@@ -95,7 +95,23 @@ class PackagingTests(unittest.TestCase):
self.assertIn('Assert-NoExcludedData -Root $releaseDir', script) self.assertIn('Assert-NoExcludedData -Root $releaseDir', script)
self.assertIn('-m app.release_manifest manifest', script) self.assertIn('-m app.release_manifest manifest', script)
self.assertIn('-m app.release_manifest metadata', 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.spec', script)
self.assertIn('cmshopee-updater.exe', script) self.assertIn('cmshopee-updater.exe', script)
@@ -158,6 +174,25 @@ class PackagingTests(unittest.TestCase):
) )
self.assertEqual("cmshopee-portable-v1", release["package_format"]) 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__": if __name__ == "__main__":
unittest.main() unittest.main()