66 lines
3.3 KiB
Python
Executable File
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))
|