@@ -3,6 +3,11 @@ name: Container (panel)
33# The Stitch (web UI) image. Separate from the bot image so a frontend change doesn't
44# rebuild and republish the bot, and so a broken panel build can never hold up a
55# bot release.
6+ #
7+ # Multi-arch: linux/amd64 and linux/arm64 on native runners, then a merge job
8+ # stitches the digests into one manifest list. Apple Silicon hosts need the
9+ # arm64 variant — a single-arch amd64 :latest fails with
10+ # "no matching manifest for linux/arm64/v8".
611
712on :
813 pull_request :
2631 - ' web/**'
2732 workflow_dispatch :
2833
34+ env :
35+ REGISTRY_IMAGE : ghcr.io/textile-protocol/textile-stitch-panel
36+
2937permissions :
3038 contents : read
3139 packages : write
@@ -36,8 +44,21 @@ concurrency:
3644
3745jobs :
3846 build :
39- runs-on : ubuntu-22.04
47+ runs-on : ${{ matrix.runner }}
48+ strategy :
49+ fail-fast : false
50+ matrix :
51+ include :
52+ - platform : linux/amd64
53+ runner : ubuntu-22.04
54+ - platform : linux/arm64
55+ runner : ubuntu-24.04-arm
4056 steps :
57+ - name : Prepare
58+ run : |
59+ platform='${{ matrix.platform }}'
60+ echo "PLATFORM_PAIR=${platform//\//-}" >> "$GITHUB_ENV"
61+
4162 - uses : actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
4263 with :
4364 persist-credentials : false
@@ -56,23 +77,104 @@ jobs:
5677 id : meta
5778 uses : docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6
5879 with :
59- images : ghcr.io/textile-protocol/textile-stitch-panel
60- tags : |
61- type=sha,prefix=sha-
62- type=raw,value=latest,enable={{is_default_branch}}
63- type=ref,event=branch
64- type=ref,event=pr
80+ images : ${{ env.REGISTRY_IMAGE }}
6581
6682 - name : Build container image
83+ id : build
6784 uses : docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
6885 with :
6986 context : .
7087 file : Dockerfile.panel
71- push : ${{ github.event_name != 'pull_request' }}
72- tags : ${{ steps.meta.outputs.tags }}
88+ platforms : ${{ matrix.platform }}
7389 labels : ${{ steps.meta.outputs.labels }}
74- cache-from : type=gha,scope=panel
90+ # PRs only compile-check each arch. Digests are pushed (untagged) on
91+ # main so the merge job can assemble the multi-arch manifest list.
92+ outputs : ${{ github.event_name == 'pull_request' && 'type=cacheonly' || format('type=image,name={0},push-by-digest=true,name-canonical=true,push=true', env.REGISTRY_IMAGE) }}
93+ cache-from : type=gha,scope=panel-${{ env.PLATFORM_PAIR }}
7594 # Only write the shared build cache from trusted (non-PR) runs. A PR can
7695 # run arbitrary code during the build; letting it populate the gha cache
7796 # would let it poison a layer later consumed by the main publish build.
78- cache-to : ${{ github.event_name != 'pull_request' && 'type=gha,mode=max,scope=panel' || '' }}
97+ cache-to : ${{ github.event_name != 'pull_request' && format('type=gha,mode=max,scope=panel-{0}', env.PLATFORM_PAIR) || '' }}
98+
99+ - name : Export digest
100+ if : github.event_name != 'pull_request'
101+ run : |
102+ mkdir -p "${{ runner.temp }}/digests"
103+ digest='${{ steps.build.outputs.digest }}'
104+ touch "${{ runner.temp }}/digests/${digest#sha256:}"
105+
106+ - name : Upload digest
107+ if : github.event_name != 'pull_request'
108+ uses : actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
109+ with :
110+ name : digests-panel-${{ env.PLATFORM_PAIR }}
111+ path : ${{ runner.temp }}/digests/*
112+ if-no-files-found : error
113+ retention-days : 1
114+
115+ merge :
116+ needs : build
117+ if : github.event_name != 'pull_request'
118+ runs-on : ubuntu-22.04
119+ steps :
120+ - name : Download digests
121+ uses : actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
122+ with :
123+ path : ${{ runner.temp }}/digests
124+ pattern : digests-panel-*
125+ merge-multiple : true
126+
127+ - uses : docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4
128+
129+ - name : Log in to GitHub Container Registry
130+ uses : docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4
131+ with :
132+ registry : ghcr.io
133+ username : ${{ github.actor }}
134+ password : ${{ secrets.GITHUB_TOKEN }}
135+
136+ - name : Extract Docker metadata
137+ id : meta
138+ uses : docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6
139+ with :
140+ images : ${{ env.REGISTRY_IMAGE }}
141+ tags : |
142+ type=sha,prefix=sha-
143+ type=raw,value=latest,enable={{is_default_branch}}
144+ type=ref,event=branch
145+
146+ - name : Create manifest list and push
147+ working-directory : ${{ runner.temp }}/digests
148+ run : |
149+ # Require both digests before tagging :latest — a partial publish would
150+ # strand Apple Silicon (arm64) or Linux servers (amd64).
151+ set -euo pipefail
152+ count="$(find . -type f | wc -l | tr -d ' ')"
153+ if [ "$count" -lt 2 ]; then
154+ echo "error: expected digests for linux/amd64 and linux/arm64, found $count" >&2
155+ find . -type f -print >&2 || true
156+ exit 1
157+ fi
158+ # shellcheck disable=SC2046
159+ docker buildx imagetools create \
160+ $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
161+ $(printf '%s@sha256:%s ' "$REGISTRY_IMAGE" *)
162+
163+ - name : Verify multi-arch manifest
164+ run : |
165+ set -euo pipefail
166+ tag='${{ steps.meta.outputs.version }}'
167+ docker buildx imagetools inspect "${REGISTRY_IMAGE}:${tag}"
168+ raw="$(docker buildx imagetools inspect --raw "${REGISTRY_IMAGE}:${tag}")"
169+ arches="$(printf '%s' "$raw" | jq -r '
170+ [.manifests[]?
171+ | select(.platform.os == "linux")
172+ | select(.platform.architecture == "amd64" or .platform.architecture == "arm64")
173+ | .platform.architecture]
174+ | unique | sort | join(",")
175+ ')"
176+ if [ "$arches" != "amd64,arm64" ]; then
177+ echo "error: ${REGISTRY_IMAGE}:${tag} must publish linux/amd64 and linux/arm64 (got: ${arches:-none})" >&2
178+ exit 1
179+ fi
180+ echo "ok: ${REGISTRY_IMAGE}:${tag} includes linux/amd64 and linux/arm64"
0 commit comments