Skip to content

feat(registry): push to ghcr.io, pull through a cell's remote repository - #1145

Merged
antoinetoussaint-byte merged 2 commits into
mainfrom
feat/ghcr-push-pull-through
Oct 24, 2026
Merged

antoinetoussaint-byte merged 2 commits into
mainfrom
feat/ghcr-push-pull-through

Conversation

@antoinetoussaint-byte

@antoinetoussaint-byte antoinetoussaint-byte commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Why

Owner decision 2026-10-24: obin-ai images are pushed only to ghcr.io (private packages, one canonical registry), and each cell pulls through a same-cloud Artifact Registry remote repository whose upstream is ghcr.io — workload-identity pulls, no registry credential in any cell. This is the CLI half: the push and the pull become two registries when the environment mirrors its own registry.url host.

Classification: fix at the owner of the behaviour (the environment registry declaration and the build/render path that names, pushes and probes images). No hack.

Design

  • EnvironmentRegistry.PullRepository() (pkg/environments): url read through mirrors, with the same normalization a base image is mirrored with. mirroredReference/referenceRegistry moved from pkg/orchestration to environments.MirroredReference/ReferenceRegistry so there is one spelling; orchestration delegates. A repository prefix is parsed with one probe component so myorg is docker.io/myorg, not docker.io/library/myorg, and a bare localhost:5001 is a host.
  • registry.artifacts (new, optional): where frontend bundles are published and read instead of url — AR remote repos are not verified to serve non-image OCI artifacts. Validated with validateRegistryReference. auth: ghcr now requires a ghcr.io url. Both parse through the strict coordinate-contract decoder (KnownFields(true)), covered by a test.
  • auth: ghcr is wired (pkg/builder): token from GITHUB_TOKEN, else GH_TOKEN, else gh auth token --hostname github.com; user from GITHUB_ACTOR, else gh api user --jq .login; docker login ghcr.io --password-stdin, same shape as the ecr/acr arms. The token goes on stdin only and is never logged. Commands run behind a seam for tests.
  • prepareSnapshotRegistry sets the agent's DockerRepository to PullRepository(), so manifests and the cell file name the remote-repository path with the digest. Login stays against url/auth.
  • recipeAsBuilt: when pushing and pull≠push, clones the recipe and replaces the <pull>/ prefix of Image with <url>/ (foreign images untouched). Push tag, released-version check, reuse cache and imageStillExists therefore all use the ghcr reference; the plan (and so manifests/cell file/SBOM subjects) keeps the pull name.
  • Released-version check: only a not-found probe means "not published"; any other probe error (auth refusal, timeout, unreachable) fails the render instead of building over a released tag. classifyImageProbeError in image_cache.go maps buildx's not found / manifest unknown / name unknown to errImageNotFound (confirmed against ghcr.io: a missing package and a missing tag both answer ERROR: <ref>: not found).
  • withSourceLabel: when url is on ghcr.io, a pushed build gets --label org.opencontainers.image.source=<workspace GitHub https URL> (+ --annotation index:… multi-platform), which links a private package to its repository. Applied after the identity is computed, like withBuildInputsLabel (both now share withBuildFlags), so it is not an identity input. The URL comes from the workspace's origin remote; a non-GitHub remote adds nothing.
  • Legacy in-agent build path (builder.go): when pull≠push, docker tag to the push reference, push that, and inspect its digest.
  • frontendBundleRepositoryOf uses registry.artifacts when set, else url.

Reused: the existing mirrors map and its normalization, validateRegistryReference, the RegistryLogin arms' stdin pattern, the resolveImageManifestDigest probe seam, the withBuildInputsLabel insertion point. Deliberately not added: a separate pull field (the mirror already says where a registry is read from), any credential in the cell.

Compatibility

