Files
archy/scripts/check_production_dashboard_boundary.py
Dorian 8366a4dbc8 Bind production dashboard build to non-demo profile
(cherry picked from commit 1320db90b65afd3e79543ca6c515a21c800a3fde)
2026-11-12 14:27:04 -05:00

66 lines
3.3 KiB
Python
Executable File

#!/usr/bin/env python3
"""Fail a dashboard release if demo servers can enter its production bundle."""
import argparse
import hashlib
import json
import os
import re
from pathlib import Path
MOCK_SERVER_NAMES = {"mock-backend.js", "content-payments.js", "films.js"}
IMPORT = re.compile(r"(?:\bfrom\s*|\bimport\s*(?:\(\s*)?|\brequire\s*\(\s*)['\"]([^'\"]+)['\"]")
def check(root: Path, dist: Path, builder: str, source_commit: str) -> dict:
if os.environ.get("VITE_DEMO") != "0" or os.environ.get("VITE_DEMO_CONTENT") in {"1", "true"}:
raise ValueError("production dashboard requires VITE_DEMO=0 and demo content disabled")
if builder != "native-vite-production":
raise ValueError("release builder must be native-vite-production, never a demo Dockerfile")
if not re.fullmatch(r"[0-9a-f]{40}", source_commit):
raise ValueError("exact source commit is required")
source = root / "neode-ui" / "src"
for path in source.rglob("*"):
if not path.is_file() or path.suffix not in {".ts", ".tsx", ".js", ".vue"}:
continue
if "__tests__" in path.parts or path.name.endswith((".test.ts", ".spec.ts")):
continue
for specifier in IMPORT.findall(path.read_text(encoding="utf-8")):
if "/mock/" in f"/{specifier}/" or any(name in specifier for name in MOCK_SERVER_NAMES):
raise ValueError(f"production source imports a demo mock: {path.relative_to(root)}")
if not (dist / "index.html").is_file():
raise ValueError("built dashboard index.html is missing")
profile_path = dist / "ARCHY-BUILD-PROFILE.json"
try:
profile = json.loads(profile_path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as exc:
raise ValueError("build-time profile is missing or invalid") from exc
if profile != {"schema": 1, "builder": "vite-dashboard", "demo": False, "source_revision": source_commit}:
raise ValueError("build-time profile does not bind production mode and exact source")
entries = []
for path in sorted(dist.rglob("*")):
if path.is_symlink():
raise ValueError(f"symlink in production static tree: {path.relative_to(dist)}")
if not path.is_file():
continue
relative = path.relative_to(dist).as_posix()
if Path(relative).name in MOCK_SERVER_NAMES or relative.startswith(("mock/", "demo/")):
raise ValueError(f"demo mock/server asset in production static tree: {relative}")
digest = hashlib.sha256(path.read_bytes()).hexdigest()
entries.append((relative, path.stat().st_size, digest))
tree = hashlib.sha256()
for relative, size, digest in entries:
tree.update(f"{relative}\0{size}\0{digest}\n".encode())
return {"builder": builder, "vite_demo": "0", "source_commit": source_commit, "files": len(entries), "tree_sha256": tree.hexdigest()}
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--repo-root", type=Path, required=True)
parser.add_argument("--dist", type=Path, required=True)
parser.add_argument("--builder", required=True)
parser.add_argument("--source-commit", required=True)
args = parser.parse_args()
print(json.dumps(check(args.repo_root.resolve(), args.dist.resolve(), args.builder, args.source_commit), sort_keys=True))