Skip to content

ci: build each architecture on its own native runner - #88

Merged
askides merged 1 commit into
mainfrom
ci/native-arm-runners
Aug 5, 2026
Merged

ci: build each architecture on its own native runner#88
askides merged 1 commit into
mainfrom
ci/native-arm-runners

Conversation

@askides

@askides askides commented Aug 5, 2026

Copy link
Copy Markdown
Owner

The publish job built linux/amd64 and linux/arm64 together on a single
amd64 runner, so the arm64 half ran under QEMU. Measured on this repository:

Build Time
PR gate, amd64 only 1m58s
main publish, amd64 + arm64 emulated 33 min and counting

What gets emulated is pnpm install and the Vite build, translated instruction
by instruction. For reference, the same Dockerfile builds cold in 38s natively.

This repository is public, so ubuntu-24.04-arm runners are free. Each
architecture now builds natively on its own runner, concurrently.

How it works

publish becomes a two-entry matrix. Neither leg tags anything —
push-by-digest=true uploads an untagged image and returns its digest, which
travels to the new merge job as an artifact. merge assembles both digests
into one tagged manifest list.

Tagging per leg would have let whichever finished last overwrite the other,
leaving latest resolvable to only one architecture. merge therefore
inspects the pushed list and fails if both architectures are not on it
that being the exact failure this shape exists to prevent.

Cache scopes are per architecture (publish-amd64 / publish-arm64); a shared
scope would have the two legs evict each other's layers and start cold on every
run. Tag selection is unchanged, still keyed off release_created.

Also

Drops --filter tracker... from the Dockerfile's install. tracker is a
workspace dependency of web, so --filter web... already selects it.

Verified locally

  • Image builds cold in 31s and is unchanged at 590MB
  • tracker.js is present at 5114 bytes, identical to a local build — so the
    tracker really is still being built
  • Container boots and serves GET /signin with HTTP 200

I also tested replacing the prod-deps stage with pnpm deploy --prod, which
pnpm's Docker docs recommend for monorepos. It produces an image that cannot
boot
: pnpm deploy follows publish semantics and honours .gitignore, where
apps/web/.gitignore lists /build/, so the build output is silently omitted.
It was larger too (619MB). Not pursued — noting it so nobody else tries.

The publish job built linux/amd64 and linux/arm64 together on one amd64
runner, which meant the arm64 half ran under QEMU — and it was not a small
tax. The same Dockerfile finishes in 1m58s for amd64 alone and had not
finished after 25 minutes once arm64 joined it, because what gets emulated
is `pnpm install` and the Vite build, instruction by instruction.

The repository is public, so ubuntu-24.04-arm runners are free. Each
architecture now builds natively on its own runner, concurrently.

Neither leg tags anything. push-by-digest uploads an untagged image and
returns a digest; a merge job assembles the two digests into one tagged
manifest list. Tagging per leg would have let whichever finished last
overwrite the other, leaving `latest` pointing at a single architecture.
The merge job inspects the result and fails if both are not on it, since
that is the failure this shape exists to prevent.

Cache scopes are per architecture: sharing one would have the two legs
evict each other's layers and start cold every run.

Also drops `--filter tracker...` from the Dockerfile's install. tracker is a
workspace dependency of web, so `--filter web...` already selects it —
verified by building and booting the image, which serves /signin with a 200
and carries the same 5114-byte tracker.js at the same 590MB.
@askides
askides merged commit ae2b391 into main Aug 5, 2026
7 checks passed
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