stackchain-dashboard/scripts/verify_release.py
timmy 2082fbd5eb
All checks were successful
CI / lint (pull_request) Successful in 2m38s
CI / build-release (pull_request) Successful in 6s
CI / browser-journey (pull_request) Successful in 2m24s
CI / release-candidate (pull_request) Has been skipped
ci: build releases from declared Git commits (Closes #1044)
2026-08-17 22:25:38 +00:00

101 lines
4.1 KiB
Python

#!/usr/bin/env python3
"""Verify a Stackchain Dashboard release bundle before promotion."""
from __future__ import annotations
import argparse
import hashlib
import json
import sys
import tarfile
from pathlib import Path, PurePosixPath
from build_release import manifest_for, runtime_files_at_commit
def one(directory: Path, pattern: str) -> Path:
matches = sorted(directory.glob(pattern))
if len(matches) != 1:
raise ValueError(f"expected exactly one {pattern} file, found {len(matches)}")
return matches[0]
def digest(data: bytes) -> str:
return hashlib.sha256(data).hexdigest()
def verify(input_dir: Path, commit: str, repository: Path) -> None:
archive_path = one(input_dir, "*.tar.gz")
manifest_path = one(input_dir, "*.manifest.json")
checksum_path = one(input_dir, "*.sha256")
manifest = json.loads(manifest_path.read_text())
if manifest.get("commit") != commit:
raise ValueError("manifest commit does not match the tested commit")
artifact = manifest.get("artifact", {})
archive_data = archive_path.read_bytes()
archive_digest = digest(archive_data)
if artifact.get("name") != archive_path.name:
raise ValueError("manifest artifact name does not match the bundle")
if artifact.get("size") != len(archive_data) or artifact.get("sha256") != archive_digest:
raise ValueError("bundle does not match its manifest")
checksum_parts = checksum_path.read_text().strip().split()
if checksum_parts != [archive_digest, archive_path.name]:
raise ValueError("checksum file does not match the bundle")
expected_files = manifest.get("files")
if not isinstance(expected_files, dict):
raise ValueError("manifest files must be an object")
source_files = runtime_files_at_commit(repository, commit)
source_manifest = manifest_for(source_files, commit)["files"]
if expected_files != source_manifest:
raise ValueError("bundle manifest does not match the declared Git commit")
source_by_name = {item.name: item for item in source_files}
with tarfile.open(archive_path, "r:gz") as archive:
members = archive.getmembers()
for member in members:
path = PurePosixPath(member.name)
if path.is_absolute() or ".." in path.parts or not member.isfile():
raise ValueError("bundle contains an unsafe member")
names = {member.name for member in members}
if names != set(expected_files) | {"release-manifest.json"}:
raise ValueError("bundle contents do not match the manifest")
embedded_file = archive.extractfile("release-manifest.json")
if embedded_file is None:
raise ValueError("bundle manifest is missing")
embedded = json.load(embedded_file)
if embedded.get("commit") != commit or embedded.get("files") != expected_files:
raise ValueError("embedded manifest does not match the public manifest")
for name, metadata in expected_files.items():
bundled = archive.extractfile(name)
if bundled is None:
raise ValueError(f"bundle member is missing: {name}")
data = bundled.read()
if metadata != {"sha256": digest(data), "size": len(data)}:
raise ValueError(f"bundle member failed verification: {name}")
if data != source_by_name[name].data:
raise ValueError(f"bundle member differs from the declared Git commit: {name}")
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--input-dir", type=Path, required=True)
parser.add_argument("--commit", required=True)
parser.add_argument("--repository", type=Path, default=Path.cwd())
return parser.parse_args()
def main() -> None:
args = parse_args()
try:
verify(args.input_dir, args.commit, args.repository)
except (OSError, ValueError, json.JSONDecodeError, tarfile.TarError) as error:
print(f"release verification failed: {error}", file=sys.stderr)
raise SystemExit(1) from error
print(f"verified release bundle for {args.commit}")
if __name__ == "__main__":
main()