From f98a67dd901c11f5f421b5703e3b40450bd87ca1 Mon Sep 17 00:00:00 2001 From: LinYushen Date: Wed, 22 Apr 2026 18:28:54 +0800 Subject: [PATCH] ci(release): build docker images natively per arch and merge manifests (#1507) Multi-arch images were built on a single amd64 runner with QEMU emulating arm64. The Next.js build (Dockerfile.web) under emulation took 30+ minutes per release and was the long pole of the workflow. Split each image build across two native runners (amd64 on ubuntu-latest, arm64 on ubuntu-24.04-arm), push by digest, then merge into a manifest list with docker buildx imagetools. QEMU is no longer needed. Backend and web each become a (matrix build + merge) pair, replacing the previous single docker-images job. Per-platform GHA cache scopes avoid cross-arch cache eviction. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/release.yml | 231 ++++++++++++++++++++++++++++------ 1 file changed, 192 insertions(+), 39 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7b30cd6df..8eaa5959b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -78,18 +78,38 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }} - docker-images: + # Multi-arch images are built natively per platform on dedicated runners + # (amd64 on ubuntu-latest, arm64 on ubuntu-24.04-arm) and merged into a + # manifest list. This avoids QEMU emulation, which was making the Next.js + # arm64 build run for 30+ minutes per release. + docker-backend-build: needs: verify - runs-on: ubuntu-latest - concurrency: - group: release-docker-images-${{ github.ref }} - cancel-in-progress: true + strategy: + fail-fast: false + matrix: + include: + - platform: linux/amd64 + runs-on: ubuntu-latest + - platform: linux/arm64 + runs-on: ubuntu-24.04-arm + runs-on: ${{ matrix.runs-on }} steps: + - name: Prepare + run: | + platform=${{ matrix.platform }} + echo "PLATFORM_PAIR=${platform//\//-}" >> "$GITHUB_ENV" + - name: Checkout uses: actions/checkout@v4 - - name: Setup QEMU - uses: docker/setup-qemu-action@v3 + - name: Compute backend image labels + id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/${{ github.repository_owner }}/multica-backend + labels: | + org.opencontainers.image.title=Multica Backend + org.opencontainers.image.description=Multica self-hosted backend - name: Setup Docker Buildx uses: docker/setup-buildx-action@v3 @@ -101,8 +121,55 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + - name: Build and push by digest + id: build + uses: docker/build-push-action@v6 + with: + context: . + file: Dockerfile + pull: true + platforms: ${{ matrix.platform }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha,scope=release-backend-${{ env.PLATFORM_PAIR }} + cache-to: type=gha,mode=max,scope=release-backend-${{ env.PLATFORM_PAIR }} + build-args: | + VERSION=${{ needs.verify.outputs.tag_name }} + COMMIT=${{ github.sha }} + outputs: type=image,name=ghcr.io/${{ github.repository_owner }}/multica-backend,push-by-digest=true,name-canonical=true,push=true + + - name: Export digest + run: | + mkdir -p /tmp/digests + digest="${{ steps.build.outputs.digest }}" + touch "/tmp/digests/${digest#sha256:}" + + - name: Upload digest + uses: actions/upload-artifact@v4 + with: + name: digests-backend-${{ env.PLATFORM_PAIR }} + path: /tmp/digests/* + if-no-files-found: error + retention-days: 1 + + docker-backend-merge: + needs: [verify, docker-backend-build] + runs-on: ubuntu-latest + concurrency: + group: release-docker-backend-${{ github.ref }} + cancel-in-progress: true + steps: + - name: Download digests + uses: actions/download-artifact@v4 + with: + path: /tmp/digests + pattern: digests-backend-* + merge-multiple: true + + - name: Setup Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Compute backend image tags - id: meta_backend + id: meta uses: docker/metadata-action@v5 with: images: ghcr.io/${{ github.repository_owner }}/multica-backend @@ -112,28 +179,114 @@ jobs: type=raw,value=latest,enable=${{ needs.verify.outputs.is_stable == 'true' }} type=raw,value=${{ needs.verify.outputs.tag_name }} type=sha,prefix=sha- - labels: | - org.opencontainers.image.title=Multica Backend - org.opencontainers.image.description=Multica self-hosted backend - - name: Build and push backend image + - name: Login to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Create manifest list and push + working-directory: /tmp/digests + run: | + docker buildx imagetools create \ + $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ + $(printf 'ghcr.io/${{ github.repository_owner }}/multica-backend@sha256:%s ' *) + + - name: Inspect image + run: | + docker buildx imagetools inspect \ + ghcr.io/${{ github.repository_owner }}/multica-backend:${{ steps.meta.outputs.version }} + + docker-web-build: + needs: verify + strategy: + fail-fast: false + matrix: + include: + - platform: linux/amd64 + runs-on: ubuntu-latest + - platform: linux/arm64 + runs-on: ubuntu-24.04-arm + runs-on: ${{ matrix.runs-on }} + steps: + - name: Prepare + run: | + platform=${{ matrix.platform }} + echo "PLATFORM_PAIR=${platform//\//-}" >> "$GITHUB_ENV" + + - name: Checkout + uses: actions/checkout@v4 + + - name: Compute web image labels + id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/${{ github.repository_owner }}/multica-web + labels: | + org.opencontainers.image.title=Multica Web + org.opencontainers.image.description=Multica self-hosted web frontend + + - name: Setup Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push by digest + id: build uses: docker/build-push-action@v6 with: context: . - file: Dockerfile + file: Dockerfile.web pull: true - push: true - platforms: linux/amd64,linux/arm64 - labels: ${{ steps.meta_backend.outputs.labels }} - tags: ${{ steps.meta_backend.outputs.tags }} - cache-from: type=gha,scope=release-backend - cache-to: type=gha,mode=max,scope=release-backend + platforms: ${{ matrix.platform }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha,scope=release-web-${{ env.PLATFORM_PAIR }} + cache-to: type=gha,mode=max,scope=release-web-${{ env.PLATFORM_PAIR }} build-args: | - VERSION=${{ needs.verify.outputs.tag_name }} - COMMIT=${{ github.sha }} + REMOTE_API_URL=http://backend:8080 + NEXT_PUBLIC_APP_VERSION=${{ needs.verify.outputs.tag_name }} + outputs: type=image,name=ghcr.io/${{ github.repository_owner }}/multica-web,push-by-digest=true,name-canonical=true,push=true + + - name: Export digest + run: | + mkdir -p /tmp/digests + digest="${{ steps.build.outputs.digest }}" + touch "/tmp/digests/${digest#sha256:}" + + - name: Upload digest + uses: actions/upload-artifact@v4 + with: + name: digests-web-${{ env.PLATFORM_PAIR }} + path: /tmp/digests/* + if-no-files-found: error + retention-days: 1 + + docker-web-merge: + needs: [verify, docker-web-build] + runs-on: ubuntu-latest + concurrency: + group: release-docker-web-${{ github.ref }} + cancel-in-progress: true + steps: + - name: Download digests + uses: actions/download-artifact@v4 + with: + path: /tmp/digests + pattern: digests-web-* + merge-multiple: true + + - name: Setup Docker Buildx + uses: docker/setup-buildx-action@v3 - name: Compute web image tags - id: meta_web + id: meta uses: docker/metadata-action@v5 with: images: ghcr.io/${{ github.repository_owner }}/multica-web @@ -143,25 +296,25 @@ jobs: type=raw,value=latest,enable=${{ needs.verify.outputs.is_stable == 'true' }} type=raw,value=${{ needs.verify.outputs.tag_name }} type=sha,prefix=sha- - labels: | - org.opencontainers.image.title=Multica Web - org.opencontainers.image.description=Multica self-hosted web frontend - - name: Build and push web image - uses: docker/build-push-action@v6 + - name: Login to GHCR + uses: docker/login-action@v3 with: - context: . - file: Dockerfile.web - pull: true - push: true - platforms: linux/amd64,linux/arm64 - labels: ${{ steps.meta_web.outputs.labels }} - tags: ${{ steps.meta_web.outputs.tags }} - cache-from: type=gha,scope=release-web - cache-to: type=gha,mode=max,scope=release-web - build-args: | - REMOTE_API_URL=http://backend:8080 - NEXT_PUBLIC_APP_VERSION=${{ needs.verify.outputs.tag_name }} + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Create manifest list and push + working-directory: /tmp/digests + run: | + docker buildx imagetools create \ + $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ + $(printf 'ghcr.io/${{ github.repository_owner }}/multica-web@sha256:%s ' *) + + - name: Inspect image + run: | + docker buildx imagetools inspect \ + ghcr.io/${{ github.repository_owner }}/multica-web:${{ steps.meta.outputs.version }} # Build the Desktop installers for Linux and Windows and upload them to # the GitHub Release that the `release` job above just published. macOS