#!/usr/bin/env python3 """Publish a validated Hermes candidate manifest under its Flux release tag.""" from __future__ import annotations import argparse import base64 import json import os import re import urllib.error import urllib.parse import urllib.request from pathlib import Path from typing import Any, Callable REGISTRY_ORIGIN = "https://registry.bstein.dev" DESTINATION_PATTERN = re.compile( r"^registry\.bstein\.dev/bstein/" r"(?Phermes-(?:agent|webui|chat-router|jetson-(?:stt|tts))):" r"git-(?P[0-9a-f]{40})-build-(?P[1-9][0-9]*)$" ) DIGEST_PATTERN = re.compile(r"^sha256:[0-9a-f]{64}$") MANIFEST_TYPES = { "application/vnd.docker.distribution.manifest.v2+json", "application/vnd.docker.distribution.manifest.list.v2+json", "application/vnd.oci.image.manifest.v1+json", "application/vnd.oci.image.index.v1+json", } MAX_MANIFEST_BYTES = 4 * 1024 * 1024 class _NoRedirect(urllib.request.HTTPRedirectHandler): """Never forward registry credentials to another origin.""" def redirect_request(self, _request, _file, _code, _message, _headers, _url): return None def _registry_request(request: urllib.request.Request, timeout: int) -> Any: """Return normal and HTTP error responses without following redirects.""" opener = urllib.request.build_opener(_NoRedirect()) try: return opener.open(request, timeout=timeout) except urllib.error.HTTPError as exc: return exc def _status(response: Any) -> int: """Normalize urllib response and HTTPError status fields.""" return int(getattr(response, "status", getattr(response, "code", 0))) def _authorization(username: str, password: str) -> str: """Build a Basic authorization value without placing it in a URL.""" if not username or not password: raise RuntimeError("Harbor credentials are unavailable") encoded = base64.b64encode(f"{username}:{password}".encode()).decode("ascii") return f"Basic {encoded}" def _validated_release( destination: str, digest: str, source_revision: str, build_number: str, ) -> tuple[str, str, str]: """Bind the candidate and release tag to one reviewed source and build.""" match = DESTINATION_PATTERN.fullmatch(destination.strip()) if not match: raise ValueError("invalid Hermes candidate destination") if match.group("revision") != source_revision.strip(): raise ValueError("candidate source revision does not match evidence") if match.group("build") != build_number.strip(): raise ValueError("candidate build number does not match evidence") normalized_digest = digest.strip() if not DIGEST_PATTERN.fullmatch(normalized_digest): raise ValueError("invalid candidate digest") candidate_tag = destination.rsplit(":", 1)[1] return match.group("component"), candidate_tag, normalized_digest def _manifest_url(component: str, tag: str) -> str: """Return one same-origin, path-escaped Docker Registry manifest URL.""" encoded_tag = urllib.parse.quote(tag, safe="") return f"{REGISTRY_ORIGIN}/v2/bstein/{component}/manifests/{encoded_tag}" def promote_candidate( *, destination: str, digest: str, source_revision: str, build_number: str, username: str, password: str, opener: Callable[[urllib.request.Request, int], Any] = _registry_request, ) -> dict[str, str]: """Copy an exact candidate manifest to the immutable ``-release`` tag.""" component, candidate_tag, normalized_digest = _validated_release( destination, digest, source_revision, build_number ) release_tag = f"{candidate_tag}-release" authorization = _authorization(username, password) accept = ", ".join(sorted(MANIFEST_TYPES)) candidate_request = urllib.request.Request( _manifest_url(component, candidate_tag), headers={"Accept": accept, "Authorization": authorization}, method="GET", ) with opener(candidate_request, 30) as response: if _status(response) != 200: raise RuntimeError(f"candidate manifest returned HTTP {_status(response)}") manifest = response.read(MAX_MANIFEST_BYTES + 1) if len(manifest) > MAX_MANIFEST_BYTES: raise RuntimeError("candidate manifest exceeded the size limit") observed_digest = response.headers.get("Docker-Content-Digest", "") content_type = response.headers.get("Content-Type", "").split(";", 1)[0] if observed_digest != normalized_digest: raise RuntimeError("candidate manifest digest does not match build evidence") if content_type not in MANIFEST_TYPES: raise RuntimeError("candidate manifest returned an unsupported content type") release_url = _manifest_url(component, release_tag) head_request = urllib.request.Request( release_url, headers={"Accept": accept, "Authorization": authorization}, method="HEAD", ) with opener(head_request, 20) as response: head_status = _status(response) existing_digest = response.headers.get("Docker-Content-Digest", "") if head_status == 200: if existing_digest != normalized_digest: raise RuntimeError("release tag already exists with another digest") result = "already-present" elif head_status == 404: put_request = urllib.request.Request( release_url, data=manifest, headers={ "Authorization": authorization, "Content-Type": content_type, }, method="PUT", ) with opener(put_request, 30) as response: put_status = _status(response) promoted_digest = response.headers.get("Docker-Content-Digest", "") if put_status not in {201, 202}: raise RuntimeError(f"release manifest returned HTTP {put_status}") if promoted_digest and promoted_digest != normalized_digest: raise RuntimeError("release manifest digest changed during promotion") result = "published" else: raise RuntimeError(f"release tag preflight returned HTTP {head_status}") return { "component": component, "digest": normalized_digest, "release_tag": release_tag, "result": result, "source_revision": source_revision, } def main() -> int: """Promote one evidence-bound candidate and print credential-free metadata.""" parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--destination", required=True) parser.add_argument("--digest-file", required=True, type=Path) parser.add_argument("--source-revision", required=True) parser.add_argument("--build-number", required=True) args = parser.parse_args() try: result = promote_candidate( destination=args.destination, digest=args.digest_file.read_text(encoding="utf-8"), source_revision=args.source_revision, build_number=args.build_number, username=os.environ.get("HARBOR_USER", ""), password=os.environ.get("HARBOR_PASSWORD", ""), ) except (OSError, ValueError, RuntimeError, urllib.error.URLError) as exc: print(json.dumps({"error": str(exc)}, sort_keys=True)) return 1 print(json.dumps(result, indent=2, sort_keys=True)) return 0 if __name__ == "__main__": # pragma: no cover - exercised through main() raise SystemExit(main())