#!/usr/bin/env python3 """Plan and validate canary promotion gates.""" from __future__ import annotations import argparse import csv import subprocess from pathlib import Path REPO_ROOT = Path(__file__).resolve().parents[1] CHECKS_FILE = REPO_ROOT / "infra" / "promotion" / "checks.tsv" def checks() -> list[dict[str, str]]: with CHECKS_FILE.open(encoding="utf-8", newline="") as handle: return list(csv.DictReader(handle, delimiter="\t")) def plan() -> int: print("Promotion Plan") print("==============") print("dev branch -> validation -> main -> Argo CD sync") print() for row in checks(): marker = "required" if row["required"] == "true" else "optional-live" print(f"{marker:13} {row['stage']:18} {row['command']}") print() print("Prod promotion remains manual until dev namespace/app wiring is added.") return 0 def validate(local_only: bool) -> int: failures = 0 print("Promotion Validation") print("====================") for row in checks(): is_required = row["required"] == "true" if local_only and not is_required: print(f"skip {row['stage']} - live check gated") continue print(f"run {row['stage']} - {row['command']}") process = subprocess.run( row["command"].split(), cwd=REPO_ROOT, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, check=False, ) if process.returncode == 0: print("ok") else: failures += 1 print(process.stdout.rstrip()) print(f"failures={failures}") return 1 if failures else 0 def rollback(app: str) -> int: print("Rollback Plan") print("=============") print(f"app: {app}") print("1. identify last known good Git commit or image digest") print("2. git revert ") print("3. ./jeannie safety-case --since HEAD~1") print("4. ./jeannie validate") print("5. push to main and let Argo CD reconcile") print("6. ./jeannie synthetic-checks") return 0 def main() -> int: parser = argparse.ArgumentParser(description=__doc__) subparsers = parser.add_subparsers(dest="command", required=True) subparsers.add_parser("plan") validate_parser = subparsers.add_parser("validate") validate_parser.add_argument("--all", action="store_true", help="Include optional live checks.") rollback_parser = subparsers.add_parser("rollback") rollback_parser.add_argument("app") args = parser.parse_args() if args.command == "plan": return plan() if args.command == "validate": return validate(local_only=not args.all) if args.command == "rollback": return rollback(args.app) return 2 if __name__ == "__main__": raise SystemExit(main())