From 3415b85713915c51c0e276387a97522061288dd2 Mon Sep 17 00:00:00 2001 From: Aarav Sharma Date: Sun, 13 Sep 2026 09:42:44 -0600 Subject: [PATCH 1/2] ci(docker): build arm64 image natively on a dedicated arm runner Each platform now builds on its own runner (ubuntu-latest for amd64, ubuntu-24.04-arm for arm64) and pushes its manifest to GHCR by digest; a merge job combines the two into a single multi-arch manifest list with the computed tags. This replaces the QEMU-emulated arm64 build, which dominated the workflow's ~3 minute runtime. --- .github/workflows/docker.yml | 194 ++++++++++++++++++++++++++++++++--- website/docker-bake.hcl | 6 ++ 2 files changed, 186 insertions(+), 14 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 82870cf..32eb632 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -1,6 +1,6 @@ name: Docker image -# Default permissions are least-privilege; the push job elevates to +# Default permissions are least-privilege; the push jobs elevate to # packages: write below. This satisfies zizmor's excessive-permissions check. permissions: contents: read @@ -30,16 +30,25 @@ env: # readability. IMAGE_NAME: ${{ github.repository_owner }}/synapse-website +# Each platform is built natively on its own runner (arm64 on GitHub's free +# arm runner instead of QEMU emulation on an amd64 runner) and pushed to +# GHCR by digest; the merge job then combines the two digests into a single +# multi-arch manifest list with the computed tags. +# +# Pattern documented at +# https://docs.docker.com/build/ci/github-actions/multi-platform/ jobs: - build: - name: Build & push image + build-amd64: + name: Build & push image (amd64) runs-on: ubuntu-latest # Publish only on direct pushes to main or on v* tags. PRs run the - # build-pr job (push: false, no cache-to) below. + # build-pr-* jobs below (no login, no push). if: github.event_name != 'pull_request' permissions: contents: read packages: write + outputs: + digest: ${{ steps.digest.outputs.digest }} steps: - name: Checkout uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -61,30 +70,162 @@ jobs: uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + # The computed tags are only applied by the merge job; this step + # exists so its OCI labels get baked into the pushed image. tags: | type=ref,event=branch type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=sha,format=short type=raw,value=latest,enable={{is_default_branch}} - # bake-target makes metadata-action emit a JSON file - # (steps.meta.outputs.bake-file) that overrides the same-named - # target in docker-bake.hcl with the computed tags + labels. + # bake-target makes metadata-action emit a JSON file that supplies + # the labels for the 'synapse-website' target. bake-target: synapse-website - - name: Build and push + - name: Build and push by digest + id: build uses: docker/bake-action@76cc8060bdff6d632a465001e4cf300684c5472c # v5.7.0 with: - # The third entry is the metadata-action-generated JSON; it - # supplies tags + labels for the 'synapse-website' target. + # The metadata file supplies the labels; the set overrides then + # replace its tags with the bare repository name, which is what + # push-by-digest requires (tags are attached by the merge job). files: | website/docker-bake.hcl ${{ steps.meta.outputs.bake-file }} targets: synapse-website - push: true + set: | + *.platform=linux/amd64 + *.tags=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + *.output=type=image,push-by-digest=true,name-canonical=true,push=true - build-pr: - name: Build (PR only) + - name: Record pushed digest + id: digest + env: + BAKE_METADATA: ${{ steps.build.outputs.metadata }} + run: | + digest="$(jq -r '.["synapse-website"]["containerimage.digest"]' <<<"$BAKE_METADATA")" + if [[ -z "$digest" || "$digest" == "null" ]]; then + echo "bake metadata is missing containerimage.digest" >&2 + exit 1 + fi + echo "digest=$digest" >> "$GITHUB_OUTPUT" + + build-arm64: + name: Build & push image (arm64) + runs-on: ubuntu-24.04-arm + if: github.event_name != 'pull_request' + permissions: + contents: read + packages: write + outputs: + digest: ${{ steps.digest.outputs.digest }} + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 + + - name: Log in to GHCR + uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=sha,format=short + type=raw,value=latest,enable={{is_default_branch}} + bake-target: synapse-website + + - name: Build and push by digest + id: build + uses: docker/bake-action@76cc8060bdff6d632a465001e4cf300684c5472c # v5.7.0 + with: + files: | + website/docker-bake.hcl + ${{ steps.meta.outputs.bake-file }} + targets: synapse-website + set: | + *.platform=linux/arm64 + *.tags=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + *.output=type=image,push-by-digest=true,name-canonical=true,push=true + + - name: Record pushed digest + id: digest + env: + BAKE_METADATA: ${{ steps.build.outputs.metadata }} + run: | + digest="$(jq -r '.["synapse-website"]["containerimage.digest"]' <<<"$BAKE_METADATA")" + if [[ -z "$digest" || "$digest" == "null" ]]; then + echo "bake metadata is missing containerimage.digest" >&2 + exit 1 + fi + echo "digest=$digest" >> "$GITHUB_OUTPUT" + + merge: + name: Merge manifests & push tags + runs-on: ubuntu-latest + needs: [build-amd64, build-arm64] + if: github.event_name != 'pull_request' + permissions: + contents: read + packages: write + steps: + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 + + - name: Log in to GHCR + uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=sha,format=short + type=raw,value=latest,enable={{is_default_branch}} + + - name: Create multi-arch manifest list and push + env: + IMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + AMD64_DIGEST: ${{ needs.build-amd64.outputs.digest }} + ARM64_DIGEST: ${{ needs.build-arm64.outputs.digest }} + run: | + tag_args=() + while IFS= read -r tag; do + tag_args+=("-t" "$tag") + done <<<"$(jq -r '.tags[]' <<<"$DOCKER_METADATA_OUTPUT_JSON")" + docker buildx imagetools create "${tag_args[@]}" \ + "$IMAGE@$AMD64_DIGEST" "$IMAGE@$ARM64_DIGEST" + + - name: Inspect image + env: + IMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + VERSION: ${{ steps.meta.outputs.version }} + run: | + docker buildx imagetools inspect "$IMAGE:$VERSION" + + build-pr-amd64: + name: Build (PR only, amd64) runs-on: ubuntu-latest if: github.event_name == 'pull_request' permissions: @@ -106,4 +247,29 @@ jobs: # Use the dedicated PR target, which omits `cache-to` because # fork PRs lack permission to write to the GHA cache. targets: synapse-website-pr - push: false + set: | + *.platform=linux/amd64 + + build-pr-arm64: + name: Build (PR only, arm64) + runs-on: ubuntu-24.04-arm + if: github.event_name == 'pull_request' + permissions: + contents: read + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 + + - name: Build + uses: docker/bake-action@76cc8060bdff6d632a465001e4cf300684c5472c # v5.7.0 + with: + files: | + website/docker-bake.hcl + targets: synapse-website-pr + set: | + *.platform=linux/arm64 diff --git a/website/docker-bake.hcl b/website/docker-bake.hcl index c4614bf..0a4e7e5 100644 --- a/website/docker-bake.hcl +++ b/website/docker-bake.hcl @@ -2,6 +2,12 @@ # Defining the build here keeps the Dockerfile, tags, cache config, and # platform declarations in one file under the website/ source tree. # +# CI builds each platform natively on its own runner (amd64 and arm64 jobs +# override `platform` via `--set`, avoiding QEMU emulation) and merges the +# two pushed digests into one multi-arch manifest list. A plain local +# `docker buildx bake synapse-website` still builds both platforms on a +# single node. +# # The PR build target omits `cache-to` because fork pull requests don't have # permission to write to the GHA cache; trying to export there fails the # required check. From 09313aa2d3be3b0ef8f3ae9d66f77ad88693fa56 Mon Sep 17 00:00:00 2001 From: Aarav Sharma Date: Sun, 13 Sep 2026 10:03:09 -0600 Subject: [PATCH 2/2] ci(docker): split PR builds into a dedicated workflow Push/tag publishing (build + digest push + manifest merge) stays in docker.yml; PR validation builds move to docker-pr.yml. Each file only triggers for its own event, so check lists no longer contain skipped jobs. --- .github/workflows/docker-pr.yml | 67 +++++++++++++++++++++++++++++++++ .github/workflows/docker.yml | 64 +++---------------------------- website/docker-bake.hcl | 3 +- 3 files changed, 75 insertions(+), 59 deletions(-) create mode 100644 .github/workflows/docker-pr.yml diff --git a/.github/workflows/docker-pr.yml b/.github/workflows/docker-pr.yml new file mode 100644 index 0000000..1fbfc4e --- /dev/null +++ b/.github/workflows/docker-pr.yml @@ -0,0 +1,67 @@ +name: Docker image (PR) + +# Default permissions are least-privilege; no job needs elevated access. +# This satisfies zizmor's excessive-permissions check. +permissions: + contents: read + +# A newer push to the PR cancels any older build that is still running. +concurrency: + group: docker-image-${{ github.ref }} + cancel-in-progress: true + +# Validation-only builds for pull requests: each platform builds natively on +# its own runner, without logging in to GHCR or pushing. Publishing lives in +# docker.yml. +on: + pull_request: + branches: [main] + +jobs: + build-amd64: + name: Build image (amd64) + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 + + - name: Build + uses: docker/bake-action@76cc8060bdff6d632a465001e4cf300684c5472c # v5.7.0 + with: + files: | + website/docker-bake.hcl + # Use the dedicated PR target, which omits `cache-to` because + # fork PRs lack permission to write to the GHA cache. + targets: synapse-website-pr + set: | + *.platform=linux/amd64 + + build-arm64: + name: Build image (arm64) + runs-on: ubuntu-24.04-arm + permissions: + contents: read + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 + + - name: Build + uses: docker/bake-action@76cc8060bdff6d632a465001e4cf300684c5472c # v5.7.0 + with: + files: | + website/docker-bake.hcl + targets: synapse-website-pr + set: | + *.platform=linux/arm64 diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 32eb632..67f2d39 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -12,12 +12,12 @@ concurrency: group: docker-image-${{ github.ref }} cancel-in-progress: true +# Builds and publishes the multi-arch website image on pushes to main and +# on v* tags. Pull-request validation builds live in docker-pr.yml. on: push: branches: [main] tags: ['v*'] - pull_request: - branches: [main] workflow_dispatch: env: @@ -41,9 +41,6 @@ jobs: build-amd64: name: Build & push image (amd64) runs-on: ubuntu-latest - # Publish only on direct pushes to main or on v* tags. PRs run the - # build-pr-* jobs below (no login, no push). - if: github.event_name != 'pull_request' permissions: contents: read packages: write @@ -113,7 +110,6 @@ jobs: build-arm64: name: Build & push image (arm64) runs-on: ubuntu-24.04-arm - if: github.event_name != 'pull_request' permissions: contents: read packages: write @@ -177,7 +173,6 @@ jobs: name: Merge manifests & push tags runs-on: ubuntu-latest needs: [build-amd64, build-arm64] - if: github.event_name != 'pull_request' permissions: contents: read packages: write @@ -222,54 +217,7 @@ jobs: IMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} VERSION: ${{ steps.meta.outputs.version }} run: | - docker buildx imagetools inspect "$IMAGE:$VERSION" - - build-pr-amd64: - name: Build (PR only, amd64) - runs-on: ubuntu-latest - if: github.event_name == 'pull_request' - permissions: - contents: read - steps: - - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - persist-credentials: false - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 - - - name: Build - uses: docker/bake-action@76cc8060bdff6d632a465001e4cf300684c5472c # v5.7.0 - with: - files: | - website/docker-bake.hcl - # Use the dedicated PR target, which omits `cache-to` because - # fork PRs lack permission to write to the GHA cache. - targets: synapse-website-pr - set: | - *.platform=linux/amd64 - - build-pr-arm64: - name: Build (PR only, arm64) - runs-on: ubuntu-24.04-arm - if: github.event_name == 'pull_request' - permissions: - contents: read - steps: - - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - persist-credentials: false - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 - - - name: Build - uses: docker/bake-action@76cc8060bdff6d632a465001e4cf300684c5472c # v5.7.0 - with: - files: | - website/docker-bake.hcl - targets: synapse-website-pr - set: | - *.platform=linux/arm64 + # version is the branch name on main pushes and the semver on tag + # pushes (verified in workflow logs), so this always resolves; the + # guard just turns a future trigger change into a clear failure. + docker buildx imagetools inspect "${IMAGE}:${VERSION:?metadata-action produced no version tag}" diff --git a/website/docker-bake.hcl b/website/docker-bake.hcl index 0a4e7e5..3b4bab5 100644 --- a/website/docker-bake.hcl +++ b/website/docker-bake.hcl @@ -1,4 +1,5 @@ -# Build targets consumed by docker/bake-action (see .github/workflows/docker.yml). +# Build targets consumed by docker/bake-action (see .github/workflows/ +# docker.yml for push/tag publishes and docker-pr.yml for PR builds). # Defining the build here keeps the Dockerfile, tags, cache config, and # platform declarations in one file under the website/ source tree. #