File size: 10,007 Bytes
085f660
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
#!/usr/bin/env python3
"""Upload one prepared Hub surface and record its compare-and-swap commit."""

from __future__ import annotations

import argparse
import hashlib
import json
import os
import re
import subprocess
import sys
import tempfile
from pathlib import Path
from typing import Any

COMMIT_PATTERN = re.compile(r"[0-9a-f]{40}")
MARKER_SCHEMA = "noqt.huggingface_sync.v1"
UPLOAD_SCHEMA = "noqt.huggingface_upload.v1"
REMOTE_MARKER = "HUGGINGFACE_SYNC.json"
# One-time first-migration bridge for the known public Space parent that
# predates the source marker.  The bridge is intentionally bound to both the
# exact parent and the independently observed source commit; all other
# missing, malformed, or unreadable markers remain fail-closed.
LEGACY_BOOTSTRAP_PARENT = "c089755e699c75c68e926613d59219bf4feebac5"
LEGACY_BOOTSTRAP_SOURCE = "bf6910dbd83d30a50a486f84ac0fa96a0244e23e"


class UploadError(RuntimeError):
    """Raised when a Hub upload cannot be bound safely."""


def _sha256(path: Path) -> str:
    return hashlib.sha256(path.read_bytes()).hexdigest()


def _write_once(path: Path, value: dict[str, Any]) -> None:
    path = Path(path)
    if path.exists() or path.is_symlink():
        raise UploadError(f"output already exists: {path}")
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(json.dumps(value, indent=2, sort_keys=True) + "\n", encoding="utf-8")


def _is_missing_remote_marker_error(error: BaseException) -> bool:
    """Recognize only the Hub client's missing-entry exception variants."""

    # huggingface_hub 1.7.1 exposes both names from huggingface_hub.errors;
    # older clients called the remote 404 RemoteEntryNotFoundError.  Do not
    # classify transport/auth/permission errors as a missing marker.
    try:
        from huggingface_hub import errors as hub_errors
    except (ImportError, AttributeError):
        return False
    missing_types = tuple(
        error_type
        for name in ("EntryNotFoundError", "RemoteEntryNotFoundError")
        if isinstance(error_type := getattr(hub_errors, name, None), type)
    )
    return bool(missing_types) and isinstance(error, missing_types)


def _remote_source_revision(api: Any, repo_id: str, repo_type: str, parent: str, token: str) -> str:
    try:
        with tempfile.TemporaryDirectory(prefix="hf-sync-parent-marker-") as marker_dir:
            marker_path = Path(
                api.hf_hub_download(
                    repo_id=repo_id,
                    filename=REMOTE_MARKER,
                    repo_type=repo_type,
                    revision=parent,
                    local_dir=marker_dir,
                    token=token,
                )
            )
            if marker_path.is_symlink() or not marker_path.is_file():
                raise UploadError("remote source marker is not a regular file")
            marker = json.loads(marker_path.read_text(encoding="utf-8"))
    except UploadError:
        raise
    except Exception as error:
        if parent == LEGACY_BOOTSTRAP_PARENT and _is_missing_remote_marker_error(error):
            return LEGACY_BOOTSTRAP_SOURCE
        raise UploadError("remote source marker is unreadable; distribution remains INCOMPLETE") from error
    if not isinstance(marker, dict):
        raise UploadError("remote source marker is not an object; distribution remains INCOMPLETE")
    source_revision = marker.get("source_revision")
    if marker.get("schema") != MARKER_SCHEMA or not isinstance(source_revision, str) or not COMMIT_PATTERN.fullmatch(source_revision):
        raise UploadError("remote source marker has no exact source commit; distribution remains INCOMPLETE")
    return source_revision


def _require_source_ancestry(source_root: Path, ancestor: str, descendant: str) -> None:
    source_root = Path(source_root)
    if source_root.is_symlink():
        raise UploadError("source root must not be a symlink")
    try:
        source_root = source_root.resolve(strict=True)
    except OSError as error:
        raise UploadError("source root is unavailable; distribution remains INCOMPLETE") from error
    if not source_root.is_dir():
        raise UploadError("source root is not a directory; distribution remains INCOMPLETE")
    try:
        result = subprocess.run(
            ["git", "-C", str(source_root), "merge-base", "--is-ancestor", ancestor, descendant],
            check=False,
            capture_output=True,
            text=True,
        )
    except OSError as error:
        raise UploadError("cannot verify remote source ancestry; distribution remains INCOMPLETE") from error
    if result.returncode != 0:
        raise UploadError("remote source is not an ancestor of the candidate; distribution remains INCOMPLETE")


