feat: 统一 Client 发布产物命名 (#102)

This commit is contained in:
chengma
2026-08-10 14:55:17 +08:00
parent 6fd25984d2
commit 240626f945
12 changed files with 119 additions and 25 deletions
+14 -1
View File
@@ -5,11 +5,24 @@ from __future__ import annotations
import argparse
import hashlib
import json
import re
from pathlib import Path
from typing import Any
MANIFEST_FILE_NAME = "autobuy——manifest.json"
MANIFEST_FILE_NAME = "autobuy_manifest.json"
_VERSION_PATTERN = re.compile(
r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:\.(0|[1-9]\d*))?$"
)
def versioned_manifest_file_name(version: str) -> str:
"""返回版本化清单文件名,例如 ``autobuy_manifest_0.2.2.json``。"""
normalized = version.strip()
if _VERSION_PATTERN.fullmatch(normalized) is None:
raise ValueError("版本号必须是三段或四段非负整数")
return f"autobuy_manifest_{normalized}.json"
def file_description(path: Path) -> dict[str, Any]:
+13 -4
View File
@@ -160,23 +160,32 @@ VSVersionInfo(
[Text.UTF8Encoding]::new($false)
)
$portableZip = Join-Path $releaseRoot "$packageName-portable.zip"
# Windows PowerShell 5 may read a UTF-8 script without BOM as the system code page.
# Build the Chinese product name from Unicode code points so the release name is stable.
$portableProductName = -join @(
[char]0x81EA, [char]0x52A8, [char]0x91C7, [char]0x96C6,
[char]0x91C7, [char]0x8D2D, [char]0x5DE5, [char]0x5177
)
$portableZip = Join-Path $releaseRoot "$($portableProductName)_$version.zip"
Compress-Archive -Path (Join-Path $packageRoot "*") -DestinationPath $portableZip -CompressionLevel Optimal
$portableLatestZip = Join-Path $releaseRoot "$portableProductName.zip"
Copy-Item -LiteralPath $portableZip -Destination $portableLatestZip
$updateStage = Join-Path $buildRoot "update-stage"
New-Item -ItemType Directory -Force -Path $updateStage | Out-Null
Copy-Item -LiteralPath $appRoot -Destination (Join-Path $updateStage "app") -Recurse
$updateZip = Join-Path $releaseRoot "$packageName-update.zip"
$updateZip = Join-Path $releaseRoot "CMAutoBuy_$version.zip"
Compress-Archive -Path (Join-Path $updateStage "*") -DestinationPath $updateZip -CompressionLevel Optimal
Write-Host "[7/7] Generating the release manifest..."
$emDash = [char]0x2014
$manifestPath = Join-Path $releaseRoot ("autobuy{0}{0}manifest.json" -f $emDash)
$manifestPath = Join-Path $releaseRoot "autobuy_manifest.json"
Invoke-Checked -FailureMessage "Failed to generate the release manifest" -Command {
& $buildPython (Join-Path $clientRoot "build_tools\release_manifest.py") `
--version $version --update $updateZip --portable $portableZip `
--output $manifestPath
}
$versionedManifestPath = Join-Path $releaseRoot "autobuy_manifest_$version.json"
Copy-Item -LiteralPath $manifestPath -Destination $versionedManifestPath
Write-Host "Build complete: $releaseRoot"
Write-Host "Release manifest: $manifestPath"
+1 -1
View File
@@ -300,7 +300,7 @@ class SettingsPage(QWidget):
self.currentVersionLabel.setAccessibleName("当前软件版本")
self.updateManifestUrlInput = LineEdit(self)
self.updateManifestUrlInput.setPlaceholderText(
"https://updates.example.com/autobuy——manifest.json"
"https://updates.example.com/autobuy_manifest.json"
)
self.updateManifestUrlInput.setClearButtonEnabled(True)
self.updateManifestUrlInput.setAccessibleName("在线更新清单地址")
+2 -1
View File
@@ -30,7 +30,8 @@ MAX_UPDATE_BYTES = 500 * 1024 * 1024
MAX_EXTRACTED_BYTES = 1024 * 1024 * 1024
UPDATE_MANIFEST_SETTING = "update.manifest_url"
UPDATE_USERNAME_SETTING = "update.username"
DEFAULT_UPDATE_MANIFEST_URL = "http://cm.xiapi.com/autobuy——manifest.json"
DEFAULT_UPDATE_MANIFEST_URL = "http://cm.xiapi.com/autobuy_manifest.json"
LEGACY_DEFAULT_UPDATE_MANIFEST_URL = "http://cm.xiapi.com/autobuy——manifest.json"
DEFAULT_UPDATE_USERNAME = "admin"
_ALLOWED_HTTP_UPDATE_HOST = "cm.xiapi.com"
_VERSION_PATTERN = re.compile(r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:\.(0|[1-9]\d*))?$")
+4
View File
@@ -12,6 +12,7 @@ from .settings_repository import SettingsRepository
from .update_service import (
DEFAULT_UPDATE_MANIFEST_URL,
DEFAULT_UPDATE_USERNAME,
LEGACY_DEFAULT_UPDATE_MANIFEST_URL,
UPDATE_MANIFEST_SETTING,
UPDATE_USERNAME_SETTING,
UpdateCancelled,
@@ -142,6 +143,9 @@ class UpdateUiEventBinder(QObject):
if saved_url_record is not None and isinstance(saved_url_record.value, str)
else DEFAULT_UPDATE_MANIFEST_URL
)
if saved_url == LEGACY_DEFAULT_UPDATE_MANIFEST_URL:
saved_url = DEFAULT_UPDATE_MANIFEST_URL
repository.set(UPDATE_MANIFEST_SETTING, saved_url)
saved_username = (
saved_username_record.value
if saved_username_record is not None
+1 -1
View File
@@ -3,4 +3,4 @@
发布脚本和程序界面需要版本号时都从这里读取,避免多个文件各写一份。
"""
__version__ = "0.2.1"
__version__ = "0.2.2"
+16 -4
View File
@@ -16,7 +16,11 @@ from build_tools.launcher import (
install_root,
is_main_program_running,
)
from build_tools.release_manifest import MANIFEST_FILE_NAME, write_manifest
from build_tools.release_manifest import (
MANIFEST_FILE_NAME,
versioned_manifest_file_name,
write_manifest,
)
from src.db import data_dir
@@ -140,15 +144,15 @@ class ReleaseManifestTest(unittest.TestCase):
def test_manifest_name_and_hash_match_release_files(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
update_zip = root / "CMAutoBuy-0.1.0-update.zip"
portable_zip = root / "CMAutoBuy-0.1.0-portable.zip"
update_zip = root / "CMAutoBuy_0.1.0.zip"
portable_zip = root / "自动采集采购工具_0.1.0.zip"
update_zip.write_bytes(b"update")
portable_zip.write_bytes(b"portable")
output = root / MANIFEST_FILE_NAME
write_manifest("0.1.0", update_zip, portable_zip, output)
self.assertEqual(output.name, "autobuy——manifest.json")
self.assertEqual(output.name, "autobuy_manifest.json")
manifest = json.loads(output.read_text(encoding="utf-8"))
self.assertEqual(manifest["version"], "0.1.0")
self.assertEqual(manifest["update"]["size"], len(b"update"))
@@ -158,6 +162,14 @@ class ReleaseManifestTest(unittest.TestCase):
)
self.assertEqual(manifest["portable"]["file"], portable_zip.name)
def test_versioned_manifest_name_is_stable_and_validated(self):
self.assertEqual(
versioned_manifest_file_name("0.1.0"),
"autobuy_manifest_0.1.0.json",
)
with self.assertRaises(ValueError):
versioned_manifest_file_name("../latest")
if __name__ == "__main__":
unittest.main()
+4 -3
View File
@@ -12,6 +12,7 @@ from pathlib import Path
from src.update_service import (
DEFAULT_UPDATE_MANIFEST_URL,
LEGACY_DEFAULT_UPDATE_MANIFEST_URL,
UnsafeUpdateArchiveError,
UpdateConfigurationError,
UpdateIntegrityError,
@@ -153,9 +154,9 @@ class UpdateServiceTest(unittest.TestCase):
)
self.assertNotIn("unit-test-password", repr(credentials))
def test_default_unicode_manifest_path_is_encoded_for_http_request(self):
def test_legacy_unicode_manifest_path_is_encoded_for_http_request(self):
request = UpdateService._make_request(
DEFAULT_UPDATE_MANIFEST_URL,
LEGACY_DEFAULT_UPDATE_MANIFEST_URL,
UpdateCredentials("release-reader", "unit-test-password"),
)
@@ -257,7 +258,7 @@ class UpdateServiceTest(unittest.TestCase):
health = json.loads(
(self.update_directory / "healthy.json").read_text(encoding="utf-8")
)
self.assertEqual(health["version"], "0.2.1")
self.assertEqual(health["version"], "0.2.2")
if __name__ == "__main__":
+50 -4
View File
@@ -19,6 +19,7 @@ from src.settings_ui import SettingsPage
from src.update_service import (
DEFAULT_UPDATE_MANIFEST_URL,
DEFAULT_UPDATE_USERNAME,
LEGACY_DEFAULT_UPDATE_MANIFEST_URL,
UPDATE_MANIFEST_SETTING,
UPDATE_USERNAME_SETTING,
UpdateCheckResult,
@@ -46,7 +47,7 @@ class FakeCredentialStore:
class FakeUpdateService:
def __init__(self, result=None, delay=0.0, download_error=None):
self.result = result or UpdateCheckResult("0.2.1", "0.2.1", False)
self.result = result or UpdateCheckResult("0.2.2", "0.2.2", False)
self.delay = delay
self.download_error = download_error
self.check_count = 0
@@ -57,7 +58,7 @@ class FakeUpdateService:
def check(
self,
manifest_url,
current_version="0.2.1",
current_version="0.2.2",
is_cancelled=None,
credentials=None,
):
@@ -141,7 +142,7 @@ class UpdateUiEventTest(unittest.TestCase):
def test_update_card_uses_safe_defaults_and_password_input(self):
page = self._page(FakeUpdateService())
self.assertEqual(page.currentVersionLabel.text(), "0.2.1")
self.assertEqual(page.currentVersionLabel.text(), "0.2.2")
self.assertEqual(
page.updateManifestUrlInput.text(), DEFAULT_UPDATE_MANIFEST_URL
)
@@ -257,6 +258,51 @@ class UpdateUiEventTest(unittest.TestCase):
page.eventBinder.shutdown()
page.deleteLater()
def test_legacy_default_manifest_url_is_migrated_before_auto_check(self):
self.repository.set_many(
{
UPDATE_MANIFEST_SETTING: LEGACY_DEFAULT_UPDATE_MANIFEST_URL,
UPDATE_USERNAME_SETTING: TEST_USERNAME,
}
)
store = FakeCredentialStore((TEST_USERNAME, TEST_PASSWORD))
service = FakeUpdateService()
page = self._page(service, store)
self._wait_until(lambda: service.check_count == 1)
self._wait_until(
lambda: page.eventBinder.updateEventBinder._check_thread is None
)
self.assertEqual(
self.repository.get(UPDATE_MANIFEST_SETTING),
DEFAULT_UPDATE_MANIFEST_URL,
)
self.assertEqual(service.manifest_url, DEFAULT_UPDATE_MANIFEST_URL)
page.eventBinder.shutdown()
page.deleteLater()
def test_custom_manifest_url_is_not_migrated(self):
self.repository.set_many(
{
UPDATE_MANIFEST_SETTING: MANIFEST_URL,
UPDATE_USERNAME_SETTING: TEST_USERNAME,
}
)
store = FakeCredentialStore((TEST_USERNAME, TEST_PASSWORD))
service = FakeUpdateService()
page = self._page(service, store)
self._wait_until(lambda: service.check_count == 1)
self._wait_until(
lambda: page.eventBinder.updateEventBinder._check_thread is None
)
self.assertEqual(self.repository.get(UPDATE_MANIFEST_SETTING), MANIFEST_URL)
self.assertEqual(service.manifest_url, MANIFEST_URL)
page.eventBinder.shutdown()
page.deleteLater()
def test_confirmed_new_version_downloads_with_saved_credentials(self):
update = UpdateInfo(
version="0.3.0",
@@ -267,7 +313,7 @@ class UpdateUiEventTest(unittest.TestCase):
sha256="0" * 64,
)
service = FakeUpdateService(
UpdateCheckResult("0.2.1", "0.3.0", True, update)
UpdateCheckResult("0.2.2", "0.3.0", True, update)
)
page = self._page(service)
self._save_configuration(page)