feat: mirror-image reusable workflow - #23
Merged
Merged
Conversation
Registry throughput varies enormously by peering, and a badly peered one is not
merely slow — it fails. Measured from one site against the same 926MB image:
ghcr.io 192 MB/s, docker.io 86, quay.io 79, registry.k8s.io 20, and
mcr.microsoft.com 2.2. That one registry was 391s of a ~640s CI job, and its
sibling CDN blew a 30s client timeout under load and failed builds outright.
Mirroring took the job to ~80s.
Lifted from cshuttle/nmon, generalised: source, destination, tag, platform and
runner are all inputs, and nothing estate-specific is baked in — this repo is
public.
Three things it does that a naive version gets wrong, each learned the hard way:
- STAGES TO DISK rather than `crane copy`. A streamed copy holds the upload
open for the whole download; against a slow source the destination cancels
it (`stream ID 5; CANCEL; received from peer`) after minutes of work.
- SKIPS when already current, comparing LAYER digests. The push adds an OCI
source label, so the mirror's manifest and config digests never equal
upstream's even when content is identical — a manifest comparison would
re-copy every run, which for the weekly re-sync means a 7-minute pull for
nothing.
- Pins --platform. crane defaults to `all`, and mirroring an unused
architecture doubles the bytes over exactly the leg being avoided.
Layer extraction is sed/grep/tr, not python or jq: the minimal ARC runner image
ships neither reliably (this repo's own selftest already works around the
missing PyYAML). `tr -d` before `sed` is load-bearing — crane pretty-prints, so
a line-based sed leaves the CONFIG digest in the comparison and silently
compares the wrong thing. Caught by diffing it against a JSON parser rather
than trusting it.
Verified locally against the live registries: current tag takes the skip path,
absent tag takes the copy path with no stderr noise, and the shell extraction
matches a JSON parse exactly. actionlint clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Lifts the image-mirror recipe out of
cshuttle/nmonand generalises it. Everything estate-specific is an input — this repo is public, so no hostnames or registry choices are baked in.Why
Registry throughput varies enormously by peering, and a badly peered one is not merely slow — it fails. Measured from one site against the same 926MB image:
That one registry was 391s of a ~640s CI job, and its sibling CDN blew a 30s client timeout under load and failed builds outright. Mirroring took the job to ~80s.
The README tells adopters to measure first — a mirror only helps if the pull is genuinely the bottleneck. Extraction was 8s of that 400s, and raising runner concurrency against a starved path makes it worse.
Three things a naive version gets wrong
crane copy. A streamed copy holds the upload open for the whole download; against a slow source the destination cancels it (stream ID 5; CANCEL; received from peer) after minutes of work.--platform.cranedefaults toall; mirroring an unused architecture doubles the bytes over exactly the leg being avoided.Implementation note
Layer extraction is
sed/grep/tr, not python or jq — the minimal ARC runner image ships neither reliably, as this repo's own selftest already documents for PyYAML.tr -dbeforesedis load-bearing:cranepretty-prints, so a line-basedsedleaves the config digest in the comparison and silently compares the wrong thing. I caught that by diffing the shell version against a JSON parser rather than trusting it — it would have re-copied on every run while looking correct.Verification
Against the live registries: current tag → skip path, absent tag → copy path with no stderr noise, and the shell extraction matches a JSON parse exactly.
actionlint -shellcheck=clean.🤖 Generated with Claude Code