def upload_surface(
    *,
    repo_id: str,
    repo_type: str,
    folder: Path,
    revision: str,
    source_revision: str,
    output: Path,
    source_root: Path = Path("."),
) -> dict[str, Any]:
    if repo_type != "space":
        raise UploadError("only the configured Space repository type is supported")
    if not COMMIT_PATTERN.fullmatch(source_revision):
        raise UploadError("source revision must be an exact 40-character commit")
    output = Path(output)
    if output.exists() or output.is_symlink():
        raise UploadError(f"output already exists: {output}")
    folder = Path(folder)
    if folder.is_symlink():
        raise UploadError("upload folder must not be a symlink")
    folder = folder.resolve(strict=True)
    if not folder.is_dir() or folder.is_symlink():
        raise UploadError("upload folder must be a regular directory")
    marker_path = folder / "HUGGINGFACE_SYNC.json"
    manifest_path = folder / "HUGGINGFACE_MANIFEST.json"
    try:
        marker = json.loads(marker_path.read_text(encoding="utf-8"))
        manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
    except (OSError, UnicodeDecodeError, json.JSONDecodeError) as error:
        raise UploadError("prepared surface metadata is unreadable") from error
    if (
        marker.get("schema") != MARKER_SCHEMA
        or marker.get("source_revision") != source_revision
        or manifest.get("source_revision") != source_revision
        or marker.get("manifest_sha256") != _sha256(manifest_path)
    ):
        raise UploadError("prepared surface is not bound to the requested source revision")
    token = os.environ.get("HF_TOKEN", "")
    if not token:
        raise UploadError("HF_TOKEN is missing; distribution remains INCOMPLETE")
    try:
        from huggingface_hub import HfApi

        api = HfApi(token=token)
        parent = api.repo_info(repo_id=repo_id, repo_type=repo_type, revision=revision).sha
        if not isinstance(parent, str) or not COMMIT_PATTERN.fullmatch(parent):
            raise UploadError("remote parent revision is not an exact commit")
        remote_source_revision = _remote_source_revision(api, repo_id, repo_type, parent, token)
        _require_source_ancestry(source_root, remote_source_revision, source_revision)
        commit = api.upload_folder(
            repo_id=repo_id,
            repo_type=repo_type,
            folder_path=folder,
            path_in_repo=".",
            revision=revision,
            parent_commit=parent,
            commit_message=f"Sync GitHub source {source_revision}",
            commit_description=(
                "Canonical source: "
                f"https://github.com/noqt/Lumi-Eggcracker/commit/{source_revision}"
            ),
            delete_patterns=["*"],
            token=token,
        )
    except UploadError:
        raise
    except Exception as error:  # pragma: no cover - exercised by the remote service
        raise UploadError(f"Hub upload failed ({type(error).__name__}); distribution remains INCOMPLETE") from error
    upload_commit = getattr(commit, "oid", None)
    commit_url = getattr(commit, "commit_url", None)
    if not isinstance(upload_commit, str) or not COMMIT_PATTERN.fullmatch(upload_commit):
        raise UploadError("Hub upload returned no exact commit; distribution remains INCOMPLETE")
    if not isinstance(commit_url, str) or not commit_url.startswith("https://huggingface.co/"):
        raise UploadError("Hub upload returned no canonical commit link")
    result = {
        "schema": UPLOAD_SCHEMA,
        "repository": repo_id,
        "repo_type": repo_type,
        "branch": revision,
        "source_revision": source_revision,
        "source_url": f"https://github.com/noqt/Lumi-Eggcracker/commit/{source_revision}",
        "parent_commit": parent,
        "upload_commit": upload_commit,
        "commit_url": commit_url,
        "manifest_sha256": marker["manifest_sha256"],
    }
    _write_once(Path(output), result)
    return result


def _parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--repo-id", required=True)
    parser.add_argument("--repo-type", required=True)
    parser.add_argument("--folder", required=True, type=Path)
    parser.add_argument("--revision", required=True)
    parser.add_argument("--source-revision", required=True)
    parser.add_argument("--output", required=True, type=Path)
    parser.add_argument("--source-root", type=Path, default=Path("."))
    return parser


def main() -> int:
    args = _parser().parse_args()
    try:
        result = upload_surface(
            repo_id=args.repo_id,
            repo_type=args.repo_type,
            folder=args.folder,
            revision=args.revision,
            source_revision=args.source_revision,
            output=args.output,
            source_root=args.source_root,
        )
    except UploadError as error:
        print(f"HUGGINGFACE_SYNC_INCOMPLETE: {error}", file=sys.stderr)
        return 2
    print(json.dumps({key: result[key] for key in ("source_revision", "parent_commit", "upload_commit", "manifest_sha256")}, sort_keys=True))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())