#!/usr/bin/env python3 """Build a reproducible release bundle from an exact Git commit.""" from __future__ import annotations import argparse import gzip import hashlib import io import json import re import subprocess import tarfile from dataclasses import dataclass from pathlib import Path, PurePosixPath RUNTIME_DIRECTORIES = ("docs", "frontend", "src") RUNTIME_FILES = ( "README.md", "requirements.txt", "scripts/rotate_private_state.py", "scripts/rotate_unfiled_drafts.py", ) EXCLUDED_PARTS = {"__pycache__", ".pytest_cache"} EXCLUDED_SUFFIXES = (".pyc", ".pyo") COMMIT_PATTERN = re.compile(r"[0-9a-f]{40}") @dataclass(frozen=True) class RuntimeFile: name: str data: bytes executable: bool def sha256(data: bytes) -> str: return hashlib.sha256(data).hexdigest() def _git(root: Path, *args: str, text: bool = False) -> subprocess.CompletedProcess: return subprocess.run( ["git", "-C", str(root), *args], capture_output=True, check=False, text=text, ) def runtime_files_at_commit(root: Path, commit: str) -> list[RuntimeFile]: """Read the runtime allowlist directly from the declared Git tree.""" root = root.resolve() if not COMMIT_PATTERN.fullmatch(commit): raise ValueError("declared commit must be a full 40-character Git object ID") resolved = _git(root, "rev-parse", "--verify", f"{commit}^{{commit}}", text=True) if resolved.returncode != 0 or resolved.stdout.strip() != commit: raise ValueError("declared commit is unavailable") listing = _git(root, "ls-tree", "-rz", "--full-tree", commit) if listing.returncode != 0: raise ValueError("could not inspect the declared commit") files: list[RuntimeFile] = [] for record in listing.stdout.split(b"\0"): if not record: continue metadata, raw_name = record.split(b"\t", 1) mode, object_type, object_id = metadata.decode("ascii").split() name = raw_name.decode("utf-8") path = PurePosixPath(name) selected = name in RUNTIME_FILES or (path.parts and path.parts[0] in RUNTIME_DIRECTORIES) if not selected or object_type != "blob": continue if mode not in {"100644", "100755"}: raise ValueError(f"release member is not a regular file: {name}") if EXCLUDED_PARTS.intersection(path.parts) or name.endswith(EXCLUDED_SUFFIXES): raise ValueError(f"declared commit contains generated release member: {name}") blob = _git(root, "cat-file", "blob", object_id) if blob.returncode != 0: raise ValueError(f"could not read release member: {name}") files.append(RuntimeFile(name, blob.stdout, mode == "100755")) names = {item.name for item in files} missing = [name for name in RUNTIME_FILES if name not in names] if missing or not any(name.startswith("src/") for name in names) or not any( name.startswith("frontend/") for name in names ): raise ValueError("declared commit is missing required runtime files") return sorted(files, key=lambda item: item.name) def manifest_for(files: list[RuntimeFile], commit: str) -> dict: return { "schema_version": 1, "commit": commit, "files": { item.name: {"sha256": sha256(item.data), "size": len(item.data)} for item in files }, } def json_bytes(payload: dict) -> bytes: return (json.dumps(payload, indent=2, sort_keys=True) + "\n").encode() def tar_info(name: str, size: int, epoch: int, executable: bool = False) -> tarfile.TarInfo: info = tarfile.TarInfo(name) info.size = size info.mtime = epoch info.mode = 0o755 if executable else 0o644 info.uid = info.gid = 0 info.uname = info.gname = "root" return info def build(root: Path, output_dir: Path, commit: str, epoch: int) -> tuple[Path, Path, Path]: files = runtime_files_at_commit(root, commit) embedded_manifest = manifest_for(files, commit) archive_members: dict[str, tuple[bytes, bool]] = { item.name: (item.data, item.executable) for item in files } archive_members["release-manifest.json"] = (json_bytes(embedded_manifest), False) output_dir.mkdir(parents=True, exist_ok=True) stem = f"stackchain-dashboard-{commit[:12]}" archive_path = output_dir / f"{stem}.tar.gz" with archive_path.open("wb") as raw: with gzip.GzipFile(fileobj=raw, mode="wb", filename="", mtime=epoch) as compressed: with tarfile.open(fileobj=compressed, mode="w", format=tarfile.PAX_FORMAT) as archive: for name in sorted(archive_members): data, executable = archive_members[name] archive.addfile(tar_info(name, len(data), epoch, executable), io.BytesIO(data)) archive_data = archive_path.read_bytes() digest = sha256(archive_data) public_manifest = { **embedded_manifest, "artifact": { "name": archive_path.name, "sha256": digest, "size": len(archive_data), }, } manifest_path = output_dir / f"{stem}.manifest.json" checksum_path = output_dir / f"{stem}.sha256" manifest_path.write_bytes(json_bytes(public_manifest)) checksum_path.write_text(f"{digest} {archive_path.name}\n") return archive_path, manifest_path, checksum_path def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--root", type=Path, default=Path.cwd()) parser.add_argument("--output-dir", type=Path, default=Path("dist")) parser.add_argument("--commit", required=True) parser.add_argument("--source-date-epoch", type=int, required=True) return parser.parse_args() def main() -> None: args = parse_args() try: paths = build(args.root, args.output_dir, args.commit, args.source_date_epoch) except (OSError, ValueError) as error: raise SystemExit(f"release build failed: {error}") from error for path in paths: print(path) if __name__ == "__main__": main()