File size: 8,170 Bytes
a74a718 | 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 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 | from __future__ import annotations
import argparse
import json
import os
import subprocess
import tempfile
import time
import urllib.error
import urllib.request
from pathlib import Path
from typing import Iterable
OFFICIAL_UPSTREAM_REMOTE = "https://github.com/NousResearch/hermes-agent.git"
PROJECT_ROOT = Path(__file__).resolve().parent.parent
UPSTREAM_REF_FILE = PROJECT_ROOT / "docker" / "HERMES_UPSTREAM_REF"
DEFAULT_WAIT_TIMEOUT = 1800
POLL_INTERVAL_SECONDS = 5
def run(cmd, **kwargs):
kwargs.setdefault("check", True)
return subprocess.run(cmd, **kwargs)
def is_hf_space_runtime(env: dict[str, str] | None = None) -> bool:
env = env or os.environ
return bool(env.get("SPACE_ID") or env.get("SPACE_HOST"))
def get_hf_token(env: dict[str, str] | None = None) -> str | None:
env = env or os.environ
for key in ("HF_TOKEN", "HUGGINGFACE_TOKEN", "HF_API_TOKEN"):
value = env.get(key)
if value:
return value
return None
def list_remote_tags(remote: str = OFFICIAL_UPSTREAM_REMOTE) -> list[str]:
result = run(
["git", "ls-remote", "--tags", "--refs", "--sort=-v:refname", remote],
capture_output=True,
text=True,
)
tags: list[str] = []
for line in result.stdout.splitlines():
parts = line.split()
if len(parts) != 2:
continue
ref = parts[1]
prefix = "refs/tags/"
if ref.startswith(prefix):
tags.append(ref[len(prefix) :])
return tags
def pick_target_ref(requested_ref: str | None) -> str:
if requested_ref:
return requested_ref
tags = list_remote_tags()
if not tags:
raise RuntimeError("Could not determine the latest upstream Hermes tag.")
return tags[0]
def build_space_clone_url(space_id: str, token: str) -> str:
return f"https://oauth2:{token}@huggingface.co/spaces/{space_id}"
def clone_space_repo(space_id: str, token: str, workdir: Path | None = None) -> Path:
root = Path(workdir) if workdir else Path(tempfile.mkdtemp(prefix="hermes-space-update-"))
repo_dir = root / "repo"
run(["git", "clone", "--depth", "1", build_space_clone_url(space_id, token), str(repo_dir)])
return repo_dir
def read_current_ref(path: Path = UPSTREAM_REF_FILE) -> str | None:
if not path.exists():
return None
value = path.read_text(encoding="utf-8").strip()
return value or None
def apply_space_repo_update(
space_id: str,
token: str,
target_ref: str,
workdir: Path | None = None,
) -> str | None:
repo_dir = clone_space_repo(space_id, token, workdir=workdir)
ref_file = repo_dir / "docker" / "HERMES_UPSTREAM_REF"
current_ref = read_current_ref(ref_file)
if current_ref == target_ref:
return None
ref_file.parent.mkdir(parents=True, exist_ok=True)
ref_file.write_text(f"{target_ref}\n", encoding="utf-8")
run(["git", "config", "user.email", "hermes-space-updater@local"], cwd=repo_dir)
run(["git", "config", "user.name", "Hermes Space Updater"], cwd=repo_dir)
run(["git", "add", "docker/HERMES_UPSTREAM_REF"], cwd=repo_dir)
run(["git", "commit", "-m", f"Update Hermes upstream ref to {target_ref}"], cwd=repo_dir)
run(["git", "push", "origin", "HEAD:main"], cwd=repo_dir)
result = run(["git", "rev-parse", "HEAD"], cwd=repo_dir, capture_output=True, text=True)
return result.stdout.strip()
def _runtime_request(space_id: str, token: str | None = None):
request = urllib.request.Request(f"https://huggingface.co/api/spaces/{space_id}/runtime")
if token:
request.add_header("Authorization", f"Bearer {token}")
with urllib.request.urlopen(request, timeout=30) as response:
return json.loads(response.read().decode("utf-8"))
def wait_for_space_runtime(
space_id: str,
expected_sha: str,
token: str | None = None,
timeout: int = DEFAULT_WAIT_TIMEOUT,
poll_interval: int = POLL_INTERVAL_SECONDS,
) -> dict:
deadline = time.time() + timeout
last_stage = None
while time.time() < deadline:
payload = _runtime_request(space_id, token)
stage = payload.get("stage")
sha = payload.get("sha")
if stage != last_stage:
print(f"• Space stage: {stage} (sha={sha})")
last_stage = stage
if sha == expected_sha and stage == "RUNNING":
return payload
if stage in {"BUILD_ERROR", "RUNTIME_ERROR", "CONFIG_ERROR"}:
raise RuntimeError(f"Space rebuild failed with stage {stage} (sha={sha})")
time.sleep(poll_interval)
raise TimeoutError(f"Timed out waiting for Space runtime to reach {expected_sha}")
def _gateway_state_paths(env: dict[str, str] | None = None) -> tuple[Path, Path]:
env = env or os.environ
hermes_home = Path(env.get("HERMES_HOME", str(Path.home() / ".hermes")))
return hermes_home / ".update_output.txt", hermes_home / ".update_exit_code"
def write_gateway_exit_code(exit_code: int, env: dict[str, str] | None = None) -> None:
_output_path, exit_code_path = _gateway_state_paths(env)
exit_code_path.write_text(str(exit_code), encoding="utf-8")
def run_update_cli(argv: Iterable[str] | None = None) -> int:
parser = argparse.ArgumentParser(
prog="hermes update",
description="Update a Hugging Face Space by bumping its upstream Hermes ref and triggering a rebuild.",
)
parser.add_argument("--gateway", action="store_true", default=False)
parser.add_argument("--ref", help="Update to a specific upstream tag or commit instead of the latest tag.")
parser.add_argument("--no-wait", action="store_true", default=False, help="Push the Space update and exit without waiting for rebuild completion.")
parser.add_argument("--timeout", type=int, default=DEFAULT_WAIT_TIMEOUT)
args = parser.parse_args(list(argv) if argv is not None else None)
try:
if not is_hf_space_runtime():
print("✗ HF Space update flow is only available inside a Hugging Face Space runtime.")
if args.gateway:
write_gateway_exit_code(1)
return 1
space_id = os.environ.get("SPACE_ID")
if not space_id:
print("✗ SPACE_ID is not set; cannot locate the current Hugging Face Space.")
if args.gateway:
write_gateway_exit_code(1)
return 1
token = get_hf_token()
if not token:
print("✗ HF_TOKEN (or HUGGINGFACE_TOKEN) is required to update the Space repository.")
if args.gateway:
write_gateway_exit_code(1)
return 1
target_ref = pick_target_ref(args.ref)
current_ref = read_current_ref()
if current_ref == target_ref:
print(f"Already up to date: {target_ref}")
if args.gateway:
write_gateway_exit_code(0)
return 0
print(f"⚕ Updating Hugging Face Space {space_id} to upstream ref {target_ref}...")
commit_sha = apply_space_repo_update(space_id=space_id, token=token, target_ref=target_ref)
if commit_sha is None:
print(f"Already up to date: {target_ref}")
if args.gateway:
write_gateway_exit_code(0)
return 0
print(f"✓ Pushed Space commit {commit_sha}")
if args.no_wait:
print("✓ Space rebuild submitted. Current runtime will keep serving until the new build is ready.")
if args.gateway:
write_gateway_exit_code(0)
return 0
print("⏳ Waiting for Hugging Face Space rebuild to finish...")
payload = wait_for_space_runtime(space_id, commit_sha, token=token, timeout=args.timeout)
if args.gateway:
write_gateway_exit_code(0)
print(f"✓ Space is RUNNING at {payload.get('sha')}")
return 0
except (subprocess.CalledProcessError, RuntimeError, TimeoutError, urllib.error.URLError) as exc:
print(f"✗ {exc}")
if args.gateway:
write_gateway_exit_code(1)
return 1
if __name__ == "__main__":
raise SystemExit(run_update_cli())
|