mirror of
https://github.com/Sea-Haven-Industries/.github.git
synced 2026-10-07 13:58:55 +00:00
96 lines
3 KiB
Python
96 lines
3 KiB
Python
#!/usr/bin/env python3
|
|
"""Fail when a change set mixes Terraform with deployable application files.
|
|
|
|
APP_PATHS is newline-separated. A trailing slash is a directory prefix.
|
|
Any other entry is an exact file. Paths that are not listed are neutral, so
|
|
workflows, docs, and tests may travel with either side. An empty APP_PATHS
|
|
skips the check.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import os
|
|
import sys
|
|
|
|
|
|
def parse_app_rules(raw: str) -> tuple[frozenset[str], frozenset[str]]:
|
|
prefixes: set[str] = set()
|
|
exact: set[str] = set()
|
|
for line in raw.splitlines():
|
|
item = line.strip().replace("\\", "/")
|
|
if not item or item.startswith("#"):
|
|
continue
|
|
if item.endswith("/"):
|
|
prefixes.add(item)
|
|
else:
|
|
exact.add(item)
|
|
return frozenset(prefixes), frozenset(exact)
|
|
|
|
|
|
def is_terraform_path(path: str) -> bool:
|
|
normalized = path.replace("\\", "/")
|
|
return normalized == "terraform" or normalized.startswith("terraform/")
|
|
|
|
|
|
def is_app_path(path: str, prefixes: frozenset[str], exact: frozenset[str]) -> bool:
|
|
normalized = path.replace("\\", "/")
|
|
if normalized in exact:
|
|
return True
|
|
for prefix in prefixes:
|
|
if normalized.startswith(prefix) or f"{normalized}/" == prefix:
|
|
return True
|
|
return False
|
|
|
|
|
|
def isolation_violation(
|
|
paths: list[str], app_paths: str
|
|
) -> tuple[list[str], list[str]] | None:
|
|
prefixes, exact = parse_app_rules(app_paths)
|
|
if not prefixes and not exact:
|
|
return 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, prefixes, exact)})
|
|
if terraform_files and app_files:
|
|
return terraform_files, app_files
|
|
return None
|
|
|
|
|
|
def first_isolation_violation(
|
|
file_sets: list[list[str]], app_paths: str
|
|
) -> tuple[list[str], list[str]] | None:
|
|
for paths in file_sets:
|
|
violation = isolation_violation(paths, app_paths)
|
|
if violation is not None:
|
|
return violation
|
|
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, os.environ.get("APP_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())
|