File size: 6,336 Bytes
cfe0896
 
 
 
 
 
 
 
 
 
 
 
 
c5dbf9f
cfe0896
 
 
 
 
 
 
 
 
 
 
 
c5dbf9f
 
 
 
 
 
 
 
 
 
 
cfe0896
 
 
 
 
 
 
 
 
 
 
 
c5dbf9f
 
cfe0896
 
 
 
 
c5dbf9f
 
cfe0896
c5dbf9f
cfe0896
c5dbf9f
cfe0896
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
711b9b5
 
 
 
 
 
 
 
 
 
cfe0896
 
 
 
711b9b5
 
 
 
 
cfe0896
 
 
 
711b9b5
 
 
 
 
 
cfe0896
711b9b5
cfe0896
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
"""
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())