From 7f12488da514c68dacbe2311ca1b36fed435d501 Mon Sep 17 00:00:00 2001 From: Jonas Grosch Date: Wed, 30 Sep 2026 21:15:28 +0200 Subject: [PATCH] ci: publish a multi-arch container image to GHCR Builds the Containerfile for linux/amd64 and linux/arm64, each natively on a runner of its own architecture (no emulation), and merges both into one tag under ghcr.io//. The repository's default branch publishes latest, and sha-; version tags v* publish and .. Pull requests that touch the build inputs build both architectures without pushing, so a broken Containerfile shows before merge. Other branch pushes do nothing. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/image.yml | 119 ++++++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 .github/workflows/image.yml diff --git a/.github/workflows/image.yml b/.github/workflows/image.yml new file mode 100644 index 00000000..ded20d38 --- /dev/null +++ b/.github/workflows/image.yml @@ -0,0 +1,119 @@ +# Publish the walgit container image (Containerfile) to GitHub Container Registry as +# ghcr.io//, for linux/amd64 and linux/arm64. Each architecture builds +# natively on a runner of its own architecture — no emulation — and the two are merged +# into one multi-arch tag. +# +# Publishes the repository's default branch (tags: latest, , sha-) and +# version tags v* (tags: , .). Pull requests build both +# architectures without pushing, so a broken Containerfile shows up before merge. +name: image + +on: + push: + branches: ["**"] + tags: ["v*"] + pull_request: + paths: [Containerfile, Cargo.toml, Cargo.lock, rust-toolchain.toml, "crates/**", "web/**", .github/workflows/image.yml] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: image-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +env: + IMAGE: ghcr.io/${{ github.repository }} + PUBLISH: ${{ github.event_name != 'pull_request' && (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) || startsWith(github.ref, 'refs/tags/v')) }} + +jobs: + build: + # Branch pushes other than the default branch have nothing to publish. + if: github.event_name != 'push' || github.ref == format('refs/heads/{0}', github.event.repository.default_branch) || startsWith(github.ref, 'refs/tags/v') + strategy: + fail-fast: true + matrix: + include: + - { platform: linux/amd64, runner: ubuntu-24.04 } + - { platform: linux/arm64, runner: ubuntu-24.04-arm } + runs-on: ${{ matrix.runner }} + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v5 + - uses: docker/setup-buildx-action@v3 + - uses: docker/login-action@v3 + if: env.PUBLISH == 'true' + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.IMAGE }} + - id: build + uses: docker/build-push-action@v6 + with: + context: . + file: Containerfile + platforms: ${{ matrix.platform }} + build-args: WALGIT_BUILD_SHA=${{ github.sha }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha,scope=${{ matrix.platform }} + cache-to: type=gha,mode=max,scope=${{ matrix.platform }} + outputs: ${{ env.PUBLISH == 'true' && format('type=image,name={0},push-by-digest=true,name-canonical=true,push=true', env.IMAGE) || 'type=cacheonly' }} + - name: Record the digest + if: env.PUBLISH == 'true' + run: | + mkdir -p "${{ runner.temp }}/digests" + d="${{ steps.build.outputs.digest }}" + touch "${{ runner.temp }}/digests/${d#sha256:}" + - uses: actions/upload-artifact@v4 + if: env.PUBLISH == 'true' + with: + name: digest-${{ strategy.job-index }} + path: ${{ runner.temp }}/digests/* + if-no-files-found: error + retention-days: 1 + + publish: + needs: build + if: github.event_name != 'pull_request' && (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) || startsWith(github.ref, 'refs/tags/v')) + runs-on: ubuntu-24.04 + permissions: + contents: read + packages: write + steps: + - uses: actions/download-artifact@v5 + with: + pattern: digest-* + merge-multiple: true + path: ${{ runner.temp }}/digests + - uses: docker/setup-buildx-action@v3 + - uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.IMAGE }} + tags: | + type=raw,value=latest,enable={{is_default_branch}} + type=ref,event=branch + type=sha,prefix=sha- + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + - name: Merge the architectures into one tag + working-directory: ${{ runner.temp }}/digests + run: | + mapfile -t tags < <(jq -r '.tags[] | "-t", .' <<< "$DOCKER_METADATA_OUTPUT_JSON") + sources=() + for d in *; do sources+=("${IMAGE}@sha256:${d}"); done + docker buildx imagetools create "${tags[@]}" "${sources[@]}" + - name: Inspect + run: docker buildx imagetools inspect "${{ env.IMAGE }}:sha-${GITHUB_SHA::7}"