0012. Native arm64 Release Builds on Hosted arm64 Runners¶
Status¶
Accepted. Amends ADR 0011 (replaces its "arm64 via QEMU" decision and the
multi-arch :buildcache tag; revisits its rejection of push-by-digest).
Context¶
ADR 0011 chose QEMU emulation for the arm64 half of release builds because GitHub-hosted arm64 runners were paid-only for private repositories, and "slow but rare" seemed acceptable at release cadence.
Two facts invalidated that decision during the v1.0.0-rc.1 rehearsal (2026-06-10):
- QEMU cannot build this image at all. The Dockerfile compiles Ruby
from source via mise/ruby-build (no precompiled Ruby exists for
musl/aarch64). Under QEMU the build ran
make -j 2for ~115 minutes and then failed (BUILD FAILED (Alpine Linux 3.23.4 on aarch64 using ruby-build)). Ruby's build executes its own freshly compiledminirubymid-build, a known QEMU-user failure mode. Python 3.14, a plain C compile, succeeded under the same emulation. Native arm64 (the Apple Silicon dev host) builds the same stage without issue. - The pricing constraint expired. Since 2026-01-29, arm64 standard
runners (
ubuntu-24.04-arm) are available in private repositories as standard GitHub-hosted runners, billed against the included minutes at a lower per-minute rate than x64.
Decision¶
The release push job becomes a two-leg matrix of native builds:
linux/amd64onubuntu-latest,linux/arm64onubuntu-24.04-arm.- Each leg builds with
provenance: falseand pushes by digest (no tags), exporting layer cache to a per-arch registry tag (:buildcache-amd64,:buildcache-arm64). - A
mergejob downloads both digests and runsdocker buildx imagetools createto publish a single manifest list under the semver tags (X.Y.Z/X.Y/X/latest, prereleases exact-only).
The multi-arch :buildcache tag from ADR 0011 is retired: per-arch jobs
writing per-arch cache tags cannot clobber each other, which was the only
reason the second tag existed. (ci.yml keeps reading/writing
:buildcache-amd64 unchanged. The orphaned :buildcache tag on Docker Hub
can be deleted manually.)
Alternatives considered:
- Keep QEMU, install Ruby from apk -- avoids the compile entirely, but unpins Ruby from mise/Renovate and ties its version to Alpine's package tree; image contents would diverge from the mise-managed convention for one runtime only.
- Self-hosted arm64 runner (e.g. the Apple Silicon host) -- free and native, but an always-on maintenance and security liability for a personal repo.
- Make the repo public for free arm64 runners -- out of scope as a side effect of a build fix.
Consequences¶
Easier:
- Release arm64 builds are native: minutes-scale instead of hours-scale, and immune to QEMU emulation bugs.
- ADR 0011's "QEMU release builds take hours" accepted cost is deleted.
- The released bytes are exactly the pushed digests (ADR 0011 accepted a cache-identical rebuild instead; push-by-digest also removes that caveat).
Harder / accepted costs:
- arm64 leg consumes paid-tier (but discounted) standard minutes from the monthly quota; at release cadence this is small against the ADR 0011 minutes budget.
- One more registry cache tag (
:buildcache-arm64) and the digest-merge plumbing (artifact handoff between jobs) inrelease.yml. - The amd64 image that ships is built in the
pushleg, not thebuild-testjob; it resolves from the same:buildcache-amd64the test job just wrote, so the cache-identical caveat from ADR 0011 still applies to the relationship between tested and shipped amd64 bytes.