Skip to content

Commit 367bb03

Browse files
authored
Update docker CI to run arm64 build on a dedicated runner (#21)
* 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. * 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.
1 parent ab776a3 commit 367bb03

3 files changed

Lines changed: 213 additions & 25 deletions

File tree

‎.github/workflows/docker-pr.yml‎

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
name: Docker image (PR)
2+
3+
# Default permissions are least-privilege; no job needs elevated access.
4+
# This satisfies zizmor's excessive-permissions check.
5+
permissions:
6+
contents: read
7+
8+
# A newer push to the PR cancels any older build that is still running.
9+
concurrency:
10+
group: docker-image-${{ github.ref }}
11+
cancel-in-progress: true
12+
13+
# Validation-only builds for pull requests: each platform builds natively on
14+
# its own runner, without logging in to GHCR or pushing. Publishing lives in
15+
# docker.yml.
16+
on:
17+
pull_request:
18+
branches: [main]
19+
20+
jobs:
21+
build-amd64:
22+
name: Build image (amd64)
23+
runs-on: ubuntu-latest
24+
permissions:
25+
contents: read
26+
steps:
27+
- name: Checkout
28+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
29+
with:
30+
persist-credentials: false
31+
32+
- name: Set up Docker Buildx
33+
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
34+
35+
- name: Build
36+
uses: docker/bake-action@76cc8060bdff6d632a465001e4cf300684c5472c # v5.7.0
37+
with:
38+
files: |
39+
website/docker-bake.hcl
40+
# Use the dedicated PR target, which omits `cache-to` because
41+
# fork PRs lack permission to write to the GHA cache.
42+
targets: synapse-website-pr
43+
set: |
44+
*.platform=linux/amd64
45+
46+
build-arm64:
47+
name: Build image (arm64)
48+
runs-on: ubuntu-24.04-arm
49+
permissions:
50+
contents: read
51+
steps:
52+
- name: Checkout
53+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
54+
with:
55+
persist-credentials: false
56+
57+
- name: Set up Docker Buildx
58+
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
59+
60+
- name: Build
61+
uses: docker/bake-action@76cc8060bdff6d632a465001e4cf300684c5472c # v5.7.0
62+
with:
63+
files: |
64+
website/docker-bake.hcl
65+
targets: synapse-website-pr
66+
set: |
67+
*.platform=linux/arm64

‎.github/workflows/docker.yml‎

Lines changed: 138 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: Docker image
22

3-
# Default permissions are least-privilege; the push job elevates to
3+
# Default permissions are least-privilege; the push jobs elevate to
44
# packages: write below. This satisfies zizmor's excessive-permissions check.
55
permissions:
66
contents: read
@@ -12,12 +12,12 @@ concurrency:
1212
group: docker-image-${{ github.ref }}
1313
cancel-in-progress: true
1414

15+
# Builds and publishes the multi-arch website image on pushes to main and
16+
# on v* tags. Pull-request validation builds live in docker-pr.yml.
1517
on:
1618
push:
1719
branches: [main]
1820
tags: ['v*']
19-
pull_request:
20-
branches: [main]
2121
workflow_dispatch:
2222

2323
env:
@@ -30,16 +30,22 @@ env:
3030
# readability.
3131
IMAGE_NAME: ${{ github.repository_owner }}/synapse-website
3232

33+
# Each platform is built natively on its own runner (arm64 on GitHub's free
34+
# arm runner instead of QEMU emulation on an amd64 runner) and pushed to
35+
# GHCR by digest; the merge job then combines the two digests into a single
36+
# multi-arch manifest list with the computed tags.
37+
#
38+
# Pattern documented at
39+
# https://docs.docker.com/build/ci/github-actions/multi-platform/
3340
jobs:
34-
build:
35-
name: Build & push image
41+
build-amd64:
42+
name: Build & push image (amd64)
3643
runs-on: ubuntu-latest
37-
# Publish only on direct pushes to main or on v* tags. PRs run the
38-
# build-pr job (push: false, no cache-to) below.
39-
if: github.event_name != 'pull_request'
4044
permissions:
4145
contents: read
4246
packages: write
47+
outputs:
48+
digest: ${{ steps.digest.outputs.digest }}
4349
steps:
4450
- name: Checkout
4551
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
@@ -61,34 +67,54 @@ jobs:
6167
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
6268
with:
6369
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
70+
# The computed tags are only applied by the merge job; this step
71+
# exists so its OCI labels get baked into the pushed image.
6472
tags: |
6573
type=ref,event=branch
6674
type=semver,pattern={{version}}
6775
type=semver,pattern={{major}}.{{minor}}
6876
type=sha,format=short
6977
type=raw,value=latest,enable={{is_default_branch}}
70-
# bake-target makes metadata-action emit a JSON file
71-
# (steps.meta.outputs.bake-file) that overrides the same-named
72-
# target in docker-bake.hcl with the computed tags + labels.
78+
# bake-target makes metadata-action emit a JSON file that supplies
79+
# the labels for the 'synapse-website' target.
7380
bake-target: synapse-website
7481

75-
- name: Build and push
82+
- name: Build and push by digest
83+
id: build
7684
uses: docker/bake-action@76cc8060bdff6d632a465001e4cf300684c5472c # v5.7.0
7785
with:
78-
# The third entry is the metadata-action-generated JSON; it
79-
# supplies tags + labels for the 'synapse-website' target.
86+
# The metadata file supplies the labels; the set overrides then
87+
# replace its tags with the bare repository name, which is what
88+
# push-by-digest requires (tags are attached by the merge job).
8089
files: |
8190
website/docker-bake.hcl
8291
${{ steps.meta.outputs.bake-file }}
8392
targets: synapse-website
84-
push: true
93+
set: |
94+
*.platform=linux/amd64
95+
*.tags=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
96+
*.output=type=image,push-by-digest=true,name-canonical=true,push=true
8597
86-
build-pr:
87-
name: Build (PR only)
88-
runs-on: ubuntu-latest
89-
if: github.event_name == 'pull_request'
98+
- name: Record pushed digest
99+
id: digest
100+
env:
101+
BAKE_METADATA: ${{ steps.build.outputs.metadata }}
102+
run: |
103+
digest="$(jq -r '.["synapse-website"]["containerimage.digest"]' <<<"$BAKE_METADATA")"
104+
if [[ -z "$digest" || "$digest" == "null" ]]; then
105+
echo "bake metadata is missing containerimage.digest" >&2
106+
exit 1
107+
fi
108+
echo "digest=$digest" >> "$GITHUB_OUTPUT"
109+
110+
build-arm64:
111+
name: Build & push image (arm64)
112+
runs-on: ubuntu-24.04-arm
90113
permissions:
91114
contents: read
115+
packages: write
116+
outputs:
117+
digest: ${{ steps.digest.outputs.digest }}
92118
steps:
93119
- name: Checkout
94120
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
@@ -98,12 +124,100 @@ jobs:
98124
- name: Set up Docker Buildx
99125
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
100126

101-
- name: Build
127+
- name: Log in to GHCR
128+
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
129+
with:
130+
registry: ghcr.io
131+
username: ${{ github.actor }}
132+
password: ${{ secrets.GITHUB_TOKEN }}
133+
134+
- name: Extract metadata
135+
id: meta
136+
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
137+
with:
138+
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
139+
tags: |
140+
type=ref,event=branch
141+
type=semver,pattern={{version}}
142+
type=semver,pattern={{major}}.{{minor}}
143+
type=sha,format=short
144+
type=raw,value=latest,enable={{is_default_branch}}
145+
bake-target: synapse-website
146+
147+
- name: Build and push by digest
148+
id: build
102149
uses: docker/bake-action@76cc8060bdff6d632a465001e4cf300684c5472c # v5.7.0
103150
with:
104151
files: |
105152
website/docker-bake.hcl
106-
# Use the dedicated PR target, which omits `cache-to` because
107-
# fork PRs lack permission to write to the GHA cache.
108-
targets: synapse-website-pr
109-
push: false
153+
${{ steps.meta.outputs.bake-file }}
154+
targets: synapse-website
155+
set: |
156+
*.platform=linux/arm64
157+
*.tags=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
158+
*.output=type=image,push-by-digest=true,name-canonical=true,push=true
159+
160+
- name: Record pushed digest
161+
id: digest
162+
env:
163+
BAKE_METADATA: ${{ steps.build.outputs.metadata }}
164+
run: |
165+
digest="$(jq -r '.["synapse-website"]["containerimage.digest"]' <<<"$BAKE_METADATA")"
166+
if [[ -z "$digest" || "$digest" == "null" ]]; then
167+
echo "bake metadata is missing containerimage.digest" >&2
168+
exit 1
169+
fi
170+
echo "digest=$digest" >> "$GITHUB_OUTPUT"
171+
172+
merge:
173+
name: Merge manifests & push tags
174+
runs-on: ubuntu-latest
175+
needs: [build-amd64, build-arm64]
176+
permissions:
177+
contents: read
178+
packages: write
179+
steps:
180+
- name: Set up Docker Buildx
181+
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
182+
183+
- name: Log in to GHCR
184+
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
185+
with:
186+
registry: ghcr.io
187+
username: ${{ github.actor }}
188+
password: ${{ secrets.GITHUB_TOKEN }}
189+
190+
- name: Extract metadata
191+
id: meta
192+
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
193+
with:
194+
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
195+
tags: |
196+
type=ref,event=branch
197+
type=semver,pattern={{version}}
198+
type=semver,pattern={{major}}.{{minor}}
199+
type=sha,format=short
200+
type=raw,value=latest,enable={{is_default_branch}}
201+
202+
- name: Create multi-arch manifest list and push
203+
env:
204+
IMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
205+
AMD64_DIGEST: ${{ needs.build-amd64.outputs.digest }}
206+
ARM64_DIGEST: ${{ needs.build-arm64.outputs.digest }}
207+
run: |
208+
tag_args=()
209+
while IFS= read -r tag; do
210+
tag_args+=("-t" "$tag")
211+
done <<<"$(jq -r '.tags[]' <<<"$DOCKER_METADATA_OUTPUT_JSON")"
212+
docker buildx imagetools create "${tag_args[@]}" \
213+
"$IMAGE@$AMD64_DIGEST" "$IMAGE@$ARM64_DIGEST"
214+
215+
- name: Inspect image
216+
env:
217+
IMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
218+
VERSION: ${{ steps.meta.outputs.version }}
219+
run: |
220+
# version is the branch name on main pushes and the semver on tag
221+
# pushes (verified in workflow logs), so this always resolves; the
222+
# guard just turns a future trigger change into a clear failure.
223+
docker buildx imagetools inspect "${IMAGE}:${VERSION:?metadata-action produced no version tag}"

‎website/docker-bake.hcl‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
1-
# Build targets consumed by docker/bake-action (see .github/workflows/docker.yml).
1+
# Build targets consumed by docker/bake-action (see .github/workflows/
2+
# docker.yml for push/tag publishes and docker-pr.yml for PR builds).
23
# Defining the build here keeps the Dockerfile, tags, cache config, and
34
# platform declarations in one file under the website/ source tree.
45
#
6+
# CI builds each platform natively on its own runner (amd64 and arm64 jobs
7+
# override `platform` via `--set`, avoiding QEMU emulation) and merges the
8+
# two pushed digests into one multi-arch manifest list. A plain local
9+
# `docker buildx bake synapse-website` still builds both platforms on a
10+
# single node.
11+
#
512
# The PR build target omits `cache-to` because fork pull requests don't have
613
# permission to write to the GHA cache; trying to export there fails the
714
# required check.

0 commit comments

Comments
 (0)