File size: 5,505 Bytes
5270984 a9e8b86 5270984 a9e8b86 5270984 a9e8b86 5270984 a9e8b86 5270984 a9e8b86 5270984 a9e8b86 5270984 a9e8b86 5270984 a9e8b86 5270984 a9e8b86 5270984 a9e8b86 5270984 a9e8b86 5270984 a9e8b86 5270984 a9e8b86 5270984 | 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 | """Update the HF Space to the newest official Aether image digest.
Why this exists:
- Aether's built-in updater cannot persist across HF Docker Space restarts,
because the app binary/frontend live in the immutable Docker image layer.
- Rebuilding the Space from a new upstream image is the correct Docker update
path on Hugging Face.
Behavior:
1. Check the latest digest for ghcr.io/fawney19/aether:latest.
2. Read the Space repo Dockerfile.
3. If the Dockerfile is not pinned to that digest, upload a new Dockerfile with
`ARG AETHER_IMAGE=ghcr.io/fawney19/aether@sha256:...`.
4. HF automatically rebuilds the Space on that commit.
"""
import json
import os
import re
import sys
import tempfile
import urllib.request
from pathlib import Path
from huggingface_hub import HfApi, hf_hub_download
update_image = os.environ.get("AETHER_UPDATE_IMAGE", "ghcr.io/fawney19/aether:latest")
space_repo = os.environ.get("AETHER_HF_SPACE_REPO", "iiioooo1/aether-hf")
state_path = Path(os.environ.get(
"AETHER_LAST_DIGEST_PATH", "/opt/aether/data/.last_image_digest"
))
hf_token = os.environ["HF_TOKEN"]
def parse_ghcr_image(ref: str):
"""Return (repo, reference) for ghcr.io/<repo>:<tag> or @<digest>."""
if not ref.startswith("ghcr.io/"):
raise SystemExit(f"unsupported registry: {ref}")
rest = ref[len("ghcr.io/"):]
if "@" in rest:
repo, reference = rest.split("@", 1)
return repo, reference
if ":" in rest:
repo, reference = rest.rsplit(":", 1)
else:
repo, reference = rest, "latest"
return repo, reference
def fetch_ghcr_digest(repo: str, reference: str) -> str:
token_url = f"https://ghcr.io/token?scope=repository:{repo}:pull"
with urllib.request.urlopen(token_url, timeout=15) as r:
bearer = json.loads(r.read())["token"]
manifest_url = f"https://ghcr.io/v2/{repo}/manifests/{reference}"
req = urllib.request.Request(
manifest_url,
headers={
"Authorization": f"Bearer {bearer}",
"Accept": (
"application/vnd.oci.image.index.v1+json,"
"application/vnd.docker.distribution.manifest.list.v2+json,"
"application/vnd.oci.image.manifest.v1+json"
),
},
)
with urllib.request.urlopen(req, timeout=20) as r:
digest = r.headers.get("Docker-Content-Digest", "")
if not digest.startswith("sha256:"):
raise RuntimeError(f"no usable Docker-Content-Digest header for {repo}:{reference}: {digest!r}")
return digest
def read_space_dockerfile(api_token: str, repo_id: str) -> str:
local_path = hf_hub_download(
repo_id=repo_id,
repo_type="space",
filename="Dockerfile",
token=api_token,
force_download=True,
)
return Path(local_path).read_text(encoding="utf-8")
def pin_dockerfile(content: str, pinned_ref: str) -> tuple[str, bool]:
pattern = re.compile(r"^ARG\s+AETHER_IMAGE=.*$", re.MULTILINE)
new_line = f"ARG AETHER_IMAGE={pinned_ref}"
match = pattern.search(content)
if match:
if match.group(0).strip() == new_line:
return content, False
return pattern.sub(new_line, content, count=1), True
# Fallback: insert after syntax line if the ARG is missing.
lines = content.splitlines()
if lines and lines[0].startswith("# syntax="):
lines.insert(1, new_line)
else:
lines.insert(0, new_line)
return "\n".join(lines) + "\n", True
def upload_dockerfile(api: HfApi, repo_id: str, content: str, digest: str) -> None:
with tempfile.NamedTemporaryFile("w", encoding="utf-8", delete=False) as tmp:
tmp.write(content)
tmp_path = tmp.name
try:
api.upload_file(
path_or_fileobj=tmp_path,
path_in_repo="Dockerfile",
repo_id=repo_id,
repo_type="space",
commit_message=f"chore: bump Aether upstream image {digest[:19]}",
commit_description=(
"Auto-update HF Space by pinning the official upstream image "
f"ghcr.io/fawney19/aether to digest {digest}.\n\n"
"HF rebuilds the Docker Space from this commit, which is the "
"persistent Docker update path for Hugging Face Spaces."
),
)
finally:
try:
Path(tmp_path).unlink(missing_ok=True)
except Exception:
pass
repo, reference = parse_ghcr_image(update_image)
print(f"aether-auto-update: checking ghcr.io/{repo}:{reference}", file=sys.stderr)
digest = fetch_ghcr_digest(repo, reference)
pinned_ref = f"ghcr.io/{repo}@{digest}"
print(f"aether-auto-update: latest upstream digest {digest}", file=sys.stderr)
api = HfApi(token=hf_token)
dockerfile = read_space_dockerfile(hf_token, space_repo)
updated, changed = pin_dockerfile(dockerfile, pinned_ref)
if not changed:
print("aether-auto-update: Space Dockerfile already pins latest digest; no rebuild needed", file=sys.stderr)
state_path.parent.mkdir(parents=True, exist_ok=True)
state_path.write_text(digest + "\n", encoding="utf-8")
sys.exit(0)
print(f"aether-auto-update: updating Space Dockerfile to {pinned_ref}", file=sys.stderr)
upload_dockerfile(api, space_repo, updated, digest)
state_path.parent.mkdir(parents=True, exist_ok=True)
state_path.write_text(digest + "\n", encoding="utf-8")
print("aether-auto-update: Dockerfile uploaded; HF Space rebuild should start automatically", file=sys.stderr)
|