#!/usr/bin/env python3 from __future__ import annotations import re import sys from pathlib import Path ROOT = Path(__file__).resolve().parents[1] errors: list[str] = [] warnings: list[str] = [] def fail(msg: str) -> None: errors.append(msg) def warn(msg: str) -> None: warnings.append(msg) version_file = ROOT / "VERSION" version = version_file.read_text(encoding="utf-8").strip() if version_file.exists() else "" if not re.fullmatch(r"\d+\.\d+\.\d+", version): fail("VERSION missing or not semantic x.y.z") readme = (ROOT / "README.md").read_text(encoding="utf-8") if (ROOT / "README.md").exists() else "" if not readme.startswith("---\n"): fail("README.md must start with Hugging Face YAML front matter") else: end = readme.find("\n---", 4) if end < 0: fail("README.md YAML front matter is not closed") else: meta = readme[4:end] for required in ("title:", "sdk: docker", "app_port: 7860"): if required not in meta: fail(f"README.md metadata missing: {required}") app_html = ROOT / "app.html" if not app_html.exists(): fail("root app.html is missing") else: html = app_html.read_text(encoding="utf-8") if '__APP_VERSION__' not in html: fail("app.html must use __APP_VERSION__ placeholder") if 'data-view="projectPackage"' not in html: fail("project data package sidebar entry is missing") ui = ROOT / "ui_server.py" if not ui.exists(): fail("ui_server.py is missing") else: text = ui.read_text(encoding="utf-8") if 'with_name("VERSION")' not in text: fail("ui_server.py must load VERSION as the single version source") if 'with_name("app.html")' not in text: fail("ui_server.py must serve root app.html") if 'templates/app.html' in text: fail("ui_server.py still references templates/app.html") if (ROOT / "templates" / "app.html").exists(): fail("duplicate templates/app.html must not be shipped") for forbidden in (".pytest_cache", "__pycache__"): matches = [p for p in ROOT.rglob(forbidden) if p.is_dir()] if matches: fail(f"forbidden cache directory present: {matches[0].relative_to(ROOT)}") if list(ROOT.rglob("*.pyc")): fail("compiled .pyc files must not be shipped") for required in ( ROOT / "static" / "css" / "app.css", ROOT / "static" / "js" / "app.js", ROOT / "views" / "admin.py", ROOT / "services" / "project_planner.py", ROOT / "routes" / "data_health.py", ): if not required.exists(): fail(f"P0 architecture file missing: {required.relative_to(ROOT)}") for name in ("Dockerfile", "start.sh", "requirements.txt"): if not (ROOT / name).exists(): fail(f"deployment file missing: {name}") if version and f"Current UI release: **v{version}**" not in readme: warn("README current release line is not synchronized with VERSION") for msg in warnings: print(f"[preflight] WARNING: {msg}") if errors: for msg in errors: print(f"[preflight] ERROR: {msg}", file=sys.stderr) sys.exit(1) import shutil as _shutil, subprocess as _subprocess node_bin=_shutil.which("node") frontend_smoke=ROOT/"scripts"/"frontend_runtime_smoke.js" if node_bin and frontend_smoke.exists(): proc=_subprocess.run([node_bin,str(frontend_smoke)],cwd=ROOT,capture_output=True,text=True) if proc.returncode!=0: error("frontend runtime smoke failed: "+(proc.stderr.strip() or proc.stdout.strip())[:500]) print(f"[preflight] OK: v{version}; single template; clean deployment tree")