""" Pre-submission validator for Bug Triage OpenEnv. Checks: 1) HF Space /reset returns HTTP 200 2) docker build succeeds 3) openenv validate succeeds """ from __future__ import annotations import argparse import os import re import shutil import subprocess import sys import urllib.error import urllib.request from pathlib import Path def log(msg: str) -> None: print(msg) def normalize_space_url(space_url: str) -> str: """Accept either hf.space or huggingface.co/spaces URLs.""" value = space_url.strip().rstrip("/") match = re.match(r"^https?://huggingface\.co/spaces/([^/]+)/([^/]+)$", value) if match: owner, space = match.groups() slug = f"{owner}-{space}".replace("_", "-") return f"https://{slug}.hf.space" return value def run(cmd: list[str], cwd: Path | None = None, timeout: int = 1800) -> tuple[int, str, str]: proc = subprocess.run( cmd, cwd=str(cwd) if cwd else None, capture_output=True, text=True, timeout=timeout, ) return proc.returncode, proc.stdout.strip(), proc.stderr.strip() def check_space(space_url: str, timeout_seconds: int) -> tuple[bool, str]: normalized_url = normalize_space_url(space_url) reset_url = f"{normalized_url.rstrip('/')}/reset" req = urllib.request.Request(reset_url, method="GET") try: with urllib.request.urlopen(req, timeout=timeout_seconds) as resp: code = int(resp.getcode()) if code == 200: return True, f"HF Space /reset returned HTTP {code} ({normalized_url})" return False, f"HF Space /reset returned HTTP {code} (expected 200) ({normalized_url})" except urllib.error.HTTPError as err: return False, f"HF Space /reset returned HTTP {err.code} (expected 200) ({normalized_url})" except Exception as err: return False, f"HF Space check failed: {err} ({normalized_url})" def check_docker_build(repo_dir: Path, docker_timeout: int) -> tuple[bool, str]: if shutil.which("docker") is None: return False, "docker command not found" if (repo_dir / "Dockerfile").exists(): context = repo_dir elif (repo_dir / "server" / "Dockerfile").exists(): context = repo_dir / "server" else: return False, "No Dockerfile found in repo root or server/" code, out, err = run(["docker", "build", str(context)], timeout=docker_timeout) if code == 0: return True, f"Docker build succeeded ({context})" combined = (out + "\n" + err).strip() combined_lower = combined.lower() if ( "failed to connect to the docker api" in combined_lower or "dockerdesktoplinuxengine" in combined_lower or ("access is denied" in combined_lower and ".docker\\buildx\\instances" in combined_lower) ): return False, "Docker daemon is not running or is not accessible to the current user" tail = "\n".join(combined.splitlines()[-20:]) return False, f"Docker build failed (timeout={docker_timeout}s)\n{tail}" def check_openenv_validate(repo_dir: Path) -> tuple[bool, str]: local_cli_candidates = [ repo_dir / "venv" / "Scripts" / "openenv.exe", repo_dir / "venv" / "bin" / "openenv", ] cmd = None if shutil.which("openenv") is not None: cmd = ["openenv", "validate"] else: for candidate in local_cli_candidates: if candidate.exists(): cmd = [str(candidate), "validate"] break if cmd is None: # Fallback for environments where console scripts are not on PATH. cmd = [sys.executable, "-m", "openenv.cli", "validate"] code, out, err = run(cmd, cwd=repo_dir, timeout=600) if code == 0: msg = out or "openenv validate passed" return True, msg base = "openenv validate failed" hint = "Install with: pip install openenv-core" if "No module named" in err else "" details = "\n".join(x for x in [base, hint, out, err] if x).strip() return False, details def main() -> int: parser = argparse.ArgumentParser(description="Pre-submit validator") parser.add_argument( "--repo-dir", default=".", help="Repository root (default: current directory)", ) parser.add_argument( "--space-url", default=os.getenv("HF_SPACE_URL", ""), help="HF Space base URL (or set HF_SPACE_URL)", ) parser.add_argument( "--skip-space", action="store_true", help="Skip HF Space /reset check", ) parser.add_argument( "--docker-timeout", type=int, default=1800, help="Docker build timeout in seconds (default: 1800)", ) parser.add_argument( "--http-timeout", type=int, default=20, help="HTTP timeout for HF Space check in seconds", ) args = parser.parse_args() repo_dir = Path(args.repo_dir).resolve() if not repo_dir.exists(): log(f"FAIL: repo dir does not exist: {repo_dir}") return 1 log("========================================") log("Pre-submission Validation") log("========================================") all_ok = True # Step 1: HF Space /reset log("Step 1/3: Checking HF Space /reset") if args.skip_space: log("SKIP: HF Space check skipped") else: if not args.space_url: log("FAIL: --space-url (or HF_SPACE_URL) is required unless --skip-space is used") all_ok = False else: ok, msg = check_space(args.space_url, args.http_timeout) log(("PASS: " if ok else "FAIL: ") + msg) all_ok = all_ok and ok # Step 2: docker build log("Step 2/3: Running docker build") ok, msg = check_docker_build(repo_dir, args.docker_timeout) log(("PASS: " if ok else "FAIL: ") + msg) all_ok = all_ok and ok # Step 3: openenv validate log("Step 3/3: Running openenv validate") ok, msg = check_openenv_validate(repo_dir) log(("PASS: " if ok else "FAIL: ") + msg) all_ok = all_ok and ok log("========================================") if all_ok: log("All checks passed") return 0 log("One or more checks failed") return 1 if __name__ == "__main__": raise SystemExit(main())