From 0f859139cc4065b3c410b33600446e6052a3f76b Mon Sep 17 00:00:00 2001 From: Hiroyuki Wada Date: Sat, 13 Jun 2026 15:41:36 +0900 Subject: [PATCH] ci: build multi-arch image on native runners instead of QEMU The release image build was the slow part of the release pipeline: it built linux/arm64 under QEMU emulation, and emulating a Rust release compile is extremely slow (the native per-arch binary builds finish in ~1-2 min, while the emulated docker arm64 layer can take many times longer). Rework the single `docker` job into the canonical native-runner pattern: - `docker-build`: a matrix that builds each architecture on its own native runner (linux/amd64 on ubuntu-latest, linux/arm64 on ubuntu-24.04-arm), pushing each by digest. No QEMU, so both archs compile at native speed. - `docker-merge`: assembles the multi-arch manifest list (semver + latest tags) from the per-arch digests with `docker buildx imagetools create`. GHA build cache is scoped per platform to avoid cross-arch collisions. The now-unused QEMU setup step is dropped. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/release.yml | 97 +++++++++++++++++++++++++++++------ 1 file changed, 80 insertions(+), 17 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0a81923..369eef7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -149,9 +149,20 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - docker: - name: Build and Push Docker Image - runs-on: ubuntu-latest + # Build each architecture natively (no QEMU emulation — emulating a Rust + # release compile is extremely slow) and push by digest. The merge job then + # assembles the multi-arch manifest from the per-arch digests. + docker-build: + name: Build Docker Image (${{ matrix.platform }}) + strategy: + fail-fast: false + matrix: + include: + - platform: linux/amd64 + runner: ubuntu-latest + - platform: linux/arm64 + runner: ubuntu-24.04-arm + runs-on: ${{ matrix.runner }} permissions: contents: read packages: write @@ -161,9 +172,6 @@ jobs: with: ref: ${{ github.event.release.tag_name }} - - name: Set up QEMU - uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3 - - name: Set up Docker Buildx uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 @@ -174,25 +182,80 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Extract image metadata + - name: Image labels id: meta uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5 with: images: ghcr.io/${{ github.repository }} - tags: | - type=semver,pattern={{version}},value=${{ github.event.release.tag_name }} - type=semver,pattern={{major}}.{{minor}},value=${{ github.event.release.tag_name }} - type=raw,value=latest - - name: Build and push image + - name: Build and push by digest + id: build uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6 with: context: . - platforms: linux/amd64,linux/arm64 - push: true - tags: ${{ steps.meta.outputs.tags }} + platforms: ${{ matrix.platform }} labels: ${{ steps.meta.outputs.labels }} build-args: | FEATURES=sqlite,postgresql - cache-from: type=gha - cache-to: type=gha,mode=max + cache-from: type=gha,scope=${{ matrix.platform }} + cache-to: type=gha,mode=max,scope=${{ matrix.platform }} + outputs: type=image,name=ghcr.io/${{ github.repository }},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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: digest-${{ matrix.platform == 'linux/amd64' && 'amd64' || 'arm64' }} + path: ${{ runner.temp }}/digests/* + if-no-files-found: error + retention-days: 1 + + docker-merge: + name: Merge Docker Manifest + needs: docker-build + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - name: Download digests + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + path: ${{ runner.temp }}/digests + pattern: digest-* + merge-multiple: true + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Image tags + id: meta + uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5 + with: + images: ghcr.io/${{ github.repository }} + tags: | + type=semver,pattern={{version}},value=${{ github.event.release.tag_name }} + type=semver,pattern={{major}}.{{minor}},value=${{ github.event.release.tag_name }} + type=raw,value=latest + + - name: Create and push manifest list + working-directory: ${{ runner.temp }}/digests + run: | + docker buildx imagetools create \ + $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ + $(printf 'ghcr.io/${{ github.repository }}@sha256:%s ' *) + + - name: Inspect image + run: docker buildx imagetools inspect ghcr.io/${{ github.repository }}:${{ steps.meta.outputs.version }}