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)