aboutsummaryrefslogtreecommitdiff
path: root/scripts/changelog.py
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-08-07 14:40:14 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-08-07 14:40:14 +0300
commit322ab6a5a98db5c230b7c90881b8b4baf4cee624 (patch)
treea64ad1b46bc0ef0a0748ccad8409c5e7e97cb7ae /scripts/changelog.py
parent69a07fdf061a35276295189d52d591893c429806 (diff)
fix: verify stable release checksums
Diffstat (limited to 'scripts/changelog.py')
-rw-r--r--scripts/changelog.py46
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),