#!/usr/bin/env python3 """Drift guard: the site's generated catalog MUST match the API source. Run in deploy.sh before the site build. Re-parses the API catalog and compares it to the committed site/src/lib/service-catalog.generated.ts. Exits 1 on any difference, so a price edited in the API but not regenerated (or a hand-edited generated file) is caught before it reaches customers. Usage: python3 scripts/check-service-catalog-drift.py """ import re import sys from pathlib import Path ROOT = Path(__file__).resolve().parent.parent sys.path.insert(0, str(ROOT / "scripts")) import importlib.util spec = importlib.util.spec_from_file_location("gen_catalog", ROOT / "scripts/gen-service-catalog.py") gen = importlib.util.module_from_spec(spec) spec.loader.exec_module(gen) GEN = ROOT / "site/src/lib/service-catalog.generated.ts" def parse_generated(ts: str) -> dict: m = re.search(r"export const SERVICE_META[^=]*=\s*\{(.*)\n\};", ts, re.S) if not m: raise SystemExit("drift-check: cannot parse generated SERVICE_META") body = m.group(1) out = {} for em in re.finditer(r'"([a-z0-9\-]+)":\s*\{(.*?)\},', body): slug = em.group(1) inner = em.group(2) name_m = re.search(r'name:\s*"((?:[^"\\]|\\.)*)"', inner) price_m = re.search(r"price_cents:\s*(\d+)", inner) gov_m = re.search(r'gov_fee_label:\s*"((?:[^"\\]|\\.)*)"', inner) interval_m = re.search(r'billing_interval:\s*"(month|year)"', inner) methods_m = re.search(r"allowed_methods:\s*\[([^\]]*)\]", inner) entry = {"name": gen._unescape(name_m.group(1)), "price_cents": int(price_m.group(1))} if gov_m: entry["gov_fee_label"] = gen._unescape(gov_m.group(1)) if interval_m: entry["billing_interval"] = interval_m.group(1) if methods_m: entry["allowed_methods"] = re.findall(r'"([a-z]+)"', methods_m.group(1)) out[slug] = entry return out def main() -> int: api = gen.parse_catalog(gen.SRC.read_text()) have = parse_generated(GEN.read_text()) problems = [] for slug, a in api.items(): g = have.get(slug) if not g: problems.append(f"{slug}: missing from generated file") continue if a["price_cents"] != g["price_cents"]: problems.append(f"{slug}: price API={a['price_cents']} generated={g['price_cents']}") if a["name"] != g["name"]: problems.append(f"{slug}: name mismatch") if a.get("gov_fee_label") != g.get("gov_fee_label"): problems.append(f"{slug}: gov_fee_label mismatch") if a.get("billing_interval") != g.get("billing_interval"): problems.append(f"{slug}: billing_interval API={a.get('billing_interval')} generated={g.get('billing_interval')}") if a.get("allowed_methods") != g.get("allowed_methods"): problems.append(f"{slug}: allowed_methods API={a.get('allowed_methods')} generated={g.get('allowed_methods')}") for slug in have: if slug not in api: problems.append(f"{slug}: in generated file but not in API") if problems: print("SERVICE CATALOG DRIFT DETECTED (run: python3 scripts/gen-service-catalog.py):", file=sys.stderr) for p in problems: print(" - " + p, file=sys.stderr) return 1 print(f"drift-check: OK -- {len(api)} services, API and generated catalog match.") return 0 if __name__ == "__main__": sys.exit(main())