11name : 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.
55permissions :
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.
1517on :
1618 push :
1719 branches : [main]
1820 tags : ['v*']
19- pull_request :
20- branches : [main]
2121 workflow_dispatch :
2222
2323env :
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/
3340jobs :
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}"
0 commit comments