81 lines
2.8 KiB
Python
81 lines
2.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Fail closed when PostgreSQL migration authority escapes its canonical module."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
MAX_FILE_BYTES = 2 * 1024 * 1024
|
|
DDL = re.compile(
|
|
r"(?is)\b(?:create|alter|drop|truncate|comment\s+on|grant|revoke)\s+"
|
|
r"(?:(?:or\s+replace|unique|temporary|temp|unlogged)\s+)*"
|
|
r"(?:table|index|schema|view|materialized\s+view|type|sequence|function|procedure|"
|
|
r"trigger|extension|domain|policy|role)\b"
|
|
)
|
|
RUNNER = re.compile(
|
|
r"(?is)pg_advisory_(?:xact_)?lock|sqlx\s*::\s*migrate|_sqlx_migrations|"
|
|
r"include_(?:str|bytes)!\s*\([^)]*\.sql|"
|
|
r"(?:insert\s+into|update|delete\s+from)\s+__crank_(?:core_|mcp_|ext_)?migrations"
|
|
)
|
|
|
|
|
|
def canonical(path: Path, root: Path) -> bool:
|
|
relative = path.relative_to(root).as_posix()
|
|
return relative == "crates/crank-registry/src/migrations.rs" or relative.startswith(
|
|
"crates/crank-registry/src/migrations/"
|
|
)
|
|
|
|
|
|
def candidates(root: Path) -> list[Path]:
|
|
result: list[Path] = []
|
|
for base_name in ("apps", "crates"):
|
|
base = root / base_name
|
|
if not base.exists():
|
|
continue
|
|
for path in base.rglob("*"):
|
|
if path.suffix not in {".rs", ".sql"}:
|
|
continue
|
|
if "crank-test-support" in path.relative_to(root).parts:
|
|
continue
|
|
if path.is_symlink() or not path.is_file():
|
|
raise ValueError("production Rust/SQL path must be a regular non-symlink file")
|
|
if "/tests/" in f"/{path.relative_to(root).as_posix()}/":
|
|
continue
|
|
result.append(path)
|
|
return sorted(result)
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--root", type=Path, default=Path("."))
|
|
args = parser.parse_args()
|
|
root = args.root.resolve()
|
|
try:
|
|
files = candidates(root)
|
|
violations: list[str] = []
|
|
for path in files:
|
|
if canonical(path, root):
|
|
continue
|
|
data = path.read_bytes()
|
|
if len(data) > MAX_FILE_BYTES:
|
|
raise ValueError("production Rust/SQL file exceeds scanner limit")
|
|
text = data.decode("utf-8")
|
|
if DDL.search(text) or RUNNER.search(text):
|
|
violations.append(path.relative_to(root).as_posix())
|
|
except (OSError, UnicodeDecodeError, ValueError) as error:
|
|
print(f"migration boundary check failed: {error}", file=sys.stderr)
|
|
return 1
|
|
if violations:
|
|
for path in violations:
|
|
print(f"migration boundary violation: {path}", file=sys.stderr)
|
|
return 1
|
|
print(f"migration boundary check passed: files={len(files)}")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|