Files
Nexus/scripts/run_local_release_gate.py
T

129 lines
4.1 KiB
Python
Raw Normal View History

#!/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())