#!/usr/bin/env python3 """Verify production sibling sources against deploy/runtime-contract-lock.json.""" from __future__ import annotations import json import re import subprocess import sys import tomllib from pathlib import Path from typing import Any def _git(path: Path, *args: str) -> str: return subprocess.check_output( ["git", "-C", str(path), *args], text=True, stderr=subprocess.DEVNULL, timeout=10, ).strip() def verify(lock_path: Path) -> dict[str, Any]: lock_path = lock_path.resolve() repo_root = lock_path.parent.parent payload = json.loads(lock_path.read_text(encoding="utf-8")) if set(payload) != {"schema_version", "contracts", "dependencies"}: raise ValueError("runtime lock has invalid top-level fields") if payload.get("schema_version") != "1": raise ValueError("unsupported runtime lock schema") contracts = payload.get("contracts") if not isinstance(contracts, dict) or set(contracts) != { "activity_core_contract_commit", "activity_core_schema", "glas_contract_version", }: raise ValueError("runtime lock has invalid contract fields") activity_commit = str(contracts["activity_core_contract_commit"]) if re.fullmatch(r"[0-9a-f]{40}", activity_commit) is None: raise ValueError("Activity Core contract commit must be a full Git object id") activity_schema = str(contracts["activity_core_schema"]) if re.fullmatch(r"[0-9]{4}", activity_schema) is None: raise ValueError("Activity Core schema must be a four-digit migration id") glas_contract = str(contracts["glas_contract_version"]) if re.fullmatch(r"[0-9]+\.[0-9]+", glas_contract) is None: raise ValueError("Glas contract version must pin major.minor") dependencies = payload.get("dependencies") if not isinstance(dependencies, list) or not dependencies: raise ValueError("runtime lock has no dependencies") results: list[dict[str, Any]] = [] for entry in dependencies: if not isinstance(entry, dict) or set(entry) != { "commit", "distribution", "source", "version", }: raise ValueError("runtime dependency entry has invalid fields") source = (repo_root / str(entry["source"])).resolve() if not source.is_dir(): raise ValueError(f"missing source checkout for {entry['distribution']}") head = _git(source, "rev-parse", "HEAD") if head != entry["commit"]: raise ValueError( f"{entry['distribution']} revision mismatch: " f"expected {entry['commit']} actual {head}" ) if _git(source, "status", "--porcelain"): raise ValueError(f"{entry['distribution']} source checkout is dirty") project = tomllib.loads( (source / "pyproject.toml").read_text(encoding="utf-8") ).get("project", {}) if project.get("name") != entry["distribution"]: raise ValueError(f"{entry['distribution']} package name mismatch") if project.get("version") != entry["version"]: raise ValueError(f"{entry['distribution']} package version mismatch") results.append( { "distribution": entry["distribution"], "version": entry["version"], "commit": head, "clean": True, } ) return {"ok": True, "schema_version": "1", "dependencies": results} def main(argv: list[str] | None = None) -> int: args = list(sys.argv[1:] if argv is None else argv) if len(args) > 1: print("usage: verify_runtime_lock.py [lock-file]", file=sys.stderr) return 2 default = Path(__file__).resolve().parents[1] / "deploy/runtime-contract-lock.json" lock_path = Path(args[0]) if args else default try: report = verify(lock_path) except (OSError, ValueError, subprocess.SubprocessError, json.JSONDecodeError) as exc: print( json.dumps( { "ok": False, "error": f"runtime lock verification failed ({type(exc).__name__})", "detail": str(exc)[:500], }, sort_keys=True, ) ) return 1 print(json.dumps(report, indent=2, sort_keys=True)) return 0 if __name__ == "__main__": raise SystemExit(main())