#!/usr/bin/env python3 """Fail when a change set mixes Terraform with deployable application files. Workflow, docs, and gate-script changes may travel with either side. """ from __future__ import annotations import argparse import sys APP_ROOTS = ( "src/", "public/", "pages/", "config/", ) APP_FILES = { "index.html", "vite.config.ts", "vitest.config.ts", } def is_terraform_path(path: str) -> bool: return path == "terraform" or path.startswith("terraform/") def is_app_path(path: str) -> bool: normalized = path.replace("\\", "/") if normalized in APP_FILES: return True if normalized in {"src", "public", "pages", "config"}: return True if normalized.startswith(APP_ROOTS): return True if normalized.startswith("tsconfig"): return True return normalized.startswith(".env") def isolation_violation(paths: list[str]) -> tuple[list[str], list[str]] | None: terraform_files = sorted({path for path in paths if is_terraform_path(path)}) app_files = sorted({path for path in paths if is_app_path(path)}) if terraform_files and app_files: return terraform_files, app_files return None def main() -> int: parser = argparse.ArgumentParser() parser.add_argument( "paths", nargs="*", help="Changed paths. Omit and pass newline-separated paths on stdin.", ) args = parser.parse_args() paths = list(args.paths) if not paths and not sys.stdin.isatty(): paths = [line.strip() for line in sys.stdin if line.strip()] violation = isolation_violation(paths) if violation is None: print("PASS: application and Terraform changes are isolated") return 0 terraform_files, app_files = violation print("FAIL: do not mix deployable application files with terraform/", file=sys.stderr) print("terraform:", file=sys.stderr) for path in terraform_files: print(f" {path}", file=sys.stderr) print("application:", file=sys.stderr) for path in app_files: print(f" {path}", file=sys.stderr) return 1 if __name__ == "__main__": raise SystemExit(main())