#!/usr/bin/env python3 """ Fail-fast validator for release manifests and downloads manifest. Checks presence of required components and expected fields so release pipelines can surface missing artefacts early (instead of blocking deploy tasks later). """ from __future__ import annotations import json import sys from pathlib import Path import yaml REQUIRED_COMPONENTS = [ "orchestrator", "policy-registry", "vex-lens", "issuer-directory", "findings-ledger", "vuln-explorer-api", "packs-registry", "task-runner", "web-ui", ] def load_yaml(path: Path): try: return yaml.safe_load(path.read_text()) except Exception as exc: raise SystemExit(f"ERROR: failed to parse {path}: {exc}") def check_manifest(manifest_path: Path) -> list[str]: data = load_yaml(manifest_path) comps = {c.get("name") for c in data.get("release", {}).get("components", [])} missing = [c for c in REQUIRED_COMPONENTS if c not in comps] return missing def check_downloads(downloads_path: Path) -> list[str]: missing = [] try: data = json.loads(downloads_path.read_text()) except Exception as exc: return [f"{downloads_path}: invalid JSON ({exc})"] items = data.get("items", []) if not items: missing.append(f"{downloads_path}: no items found") for idx, item in enumerate(items): for field in ("name", "type"): if field not in item: missing.append(f"{downloads_path}: item {idx} missing '{field}'") if item.get("type") == "container" and "image" not in item: missing.append(f"{downloads_path}: item {idx} missing 'image'") if item.get("type") == "archive" and "sha256" not in item: missing.append(f"{downloads_path}: item {idx} missing 'sha256'") return missing def main(): manifest = Path("deploy/releases/2025.09-stable.yaml") airgap = Path("deploy/releases/2025.09-airgap.yaml") downloads = Path("deploy/downloads/manifest.json") errors: list[str] = [] for path in (manifest, airgap): if not path.exists(): errors.append(f"{path}: file missing") continue missing = check_manifest(path) if missing: errors.append(f"{path}: missing components -> {', '.join(missing)}") if downloads.exists(): errors.extend(check_downloads(downloads)) else: errors.append(f"{downloads}: file missing") if errors: print("FAIL\n" + "\n".join(f"- {e}" for e in errors)) sys.exit(1) print("OK: required components present and downloads manifest is well-formed.") if __name__ == "__main__": main()