diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-08-07 14:40:14 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-08-07 14:40:14 +0300 |
| commit | 322ab6a5a98db5c230b7c90881b8b4baf4cee624 (patch) | |
| tree | a64ad1b46bc0ef0a0748ccad8409c5e7e97cb7ae /scripts/changelog.py | |
| parent | 69a07fdf061a35276295189d52d591893c429806 (diff) | |
fix: verify stable release checksums
Diffstat (limited to 'scripts/changelog.py')
| -rw-r--r-- | scripts/changelog.py | 46 |
1 files changed, 45 insertions, 1 deletions
diff --git a/scripts/changelog.py b/scripts/changelog.py index a5d2daf2..4d023ccc 100644 --- a/scripts/changelog.py +++ b/scripts/changelog.py @@ -186,11 +186,26 @@ def normalize_assets(value: Any, label: str) -> dict[str, str]: return normalized_assets +def normalize_sha256(value: Any, label: str) -> dict[str, str]: + if not isinstance(value, dict): + raise ChangelogError(f"{label} must be an object") + + checksums: dict[str, str] = {} + for target in ASSET_TARGETS: + checksum = value.get(target) + if not isinstance(checksum, str) or re.fullmatch( + r"[0-9a-fA-F]{64}", checksum.strip() + ) is None: + raise ChangelogError(f"{label} has invalid SHA-256 for {target}") + checksums[target] = checksum.strip().lower() + return checksums + + def normalize_release_metadata(value: Any, label: str, version: str) -> dict[str, Any]: if not isinstance(value, dict): raise ChangelogError(f"{label} must be an object") - allowed_keys = {"notes", "announcement", "assets", "protocol"} + allowed_keys = {"notes", "announcement", "assets", "sha256", "protocol"} extra_keys = sorted(set(value) - allowed_keys) if extra_keys: raise ChangelogError(f"{label} has unsupported field(s): {', '.join(extra_keys)}") @@ -213,6 +228,8 @@ def normalize_release_metadata(value: Any, label: str, version: str) -> dict[str metadata["assets"] = normalize_assets(value.get("assets"), f"{label}.assets") else: metadata["assets"] = default_release_assets(version) + if "sha256" in value: + metadata["sha256"] = normalize_sha256(value.get("sha256"), f"{label}.sha256") announcement = normalize_announcement(value.get("announcement"), label) if announcement is not None: metadata["announcement"] = announcement @@ -243,6 +260,7 @@ def build_latest_json( version: str, notes: str, assets: dict[str, str], + sha256: dict[str, str], protocol: int | None = None, announcement: dict[str, str] | None = None, releases: dict[str, Any] | None = None, @@ -256,6 +274,7 @@ def build_latest_json( protocol = read_protocol_version() ordered_assets = normalize_assets(assets, "assets") + ordered_sha256 = normalize_sha256(sha256, "sha256") normalized_announcement = normalize_announcement(announcement, "root") archived_releases = normalize_releases(releases) current_metadata: dict[str, Any] = { @@ -263,6 +282,7 @@ def build_latest_json( "protocol": protocol, "assets": ordered_assets, } + current_metadata["sha256"] = ordered_sha256 if normalized_announcement is not None: current_metadata["announcement"] = normalized_announcement archived_releases[normalized_version] = current_metadata @@ -277,6 +297,7 @@ def build_latest_json( "notes": normalized_notes, "assets": ordered_assets, } + manifest["sha256"] = ordered_sha256 if normalized_announcement is not None: manifest["announcement"] = normalized_announcement manifest["releases"] = archived_releases @@ -322,7 +343,14 @@ def manifest_from_release_payload( if isinstance(name, str) and name not in release_assets: release_assets[name] = asset + missing_assets = [name for name in EXPECTED_ASSET_NAMES.values() if name not in release_assets] + if missing_assets: + raise ChangelogError( + f"GitHub release v{normalized_version} is missing asset {missing_assets[0]}" + ) + manifest_assets: dict[str, str] = {} + manifest_sha256: dict[str, str] = {} for target, asset_name in EXPECTED_ASSET_NAMES.items(): asset = release_assets.get(asset_name) if not isinstance(asset, dict): @@ -330,13 +358,19 @@ def manifest_from_release_payload( url = str(asset.get("url") or "").strip() if not url: raise ChangelogError(f"GitHub release asset {asset_name} is missing a download URL") + digest = str(asset.get("digest") or "").strip() + digest_match = re.fullmatch(r"sha256:([0-9a-fA-F]{64})", digest) + if digest_match is None: + raise ChangelogError(f"GitHub release asset {asset_name} is missing a SHA-256 digest") manifest_assets[target] = url + manifest_sha256[target] = digest_match.group(1).lower() return { "version": normalized_version, "protocol": protocol if protocol is not None else read_protocol_version(), "notes": notes, "assets": manifest_assets, + "sha256": manifest_sha256, } @@ -358,12 +392,14 @@ def canonicalize_manifest(manifest: dict[str, Any], label: str) -> dict[str, Any raise ChangelogError(f"{label} is missing an assets object") normalized_assets = normalize_assets(assets, f"{label} assets") + normalized_sha256 = normalize_sha256(manifest.get("sha256"), f"{label} sha256") return { "version": normalize_version(version), "protocol": protocol, "notes": notes.strip(), "assets": normalized_assets, + "sha256": normalized_sha256, } @@ -391,6 +427,10 @@ def ensure_current_release_assets_are_mirrored(manifest: dict[str, Any], label: raise ChangelogError( f"{label} releases.{canonical['version']}.assets must match top-level assets" ) + if metadata.get("sha256") != canonical["sha256"]: + raise ChangelogError( + f"{label} releases.{canonical['version']}.sha256 must match top-level sha256" + ) def load_text(path: Path) -> str: @@ -431,6 +471,9 @@ def archived_releases_from_current_manifest(manifest: dict[str, Any]) -> dict[st metadata["assets"] = normalize_assets(assets, "current root assets") else: metadata["assets"] = default_release_assets(normalized_version) + sha256 = manifest.get("sha256") + if isinstance(sha256, dict): + metadata["sha256"] = normalize_sha256(sha256, "current root sha256") announcement = normalize_announcement(manifest.get("announcement"), "current root") if announcement is not None: metadata["announcement"] = announcement @@ -592,6 +635,7 @@ def cmd_sync_latest_json(args: argparse.Namespace) -> int: version, str(new_manifest["notes"]), dict(new_manifest["assets"]), + sha256=dict(new_manifest["sha256"]), protocol=int(new_manifest["protocol"]), announcement=announcement, releases=archived_releases_from_current_manifest(current_manifest), |
