From a4c7475eed3e5996a696190deb41199a54624ab9 Mon Sep 17 00:00:00 2001 From: Mateusz Date: Mon, 27 Jul 2026 22:41:54 +0200 Subject: [PATCH] fix(ci): build multi-arch images on native runners (drop QEMU) The arm64 leg was emulated via QEMU on an amd64 runner and crashed with "qemu: uncaught target signal 4 (Illegal instruction)" on the newer Node/V8 in the build stage, regardless of the pinned QEMU version. The prod cluster is arm64, so this leg must succeed. Build each architecture on its own native runner (amd64 on ubuntu-latest, arm64 on ubuntu-24.04-arm), push by digest, and assemble the manifest list in a merge job. No emulation means the illegal-instruction failure is impossible, and builds are much faster. Applied to both the dev image and release image workflows. --- .github/workflows/dev.yml | 92 +++++++++++++++++++++++------- .github/workflows/docker.yml | 107 ++++++++++++++++++++++++++--------- 2 files changed, 154 insertions(+), 45 deletions(-) diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 6177d18d..34de0403 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -52,12 +52,26 @@ jobs: - run: deno task migrate:tests - run: deno task check + # Build each architecture on its own native runner (no QEMU emulation) and + # push by digest; the manifest list is assembled in merge-image. build-image: needs: tests - runs-on: ubuntu-latest permissions: write-all - + strategy: + fail-fast: false + matrix: + include: + - platform: linux/amd64 + runner: ubuntu-latest + - platform: linux/arm64 + runner: ubuntu-24.04-arm + runs-on: ${{ matrix.runner }} steps: + - name: Prepare + run: | + platform=${{ matrix.platform }} + echo "PLATFORM_PAIR=${platform//\//-}" >> "$GITHUB_ENV" + - name: Checkout repository uses: actions/checkout@v4 @@ -68,10 +82,53 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build and push by digest + id: build + uses: docker/build-push-action@v6 + with: + context: . + platforms: ${{ matrix.platform }} + build-args: | + APP_VERSION=build-${{ github.run_number }} + cache-from: type=gha,scope=${{ env.PLATFORM_PAIR }} + cache-to: type=gha,mode=max,scope=${{ env.PLATFORM_PAIR }} + outputs: type=image,name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true + + - name: Export digest + 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: - image: tonistiigi/binfmt:qemu-v9.2.2 + name: digests-${{ env.PLATFORM_PAIR }} + path: ${{ runner.temp }}/digests/* + if-no-files-found: error + retention-days: 1 + + merge-image: + needs: build-image + runs-on: ubuntu-latest + permissions: write-all + steps: + - name: Download digests + uses: actions/download-artifact@v4 + with: + path: ${{ runner.temp }}/digests + pattern: digests-* + merge-multiple: true + + - name: Log in to the Container registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 @@ -81,21 +138,18 @@ jobs: uses: docker/metadata-action@v5 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=raw,value=build-${{ github.run_number }} - - name: Build and push Docker image - id: push - uses: docker/build-push-action@v6 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - context: . - push: true - platforms: linux/amd64,linux/arm64 - tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:build-${{ github.run_number }} - labels: ${{ steps.meta.outputs.labels }} - build-args: | - APP_VERSION=build-${{ github.run_number }} - cache-from: type=gha - cache-to: type=gha,mode=max + - name: Create manifest list and push + working-directory: ${{ runner.temp }}/digests + run: | + docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ + $(printf '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *) + + - name: Inspect image + run: | + docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:build-${{ github.run_number }} - run: echo '### Output image' >> $GITHUB_STEP_SUMMARY - run: echo '```\n${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:build-${{ github.run_number }}\n```\n' >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index de31c7d1..97dddde9 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -53,16 +53,31 @@ jobs: - run: deno task check + # Build each architecture on its own native runner (no QEMU emulation) and + # push by digest; the manifest list with the release tags is assembled in + # merge-and-push-image. build-and-push-image: needs: tests - runs-on: ubuntu-latest permissions: contents: read packages: write attestations: write id-token: write - + strategy: + fail-fast: false + matrix: + include: + - platform: linux/amd64 + runner: ubuntu-latest + - platform: linux/arm64 + runner: ubuntu-24.04-arm + runs-on: ${{ matrix.runner }} steps: + - name: Prepare + run: | + platform=${{ matrix.platform }} + echo "PLATFORM_PAIR=${platform//\//-}" >> "$GITHUB_ENV" + - name: Checkout repository uses: actions/checkout@v4 @@ -72,10 +87,61 @@ jobs: username: ${{ env.DOCKERHUB_USER }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build and push by digest + id: build + uses: docker/build-push-action@v6 with: - image: tonistiigi/binfmt:qemu-v9.2.2 + context: . + platforms: ${{ matrix.platform }} + build-args: | + APP_VERSION=${{ inputs.version || github.ref_name }} + cache-from: type=gha,scope=${{ env.PLATFORM_PAIR }} + cache-to: type=gha,mode=max,scope=${{ env.PLATFORM_PAIR }} + outputs: type=image,name=${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true + + - name: Generate artifact attestation + uses: actions/attest-build-provenance@v2 + with: + subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + subject-digest: ${{ steps.build.outputs.digest }} + push-to-registry: true + + - name: Export digest + 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: + name: digests-${{ env.PLATFORM_PAIR }} + path: ${{ runner.temp }}/digests/* + if-no-files-found: error + retention-days: 1 + + merge-and-push-image: + needs: build-and-push-image + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - name: Download digests + uses: actions/download-artifact@v4 + with: + path: ${{ runner.temp }}/digests + pattern: digests-* + merge-multiple: true + + - name: Log in to the Container registry + uses: docker/login-action@v3 + with: + username: ${{ env.DOCKERHUB_USER }} + password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 @@ -89,24 +155,13 @@ jobs: type=ref,event=tag type=raw,value=${{ inputs.version }},enable=${{ inputs.version != '' }} - - name: Build and push Docker image - id: push - uses: docker/build-push-action@v6 - with: - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - github-token: ${{ secrets.GITHUB_TOKEN }} - context: . - push: true - platforms: linux/amd64,linux/arm64 - build-args: | - APP_VERSION=${{ inputs.version || github.ref_name }} - cache-from: type=gha - cache-to: type=gha,mode=max - - - name: Generate artifact attestation - uses: actions/attest-build-provenance@v2 - with: - subject-name: ${{env.REGISTRY}}/${{ env.IMAGE_NAME}} - subject-digest: ${{ steps.push.outputs.digest }} - push-to-registry: true + - name: Create manifest list and push + working-directory: ${{ runner.temp }}/digests + run: | + docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ + $(printf '${{ env.IMAGE_NAME }}@sha256:%s ' *) + + - name: Inspect image + run: | + tag=$(jq -cr '.tags[0]' <<< "$DOCKER_METADATA_OUTPUT_JSON") + docker buildx imagetools inspect "$tag"