Skip to content

ci: publish privately, then promote to Docker Hub - #47

Merged
douglance merged 1 commit into
mainfrom
ci/private-first-publish
Aug 21, 2026
Merged

ci: publish privately, then promote to Docker Hub#47
douglance merged 1 commit into
mainfrom
ci/private-first-publish

Conversation

@douglance

Copy link
Copy Markdown
Collaborator

Why

Docker Hub is public and its tags are permanent in practice, so a release reaching it as a side effect of a tag push leaves no room to check the image first. This splits publishing from going public: build into the private GHCR package, verify, then promote.

Two holes turned up while wiring it:

  • Docker Hub has the 11 v0.2.10 version tags and no latest-* tags. Both action.yml (version: latest) and bake/action.yml (bundle-version: latest) resolve offchainlabs/arbitrum-litro:latest-<variant> by default, so the default path currently names a tag that does not exist. Aliases only started with feat(bake): customize composed contract bundles without rebuilding #39 and nothing has published them yet.
  • packages/core snapshot bake defaulted its base image to ghcr.io/offchainlabs/arbitrum-testnode-ci:latest-<variant> — the old package name, which has no latest-* tags either.

What changed

Publish Testnode

  • registries input: ghcr (default) or ghcr,dockerhub. Dispatch inputs do not exist on a tag push, so a tag falls through to ghcr.
  • Resolved once in resolve-publish-matrix and consumed by both the build and the alias job, so they cannot disagree about where a release went.
  • Resolve image refs, the taken-tag guard and the credential check collapse into scripts/ci/resolve-publish-refs.mjs, built on buildTestnodeImageRef rather than re-deriving the tag suffix in bash. It runs before the snapshot now, so a taken tag or missing credential fails in a minute instead of after ~30 minutes of init.
  • The image carries org.opencontainers.image.source. That label is what links a GHCR package to a repository and grants the repository's own workflows access to it while private, so the new package does not start orphaned.

Verify Published Image (new, dispatch only)
Boots a published version's l2 and l3-eth images through the action, authenticating against GHCR, and asserts each image's bundle labels match the tag it was pulled as. Promotion copies digests, so a mislabelled image would stay mislabelled all the way to the public registry. This is deliberately the opposite of test-action.yml's test-published-image-action, which proves the public default needs no credential.

Mirror Images to Docker Hub

  • source-repository default moves to ghcr.io/offchainlabs/arbitrum-litro, where releases now go.
  • Carries latest-<variant> across, but only when the source alias still names a version this run mirrored. An alias whose digest is not in the mirrored set is reported as held and left alone, so the public alias can never claim a version Docker Hub does not have.

Verification

  • pnpm validate: 280/280.
  • resolve-publish-refs.mjs exercised on all four paths: GHCR-only, Docker Hub without credentials (fails), Docker Hub against the existing v0.2.10-nc3.2-l2 (refuses), Docker Hub against a free v0.3.0 tag (proceeds).
  • Alias promotion exercised against a stub registry: version tags copy, latest-l2 copies because the source alias points at a mirrored version, latest-l3-eth is held because the source alias points elsewhere. Re-running reports already current rather than copying again.
  • yaml-lint clean on all three workflows.

After merge

pnpm release 0.3.0 and push the tag. That run creates the arbitrum-litro GHCR package (private, linked via the new label), then Verify Published Image for v0.3.0, then Mirror Images to Docker Hub to make it public along with its latest-* aliases.

Docker Hub is public and its tags are permanent in practice, so a release
reaching it as a side effect of a tag push leaves no room to check the image
first. Publishing now lands in the private GHCR package by default, and getting
to Docker Hub is a separate dispatch.

- `registries` input on Publish Testnode selects where a run pushes; a tag push
  falls through to `ghcr`. One resolved selection feeds both the build and the
  alias job, so they cannot disagree about where a release went.
- Verify Published Image boots a published version's l2 and l3-eth images
  through the action against GHCR, with a credential, and checks each image's
  bundle labels match the tag it was pulled as.
- Mirror Images to Docker Hub now sources from arbitrum-litro, and carries
  `latest-<variant>` across when the source alias still names a version being
  mirrored. Without this the public repository has version tags but no
  `latest-*`, which is what the action and bake resolve by default.
- Ref resolution, the taken-tag guard and the credential check move into
  scripts/ci/resolve-publish-refs.mjs, built on buildTestnodeImageRef so a
  published tag is always one a consumer resolves. It now runs before the
  snapshot rather than after, so a taken tag fails in a minute.
- The image carries org.opencontainers.image.source, which is what grants this
  repository's workflows access to the private package.
- snapshot bake defaulted to ghcr.io/offchainlabs/arbitrum-testnode-ci, a
  package that has no `latest-*` tags; it now resolves the shared default.
@douglance
douglance merged commit cc20d35 into main Aug 21, 2026
7 checks passed
@douglance
douglance deleted the ci/private-first-publish branch August 21, 2026 21:01
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