ci(image): publish amd64 and arm64, each built natively - #7
Merged
Conversation
The published image was amd64-only. That does not fail on Apple Silicon; it does something quieter, which is to pull the amd64 image and run the whole thing under QEMU. An interrogator that enumerates a state space is exactly the program that turns emulation into a wait, so "published" and "usable by other people" came apart with no error message anywhere. image.yml becomes two jobs. A matrix builds each architecture on a runner of that architecture and pushes it by digest under no tag; a second job joins the digests with `imagetools create` and attaches the version and `latest` once. Tagging inside the matrix would be two jobs racing for one tag, and the loser's architecture would be the published image — the same bug, harder to see. Setting `platforms: linux/amd64,linux/arm64` on one runner is a line rather than a job, and it compiles OCaml for arm64 under QEMU on an amd64 host: the emulation moved from the user to CI, where the stage-2 smoke checks run emulated too. arm64 runners are free on public repositories. The publish gains the check that only a manifest list needs — that the tag names both architectures. Nothing else can catch a half-published tag, since an amd64-only image pulls and runs perfectly on the amd64 runner that would be testing it. image-check.yml matrixes for the reason it exists: a check that built only amd64 would leave arm64 to be found by the release. Its build cache is scoped per architecture, because one shared scope has the two jobs evict each other every run. Verified: ocaml/opam:debian-12-ocaml-5.2 and debian:12-slim both publish linux/arm64, and ubuntu-24.04-arm is a real label, free on public repos. The arm64 BUILD is not verified locally — this machine has no buildx and no binfmt, so arm64 cannot run here at all. The image-check matrix on this pull request is that verification, on real hardware. 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 subscribe to this conversation on GitHub.
Already have an account?
Sign in.
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.
Addresses downstream feedback: the published image is amd64-only, so anyone on Apple Silicon pulling
ghcr.io/writ-lang/writruns it under QEMU.That doesn't fail — it does something quieter. An interrogator that enumerates a state space is exactly the program that turns emulation into a wait, so "published" and "usable by other people" came apart with no error message anywhere.
The approach
A matrix builds each architecture on a runner of that architecture and merges the results into one manifest list — the option the reviewer recommended, over
platforms: linux/amd64,linux/arm64on a single runner.The one-liner would compile OCaml for arm64 under QEMU on an amd64 host, and run the Dockerfile's stage-2 smoke checks emulated too. That moves the emulation from the user to CI rather than removing it.
ubuntu-24.04-armis free on public repositories, so the honest version costs nothing.image.yml→ two jobs. The matrix pushes each architecture by digest, under no tag; a second job joins the digests withimagetools createand attaches:VERSIONand:latestonce. Tagging inside the matrix would be two jobs racing for one tag, and the loser's architecture would be the published image — the same bug, harder to see.image-check.yml→ matrixed, for the reason it exists at all: a check that built only amd64 would pass on the PR and leave arm64 to the release. Cache is scoped per architecture, since one shared scope has the two jobs evict each other every run (an arm64 layer is not a usable hit for an amd64 build).New check
The publish now asserts the tag names both platforms:
Nothing else can catch a half-published tag: an amd64-only image pulls and runs perfectly on the amd64 runner that would otherwise be "testing" it. That is precisely how this gap survived the first publish.
Verification
Confirmed by querying the registries directly:
ocaml/opam:debian-12-ocaml-5.2has arm64linux/arm64(also amd64, ppc64le)debian:12-slimhas arm64linux/arm64 v8ubuntu-24.04-armis a real label, free on public reposThe arm64 build itself is not verified locally — this machine has no buildx and no binfmt, and
docker run --platform linux/arm64dies withexec format error, so arm64 cannot run here at all. Theimage-check (arm64)job on this PR is that verification, on real hardware rather than emulation.Not addressed, deliberately
The reviewer's second paragraph —
:latestpublished alongside0.1.0, and the client repo pinning the version becausemake formal's recorded output is a claim about a specificwrit— reads as confirmation rather than a request. Both tags are still published; no change.🤖 Generated with Claude Code