With no mirror for the registry.url host, PullRepository() == url, pushReference is the identity and every image is named, pushed and probed exactly as before. artifacts absent → bundles at url as before. Two deliberate behaviour changes that apply regardless of mirrors:

  1. a released-version probe failing for a reason other than not-found now stops the render (it used to fall through to a rebuild + push over the released tag);
  2. auth: ghcr with a non-ghcr.io url is refused at validation (it used to fail at login with "not yet wired").
  3. a coordinate-contract re-import that declares registry without auth now removes a previously written registry.auth (it used to keep it).
    An image pushed to ghcr.io gains the source label; it is not part of the identity, so no cached image is rebuilt for it.

Tests

  • pkg/environments: PullRepository (no mirror, other-registry mirror, mirrored, trailing slash, bare host, Hub namespace); artifacts + auth: ghcr validation; coordinate-contract import carrying auth/mirrors/artifacts.

  • pkg/builder: ghcr token sources (CI env, GH_TOKEN, gh CLI) via the exec seam, token only on stdin, refusals name no token.

  • pkg/orchestration: pushReference swaps only images under the pull base; recipeAsBuilt renames the pushed recipe and leaves the plan alone (verified to fail with the swap removed); a released version is probed and built -t ghcr.io/… (no remote path in the build argv); a non-not-found probe error stops the render with zero builds; probe classification over real exit errors; source label present on a ghcr build, absent elsewhere, and a changed source reuses the image (same identity, one build); legacy path tags and pushes the ghcr reference.

  • pkg/gitops: prepareSnapshotRegistry hands agents the remote-repository path as DockerRepository; bundle repository uses artifacts.

  • go test ./pkg/environments ./pkg/builder ./pkg/orchestration ./pkg/gitops ./cmd/environment ./cmd/build, make lint (0 issues).

  • Re-import (coordinator decision): registry.artifacts and registry.auth join registry.mirrors (cli#1140) as contract-owned keys, so a contract declaring registry without them withdraws what an earlier import wrote. Tested like fix(environment): re-import withdraws a registry mirror the contract no longer carries #1140's mirror test (TestReimportWithdrawsArtifactsAndAuthTheContractNoLongerCarries, verified to fail without the change); docs/commands.md's contract-owned line updated.

Docs

docs/commands.md: the mirror section now covers mirroring the push registry itself (push to url, deploy the mirror path), auth: ghcr, the source label and registry.artifacts; the registry.auth coverage line no longer calls ghcr a stub; the released-version section states the not-found-only rule.

Not verified

  • No real push to ghcr.io and no pull through an Artifact Registry remote repository: no cell was rendered. The rendered manifest and cell-file image are asserted at the DockerRepository the agent names images with, not through a real agent render.
  • The --annotation index: flag and the ghcr package↔repository link were not exercised against ghcr.io.
  • SBOM evidence subjects keep the pull name (from the plan); an image-scan agent resolving <remote>@digest from a dev machine needs read access to the remote repository. Not exercised.
  • Publishing bundles to an artifacts registry other than url relies on Docker credentials already present for it (no login is run for artifacts).

🤖 Generated with Claude Code

antoinetoussaint-byte and others added 2 commits October 24, 2026 14:27
A mirror declared for the host of registry.url now splits push from pull:
images are pushed (and probed, cached, checked) at registry.url and the
rendered manifests and cell file name them under the mirror with the pushed
digest. EnvironmentRegistry.PullRepository is url read through mirrors, with
the one mirroredReference spelling now shared from pkg/environments.

- registry.artifacts: where frontend bundles are published and read
- auth: ghcr wired (GITHUB_TOKEN/GH_TOKEN/gh auth token, GITHUB_ACTOR/gh api
  user, docker login --password-stdin) and requires a ghcr.io url
- a released version is only built when the registry answers not found;
  any other probe failure stops the render
- ghcr.io pushes carry org.opencontainers.image.source, outside the identity
- the legacy in-agent path tags and pushes the push reference

No mirror for the registry.url host: unchanged naming, push and probe.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
….auth

Like registry.mirrors (cli#1140), both are contract-owned: a contract that
declares registry without them withdraws the value an earlier import wrote.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@antoinetoussaint-byte
antoinetoussaint-byte added this pull request to the merge queue Oct 24, 2026
Merged via the queue into main with commit 31e88b0 Oct 24, 2026
15 of 16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant