ci: build release images on native arm64 runners, not QEMU

The release build emulated linux/arm64 with QEMU on the amd64 runner. A
runner/QEMU update started crashing 'npm ci' under emulation (illegal
instruction) and the build then hung for hours instead of failing, so
v0.17.0 never published. Build each arch on its native runner instead
(amd64 on ubuntu-24.04, arm64 on the free ubuntu-24.04-arm) and merge the
per-arch digests into one manifest with buildx imagetools. Add per-job
timeouts so a stuck build can never run for hours again.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Khalid Abdi
2026-07-20 02:12:27 +03:00
parent c5812511b5
commit 07cfd5d335
+104 -28
View File
@@ -3,6 +3,11 @@
# Trigger: push a semver tag, e.g. # Trigger: push a semver tag, e.g.
# git tag v0.1.0 && git push origin v0.1.0 # git tag v0.1.0 && git push origin v0.1.0
# #
# Multi-arch (amd64 + arm64) is built on NATIVE runners — amd64 on ubuntu-24.04,
# arm64 on ubuntu-24.04-arm — and merged into a manifest. We do NOT emulate arm64
# with QEMU anymore: a runner/QEMU update started crashing `npm ci` under
# emulation ("illegal instruction") and hanging the build for hours.
#
# The frontend image bakes NO API URL — it resolves the backend from the host # The frontend image bakes NO API URL — it resolves the backend from the host
# the browser uses at runtime — so one published image works for every clinic. # the browser uses at runtime — so one published image works for every clinic.
# #
@@ -18,27 +23,30 @@ on:
tags: tags:
- "v*.*.*" - "v*.*.*"
permissions:
contents: write # create the GitHub Release (the update check reads this)
env: env:
REGISTRY_NAMESPACE: khalidxv REGISTRY_NAMESPACE: khalidxv
jobs: jobs:
publish: # One build per (image × platform) on the platform's native runner, pushed to
runs-on: ubuntu-latest # Docker Hub by digest (no tag yet). The merge job stitches the per-arch
# digests into a single tagged multi-arch manifest.
build:
name: Build ${{ matrix.image }} (${{ matrix.platform }})
runs-on: ${{ matrix.platform == 'linux/arm64' && 'ubuntu-24.04-arm' || 'ubuntu-24.04' }}
timeout-minutes: 40
strategy:
fail-fast: false
matrix:
image: [backend, frontend]
platform: [linux/amd64, linux/arm64]
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Derive version from tag - name: Prepare platform pair
id: meta run: |
run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT" platform="${{ matrix.platform }}"
echo "PLATFORM_PAIR=${platform//\//-}" >> "$GITHUB_ENV"
# QEMU lets the amd64 runner emulate arm64 so the images below build for
# both platforms (Intel + Apple Silicon self-hosters).
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Buildx - name: Set up Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
@@ -49,25 +57,93 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build & push backend - name: Build & push by digest
id: build
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:
context: ./backend context: ./${{ matrix.image }}
push: true platforms: ${{ matrix.platform }}
platforms: linux/amd64,linux/arm64 provenance: false
tags: | outputs: type=image,name=${{ env.REGISTRY_NAMESPACE }}/temetro-${{ matrix.image }},push-by-digest=true,name-canonical=true,push=true
${{ env.REGISTRY_NAMESPACE }}/temetro-backend:${{ steps.meta.outputs.version }}
${{ env.REGISTRY_NAMESPACE }}/temetro-backend:latest
- name: Build & push frontend - name: Export digest
uses: docker/build-push-action@v6 run: |
mkdir -p "${{ runner.temp }}/digests"
digest="${{ steps.build.outputs.digest }}"
touch "${{ runner.temp }}/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@v4
with: with:
context: ./frontend name: digests-${{ matrix.image }}-${{ env.PLATFORM_PAIR }}
push: true path: ${{ runner.temp }}/digests/*
platforms: linux/amd64,linux/arm64 if-no-files-found: error
tags: | retention-days: 1
${{ env.REGISTRY_NAMESPACE }}/temetro-frontend:${{ steps.meta.outputs.version }}
${{ env.REGISTRY_NAMESPACE }}/temetro-frontend:latest # Combine the per-arch digests for each image into one multi-arch manifest and
# tag it (X.Y.Z + latest).
merge:
name: Merge ${{ matrix.image }} manifest
runs-on: ubuntu-latest
timeout-minutes: 15
needs: build
strategy:
fail-fast: false
matrix:
image: [backend, frontend]
steps:
- name: Derive version from tag
id: meta
run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
- name: Download digests
uses: actions/download-artifact@v4
with:
path: ${{ runner.temp }}/digests
pattern: digests-${{ matrix.image }}-*
merge-multiple: true
- name: Set up Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Create manifest list and push
working-directory: ${{ runner.temp }}/digests
env:
IMAGE: ${{ env.REGISTRY_NAMESPACE }}/temetro-${{ matrix.image }}
VERSION: ${{ steps.meta.outputs.version }}
run: |
docker buildx imagetools create \
-t "$IMAGE:$VERSION" \
-t "$IMAGE:latest" \
$(printf "$IMAGE@sha256:%s " *)
- name: Inspect
env:
IMAGE: ${{ env.REGISTRY_NAMESPACE }}/temetro-${{ matrix.image }}
VERSION: ${{ steps.meta.outputs.version }}
run: docker buildx imagetools inspect "$IMAGE:$VERSION"
# Both images are published — now cut the GitHub Release with the changelog.
release:
name: GitHub Release
runs-on: ubuntu-latest
timeout-minutes: 10
needs: merge
permissions:
contents: write # create the GitHub Release (the update check reads this)
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Derive version from tag
id: meta
run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
# Pull this version's section out of CHANGELOG.md so the release has real, # Pull this version's section out of CHANGELOG.md so the release has real,
# human-written notes (the auto "Full Changelog" link is still appended # human-written notes (the auto "Full Changelog" link is still appended