#!/usr/bin/env python3 """Run local release gates for the Nexus snapshot. This is intentionally conservative and network-free after dependencies are installed: - Python syntax check for touched backend/test/release scripts. - UTF-8/control-character integrity check for release-facing text files. - /app-v2 TypeScript/Vite build. - /app-v2 build output sensitive information scan. - Optional BtPanel pytest suite when a Python 3.12/3.13 test env is active. """ from __future__ import annotations import argparse import os import shutil import subprocess import sys from pathlib import Path ROOT = Path(__file__).resolve().parents[1] FRONTEND_V2 = ROOT / "frontend-v2" PY_COMPILE_TARGETS = [ "server/main.py", "server/application/services/btpanel_service.py", "server/infrastructure/btpanel/bootstrap_lock.py", "server/infrastructure/btpanel/login_url_lock.py", "server/infrastructure/btpanel/ssh_bootstrap.py", "tests/test_btpanel_bootstrap_lock.py", "tests/test_btpanel_get_config.py", "tests/test_btpanel_login_url.py", "tests/test_btpanel_login_url_route.py", "tests/test_btpanel_ssh_bootstrap.py", "tests/test_publish_release_bundle.py", "scripts/check_backend_test_env.py", "scripts/check_deploy_excludes.py", "scripts/check_text_integrity.py", "scripts/generate_release_bundle.py", "scripts/generate_release_diff.py", "scripts/prepare_release_artifact.py", "scripts/publish_release_bundle.py", "scripts/run_btpanel_tests.py", "scripts/run_local_release_gate.py", "scripts/scan_release_bundle.py", "scripts/scan_release_manifest.py", "scripts/verify_online_release.py", "scripts/write_release_metadata.py", ] SHELL_CHECK_TARGETS = [ "deploy/apply-release-bundle.sh", "deploy/deploy-production.sh", "deploy/preflight-release-host.sh", "deploy/rsync-local-to-server.sh", "deploy/sync_webapp_to_container.sh", ] def npm_cmd() -> str: return "npm.cmd" if os.name == "nt" else "npm" def bash_cmd() -> str | None: found = shutil.which("bash") if found: return found git_bash = Path("C:/Program Files/Git/bin/bash.exe") if git_bash.exists(): return str(git_bash) return None def run(cmd: list[str], *, cwd: Path = ROOT, check: bool = True) -> int: print("\n$ " + " ".join(cmd), flush=True) proc = subprocess.run(cmd, cwd=cwd) if check and proc.returncode != 0: raise SystemExit(proc.returncode) return proc.returncode def existing(paths: list[str]) -> list[str]: return [p for p in paths if (ROOT / p).exists()] def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--with-pytest", action="store_true", help="Also run the BtPanel pytest suite after env check.") parser.add_argument("--skip-frontend", action="store_true", help="Skip /app-v2 build:safe.") args = parser.parse_args() targets = existing(PY_COMPILE_TARGETS) if not targets: print("ERROR: no py_compile targets found", file=sys.stderr) return 2 run([sys.executable, "-m", "py_compile", *targets]) shell_targets = existing(SHELL_CHECK_TARGETS) if shell_targets: bash = bash_cmd() if not bash: print("ERROR: bash not found; cannot syntax-check deploy shell scripts.", file=sys.stderr) return 2 for target in shell_targets: run([bash, "-n", target]) run([sys.executable, "scripts/check_deploy_excludes.py"]) run([sys.executable, "scripts/check_text_integrity.py"]) if not args.skip_frontend: if not FRONTEND_V2.exists(): print(f"ERROR: frontend-v2 directory not found: {FRONTEND_V2}", file=sys.stderr) return 2 run([npm_cmd(), "run", "build:safe"], cwd=FRONTEND_V2) if args.with_pytest: run([sys.executable, "scripts/run_btpanel_tests.py"]) run([sys.executable, "-m", "pytest", "tests/test_publish_release_bundle.py"]) else: print("\n[release-gate] pytest skipped. Use --with-pytest under Python 3.12/3.13 to run BtPanel tests.") print("\n[release-gate] OK") return 0 if __name__ == "__main__": raise SystemExit(main())