| from __future__ import annotations
|
|
|
| import argparse
|
| import json
|
| import shutil
|
| import subprocess
|
| import sys
|
| from dataclasses import dataclass
|
| from pathlib import Path
|
| from typing import Sequence
|
|
|
| ROOT_DIR = Path(__file__).resolve().parent.parent
|
| if str(ROOT_DIR) not in sys.path:
|
| sys.path.insert(0, str(ROOT_DIR))
|
|
|
| from scripts.verify_site import verify_site
|
|
|
|
|
| DEFAULT_SITE_URL = "https://arabic-translator-mu.vercel.app"
|
| DIRECT_CLOUD_ENV_KEYS = ["ENABLE_DIRECT_CLOUD_TTS", "HF_API_TOKEN", "HF_TTS_MODEL", "DEFAULT_VOICE_ID"]
|
| VERCEL_COMMAND = shutil.which("vercel") or shutil.which("vercel.cmd") or "vercel"
|
|
|
|
|
| @dataclass
|
| class CommandResult:
|
| command: list[str]
|
| returncode: int
|
| stdout: str
|
| stderr: str
|
|
|
|
|
| def normalize_url(value: str) -> str:
|
| return value.rstrip("/")
|
|
|
|
|
| def looks_like_hf_space(url: str) -> bool:
|
| host = url.split("://", 1)[-1].split("/", 1)[0].lower()
|
| return host == "hf.space" or host.endswith(".hf.space")
|
|
|
|
|
| def validate_worker_url(worker_url: str) -> str:
|
| worker_url = normalize_url(worker_url.strip())
|
| if not worker_url.startswith("https://"):
|
| raise ValueError("WORKER_BASE_URL must start with https://")
|
| if "localhost" in worker_url or "127.0.0.1" in worker_url:
|
| raise ValueError("WORKER_BASE_URL must be public, not localhost")
|
| if ".vercel.app" in worker_url:
|
| raise ValueError("WORKER_BASE_URL must point at the worker, not the Vercel site")
|
| if not looks_like_hf_space(worker_url):
|
| raise ValueError("WORKER_BASE_URL should be a Hugging Face Space URL ending in .hf.space")
|
| return worker_url
|
|
|
|
|
| def run_command(command: Sequence[str], input_text: str | None = None, check: bool = True) -> CommandResult:
|
| completed = subprocess.run(
|
| list(command),
|
| input=input_text,
|
| text=True,
|
| capture_output=True,
|
| )
|
| result = CommandResult(
|
| command=list(command),
|
| returncode=completed.returncode,
|
| stdout=completed.stdout,
|
| stderr=completed.stderr,
|
| )
|
| if check and completed.returncode != 0:
|
| message = completed.stderr.strip() or completed.stdout.strip() or f"command failed with {completed.returncode}"
|
| raise RuntimeError(f"{' '.join(command)} failed: {message}")
|
| return result
|
|
|
|
|
| def remove_vercel_env(name: str, environment: str) -> CommandResult:
|
| result = run_command([VERCEL_COMMAND, "env", "rm", name, environment, "--yes"], check=False)
|
| combined = f"{result.stdout}\n{result.stderr}"
|
| if result.returncode != 0 and "env_not_found" not in combined and "was not found" not in combined:
|
| message = result.stderr.strip() or result.stdout.strip() or f"command failed with {result.returncode}"
|
| raise RuntimeError(f"vercel env rm {name} failed: {message}")
|
| return result
|
|
|
|
|
| def add_vercel_env(name: str, value: str, environment: str) -> CommandResult:
|
| return run_command([VERCEL_COMMAND, "env", "add", name, environment], input_text=f"{value}\n")
|
|
|
|
|
| def configure_vercel_worker(
|
| worker_url: str,
|
| site_url: str = DEFAULT_SITE_URL,
|
| code: str = "1234",
|
| environment: str = "production",
|
| redeploy: bool = True,
|
| verify: bool = False,
|
| timeout: float = 60,
|
| ) -> dict[str, object]:
|
| worker_url = validate_worker_url(worker_url)
|
| site_url = normalize_url(site_url)
|
| commands: list[CommandResult] = []
|
|
|
| commands.append(remove_vercel_env("WORKER_BASE_URL", environment))
|
| commands.append(add_vercel_env("WORKER_BASE_URL", worker_url, environment))
|
| for key in DIRECT_CLOUD_ENV_KEYS:
|
| commands.append(remove_vercel_env(key, environment))
|
| if redeploy:
|
| commands.append(run_command([VERCEL_COMMAND, "deploy", "--prod", "--yes"]))
|
|
|
| site_checks = None
|
| if verify:
|
| site_checks = [check.__dict__ for check in verify_site(site_url, code, worker_url, timeout=timeout)]
|
|
|
| return {
|
| "workerUrl": worker_url,
|
| "siteUrl": site_url,
|
| "environment": environment,
|
| "redeployed": redeploy,
|
| "verified": verify,
|
| "commands": [
|
| {
|
| "command": result.command,
|
| "returncode": result.returncode,
|
| "stdout": result.stdout,
|
| "stderr": result.stderr,
|
| }
|
| for result in commands
|
| ],
|
| "siteChecks": site_checks,
|
| "nextCommand": (
|
| f"python scripts\\prove_live_deployment.py {worker_url} --origin {site_url} "
|
| f"--code {code} --smoke-ocr-engine arabic"
|
| ),
|
| }
|
|
|
|
|
| def main_with_args(argv: list[str] | None = None) -> int:
|
| parser = argparse.ArgumentParser(description="Set Vercel WORKER_BASE_URL, redeploy, and optionally verify the site.")
|
| parser.add_argument("worker_url", help="Public Hugging Face Space worker URL, e.g. https://user-space.hf.space")
|
| parser.add_argument("--site-url", default=DEFAULT_SITE_URL)
|
| parser.add_argument("--code", default="1234")
|
| parser.add_argument("--environment", default="production")
|
| parser.add_argument("--no-redeploy", action="store_true")
|
| parser.add_argument("--verify", action="store_true", help="Run scripts.verify_site after redeploy.")
|
| parser.add_argument("--timeout", type=float, default=60)
|
| parser.add_argument("--json", action="store_true")
|
| args = parser.parse_args(argv)
|
|
|
| result = configure_vercel_worker(
|
| args.worker_url,
|
| site_url=args.site_url,
|
| code=args.code,
|
| environment=args.environment,
|
| redeploy=not args.no_redeploy,
|
| verify=args.verify,
|
| timeout=args.timeout,
|
| )
|
| if args.json:
|
| print(json.dumps(result, indent=2))
|
| else:
|
| print(f"Configured WORKER_BASE_URL={result['workerUrl']}")
|
| if result["redeployed"]:
|
| print("Redeployed Vercel production.")
|
| print("Next:")
|
| print(result["nextCommand"])
|
| site_checks = result.get("siteChecks")
|
| if site_checks is not None and not all(check["ok"] for check in site_checks):
|
| return 1
|
| return 0
|
|
|
|
|
| def main() -> int:
|
| return main_with_args()
|
|
|
|
|
| if __name__ == "__main__":
|
| raise SystemExit(main())
|
|
|