fix(ci): docker-publish reliable on main-branch (drop qemu-arm64)

The main-branch build hang since v1.4.14 was qemu-arm64 SIGILL-crashing
Next.js static-page-generation workers — `Next.js build worker exited
with code: null and signal: SIGILL` at 64-66/86 pages, every time
under load. V8's optimising tier emits CPU instructions that qemu's
user-mode emulation cannot reliably translate, and Node 22 + Alpine
3.21 hit it deterministically. Tag-builds appeared to "work" only
because they sometimes finished the static phase before the crash
window; main-branch builds rarely got that lucky and looked like
hangs.

Marc's prod target is x86_64 (Coolify on apps-01, every other
self-hosted HealthLog we know of). The arm64 image was a courtesy
build for OSS users, not production. Dropping it removes the qemu
flake completely and brings build time from 17-30 min back to ~12 min
typical.

v1.5 plan (`docs/audit/v1416-auto-deploy-fix.md`): re-add arm64 via a
matrix that uses GitHub-hosted `ubuntu-24.04-arm` runners (free for
public repos, native arm64, no qemu). The current concurrency-group +
per-ref gha cache scope from v1.4.15 C3 stays in place.
This commit is contained in:
Marc-André Bombeck committed 2026-05-10 00:34:33 +02:00
1 parent 4be6465e6c
commit cc0f3436ff
1 file changed
+30 -29
+30 -29
View File
@@ -13,22 +13,20 @@ env:
IMAGE_NAME: ${{ github.repository }}
# Cancel in-progress runs when a new commit is pushed to the same ref.
# main + a v* tag at the same SHA are different refs, so the per-ref
# gha cache scope below keeps them from contending on cache writes.
concurrency:
group: docker-publish-${{ github.ref }}
cancel-in-progress: true
jobs:
build-and-push:
name: Build & push multi-arch image
name: Build & push image
runs-on: ubuntu-latest
# 30 min wall clock — a healthy multi-arch build finishes inside 17 min
# (see v1.4.7..v1.4.13 history). The v1.4.14 main run hung 47 min on
# `[linux/amd64 runner 2/14]` before Marc cancelled because tag + branch
# were exporting to the same gha cache scope concurrently (see fix below).
# 30 min is enough for a clean build + a slow exporter, while still
# rescuing the runner if buildkit deadlocks instead of waiting for a
# human to notice.
timeout-minutes: 30
# 25 min wall clock — a healthy amd64-only build finishes inside 12 min.
# The qemu-arm64 path that was hanging at 47 min on `[linux/amd64 runner
# 2/14]` is gone; see commit message for v1.4.16 C5 rationale below.
timeout-minutes: 25
permissions:
contents: read
packages: write
@@ -37,9 +35,6 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
- name: Set up QEMU (multi-arch)
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
@@ -71,29 +66,35 @@ jobs:
org.opencontainers.image.licenses=AGPL-3.0-only
org.opencontainers.image.source=https://github.com/${{ github.repository }}
# Per-ref gha cache scope: previously both `cache-from: type=gha` and
# `cache-to: type=gha,mode=max` used buildx's default scope name
# (`buildkit`). When a release commit lands, GitHub fires the workflow
# twice in parallel — once for the `main` push, once for the `v*` tag
# — both pointed at the same SHA, both writing to the same gha cache
# key. mode=max exports every stage of every platform; the concurrent
# writers race on the cache manifest and one deadlocks. The v1.4.14
# main run sat on `[linux/amd64 runner 2/14] RUN apk add tzdata` for
# 47 minutes (cancelled by hand) while the tag run on the same SHA
# finished cleanly in 17 minutes — clear smoking-gun.
# v1.4.16 C5 — drop linux/arm64 from the multi-arch build.
#
# Splitting the scope by `github.ref_name` (e.g. `main`, `v1.4.15`,
# `pr-123`) gives each invocation its own cache slot. We `cache-from`
# both the ref-specific scope AND the canonical `main` scope so:
# - feature branches/tags can warm-start off main's last good build
# - a tag built right after a main push hits main's cache for layer
# reuse without contending on writes
# Root cause of the main-branch hang since v1.4.14: the qemu-arm64
# path (`docker/setup-qemu-action` + `tonistiigi/binfmt`) was
# SIGILL-crashing Next.js's static-page-generation workers
# (`Next.js build worker exited with code: null and signal: SIGILL`
# at 64-66/86 pages). V8's optimising tier emits CPU instructions
# that qemu's user-mode emulation cannot reliably translate under
# heavy load, and Node 22 + Alpine 3.21 hit it deterministically.
# Tag-builds appeared to "work" only because they happened to
# finish their static pages before the crash window; main-branch
# builds rarely got that lucky.
#
# Marc's prod target is x86_64 only (Coolify on apps-01, all other
# self-hosted instances we know of); the arm64 image was a
# courtesy build for OSS users, not production. v1.5 will re-add
# arm64 via a matrix that uses the GitHub-hosted `ubuntu-24.04-arm`
# runner (free for public repos, native arm64, no qemu) — see
# `docs/audit/v1416-auto-deploy-fix.md` for the broader CI plan.
#
# Per-ref gha cache scope (kept from v1.4.15 C3): tag + main
# builds at the same SHA do not contend on writes; tag warm-starts
# off main's last good build for layer reuse.
- name: Build & push image
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm64
platforms: linux/amd64
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}