diff --git a/.github/scripts/update_version_manifest.py b/.github/scripts/update_version_manifest.py index 5ae436e3..c2839797 100644 --- a/.github/scripts/update_version_manifest.py +++ b/.github/scripts/update_version_manifest.py @@ -50,7 +50,7 @@ def sign_update(payload, private_key, expected_public_key): return {"payload": payload, "signature": signature.hex()} -def version_info(version, fallback_date=None): +def version_fields(version): version = version.removeprefix("v") base = r"[0-9]+(?:\.[0-9]+){1,3}" for pattern in ( @@ -60,18 +60,41 @@ def version_info(version, fallback_date=None): ): match = re.fullmatch(pattern, version) if match: - fields = match.groupdict() - date = fields.get("date") - release_date = (datetime.datetime.strptime(date, "%Y%m%d").date().isoformat() - if date else fallback_date) - if not release_date: - raise ValueError(f"Release date is missing for {version}") - datetime.date.fromisoformat(release_date) - return {"latest_version": version, "version": version, - "releaseDate": release_date, "patch": int(fields.get("patch") or 0)} + return match.groupdict() raise ValueError(f"Invalid release version: {version}") +def version_info(version, fallback_date=None): + fields = version_fields(version) + version = version.removeprefix("v") + date = fields.get("date") + release_date = (datetime.datetime.strptime(date, "%Y%m%d").date().isoformat() + if date else fallback_date) + if not release_date: + raise ValueError(f"Release date is missing for {version}") + datetime.date.fromisoformat(release_date) + return {"latest_version": version, "version": version, + "releaseDate": release_date, "patch": int(fields.get("patch") or 0)} + + +def release_order(metadata): + """Match client ordering: zero-padded numeric components, then patch, never date.""" + version = metadata.get("latest_version") or metadata.get("version") + if not version: + return None + fields = version_fields(version) + numbers = tuple(int(part) for part in fields["base"].split(".")) + inline_patch = fields.get("patch") or "0" + patch = int(inline_patch) if len(inline_patch) <= 4 else 0 + # Legacy manifests may store the effective patch outside the version string. + explicit_patch = metadata.get("patch") + if isinstance(explicit_patch, str) and re.fullmatch(r"[0-9]+", explicit_patch): + explicit_patch = int(explicit_patch) + if type(explicit_patch) is int and 0 <= explicit_patch <= 2147483647: + patch = explicit_patch + return numbers + (0,) * (4 - len(numbers)) + (patch,) + + def artifact_downloads(directory): downloads = {} private_key = os.environ.get("CROSSDESK_UPDATE_PRIVATE_KEY", "") @@ -129,10 +152,25 @@ def merge_release(current, tag, name, notes, downloads, published_date=None): if not tag.startswith("v"): raise ValueError(f"Expected a version tag, got {tag!r}") metadata = version_info(tag, published_date) + current_order = release_order(current) + if current_order is not None and release_order(metadata) < current_order: + current_version = current.get("latest_version") or current.get("version") + print(f"Skipping {tag}: older than current desktop release {current_version}") + return dict(current) + + accepted_downloads = {} + for platform, download in downloads.items(): + previous_order = release_order(current["downloads"].get(platform, {})) + if previous_order is not None and release_order(download) < previous_order: + print(f"Skipping {tag} download for {platform}: a newer version is already published") + continue + accepted_downloads[platform] = download + if mobile and not accepted_downloads: + return dict(current) result = dict(current) result.pop("platforms", None) - result["downloads"] = {**current["downloads"], **downloads} + result["downloads"] = {**current["downloads"], **accepted_downloads} if not mobile: result.update({**metadata, "releaseName": name, "releaseNotes": notes, "tagName": tag}) return result @@ -159,11 +197,16 @@ def main(): parser.error("Supply --release-json or --tag, --name and --notes") tag, name, notes = args.tag, args.name, args.notes.read_text() published_date = None - current = json.loads(args.current.read_text()) + current_text = args.current.read_text() + current = json.loads(current_text) downloads = artifact_downloads(args.artifacts) result = merge_release(current, tag, name, notes, downloads, published_date) - args.output.write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n") - print(f"Updated {tag}: {', '.join(downloads)}") + if result == current: + args.output.write_text(current_text) + print(f"No changes to version.json for {tag}") + else: + args.output.write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n") + print(f"Updated version.json for {tag}") if __name__ == "__main__": diff --git a/docs/UPDATER.md b/docs/UPDATER.md index e2e1a669..992e81f1 100644 --- a/docs/UPDATER.md +++ b/docs/UPDATER.md @@ -59,6 +59,11 @@ GitHub Actions 从分支手动打包时,在 `Build and Release` 的 `v1.7.2-20261020`,再验证到 `v1.7.3-20261020` 的升级。 分支手动构建只上传 Actions 构建产物,不会自动发布版本清单。 +发布脚本按数字版本和 patch 比较版本,忽略构建日期。编辑或重新发布旧的 +桌面 Release 时,保留服务器现有清单并在 Actions 日志中说明跳过原因; +同版本仍可更新发布说明,新版本正常发布。各平台下载记录也不会被较低 +版本覆盖,包括独立移动端发布和桌面 Release 附带的移动端安装包。 + 本地构建可以设置同名环境变量,或使用: ```sh