Skip to content

ci(image): publish amd64 and arm64, each built natively - #7

Merged
sajonaro merged 1 commit into
mainfrom
image-multiarch
Aug 24, 2026
Merged

ci(image): publish amd64 and arm64, each built natively#7
sajonaro merged 1 commit into
mainfrom
image-multiarch

Conversation

@sajonaro

Copy link
Copy Markdown
Contributor

Addresses downstream feedback: the published image is amd64-only, so anyone on Apple Silicon pulling ghcr.io/writ-lang/writ runs 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/arm64 on 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-arm is 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 with imagetools create and attaches :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.

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:

for want in linux/amd64 linux/arm64; do
  docker buildx imagetools inspect "$image" | grep -q "$want" || exit 1
done

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:

fact evidence
ocaml/opam:debian-12-ocaml-5.2 has arm64 manifest lists linux/arm64 (also amd64, ppc64le)
debian:12-slim has arm64 manifest lists linux/arm64 v8
ubuntu-24.04-arm is a real label, free on public repos GitHub Actions runner docs

The arm64 build itself is not verified locally — this machine has no buildx and no binfmt, and docker run --platform linux/arm64 dies with exec format error, so arm64 cannot run here at all. The image-check (arm64) job on this PR is that verification, on real hardware rather than emulation.

Not addressed, deliberately

The reviewer's second paragraph — :latest published alongside 0.1.0, and the client repo pinning the version because make formal's recorded output is a claim about a specific writ — reads as confirmation rather than a request. Both tags are still published; no change.

🤖 Generated with Claude Code

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>
@sajonaro
sajonaro merged commit 084c8d5 into main Aug 24, 2026
3 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Aug 24, 2026
@sajonaro
sajonaro deleted the image-multiarch branch August 24, 2026 10:32
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant