build: version release metadata filename
This commit is contained in:
@@ -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
@@ -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
@@ -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 内不含元数据文件。
|
||||||
|
|||||||
@@ -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 的兼容期。
|
||||||
|
|
||||||
| 想要的效果 | 服务端返回 |
|
| 想要的效果 | 服务端返回 |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
|
|||||||
@@ -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
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user