@@ -13,12 +13,33 @@ jobs:
1313 ci :
1414 uses : ./.github/workflows/ci.yml
1515
16- release :
16+ # Multi-platform build via native runners. We build amd64 on
17+ # ubuntu-latest (x86_64) and arm64 on ubuntu-24.04-arm (free native
18+ # arm64 runners for public repos). Each job pushes a single-platform
19+ # image with a digest-only reference; the merge job below collects
20+ # those digests into a multi-arch manifest at :latest and :<version>.
21+ # This avoids QEMU-emulated rust compilation (~25 min → ~5 min).
22+ # Reference: https://docs.docker.com/build/ci/github-actions/multi-platform/
23+ build :
1724 needs : ci
18- runs-on : ubuntu-latest
25+ strategy :
26+ fail-fast : false
27+ matrix :
28+ include :
29+ - platform : linux/amd64
30+ runner : ubuntu-latest
31+ - platform : linux/arm64
32+ runner : ubuntu-24.04-arm
33+ runs-on : ${{ matrix.runner }}
1934 steps :
2035 - uses : actions/checkout@v4
2136
37+ - name : Platform label
38+ id : plabel
39+ run : echo "SUFFIX=${PLATFORM//\//-}" >> "$GITHUB_OUTPUT"
40+ env :
41+ PLATFORM : ${{ matrix.platform }}
42+
2243 - name : Set up Docker Buildx
2344 uses : docker/setup-buildx-action@v3
2445
@@ -29,32 +50,88 @@ jobs:
2950 username : ${{ github.actor }}
3051 password : ${{ secrets.GITHUB_TOKEN }}
3152
53+ - name : Lowercase repo name
54+ id : repo
55+ run : echo "IMAGE=ghcr.io/${GITHUB_REPOSITORY,,}" >> "$GITHUB_OUTPUT"
56+
57+ - name : Build and push (by digest)
58+ id : build
59+ uses : docker/build-push-action@v6
60+ with :
61+ context : .
62+ platforms : ${{ matrix.platform }}
63+ # push-by-digest stashes the image without a tag; merge job
64+ # below assembles the multi-arch manifest from both digests.
65+ outputs : type=image,name=${{ steps.repo.outputs.IMAGE }},push-by-digest=true,name-canonical=true,push=true
66+ cache-from : type=gha,scope=${{ steps.plabel.outputs.SUFFIX }}
67+ cache-to : type=gha,mode=max,scope=${{ steps.plabel.outputs.SUFFIX }}
68+
69+ - name : Export digest
70+ run : |
71+ mkdir -p /tmp/digests
72+ echo "${{ steps.build.outputs.digest }}" > /tmp/digests/${{ steps.plabel.outputs.SUFFIX }}
73+
74+ - name : Upload digest
75+ uses : actions/upload-artifact@v4
76+ with :
77+ name : digest-${{ steps.plabel.outputs.SUFFIX }}
78+ path : /tmp/digests/*
79+ if-no-files-found : error
80+ retention-days : 1
81+
82+ merge :
83+ needs : [ci, build]
84+ runs-on : ubuntu-latest
85+ steps :
3286 - name : Extract version from tag
3387 id : version
3488 run : echo "VERSION=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
3589
36- # GHCR rejects uppercase in repository names — PetalCat/Nexus must
37- # be normalized to petalcat/nexus before the push or buildx errors
38- # out with "repository name must be lowercase". $GITHUB_REPOSITORY
39- # is a runtime-controlled, non-attacker-controlled env var.
4090 - name : Lowercase repo name
4191 id : repo
4292 run : echo "IMAGE=ghcr.io/${GITHUB_REPOSITORY,,}" >> "$GITHUB_OUTPUT"
4393
44- - name : Build and push Docker image
45- uses : docker/build-push-action@v6
94+ - name : Download digests
95+ uses : actions/download-artifact@v4
4696 with :
47- context : .
48- push : true
49- # arm64 builds via QEMU take 20-30 min (rust cross-compile).
50- # Drop for now; re-add as a parallel job on native arm64 runners
51- # (ubuntu-24.04-arm) when we actually have arm64 deploy targets.
52- platforms : linux/amd64
53- tags : |
54- ${{ steps.repo.outputs.IMAGE }}:latest
55- ${{ steps.repo.outputs.IMAGE }}:${{ steps.version.outputs.VERSION }}
56- cache-from : type=gha
57- cache-to : type=gha,mode=max
97+ path : /tmp/digests
98+ pattern : digest-*
99+ merge-multiple : true
100+
101+ - name : Set up Docker Buildx
102+ uses : docker/setup-buildx-action@v3
103+
104+ - name : Log in to ghcr.io
105+ uses : docker/login-action@v3
106+ with :
107+ registry : ghcr.io
108+ username : ${{ github.actor }}
109+ password : ${{ secrets.GITHUB_TOKEN }}
110+
111+ - name : Create and push multi-arch manifest
112+ run : |
113+ cd /tmp/digests
114+ # Each file contains a single digest string (e.g. sha256:abc...).
115+ # `imagetools create` takes one IMAGE@digest arg per platform and
116+ # assembles them into a manifest list at the supplied tags.
117+ DIGEST_ARGS=""
118+ for f in *; do
119+ DIGEST=$(cat "$f")
120+ DIGEST_ARGS="$DIGEST_ARGS ${{ steps.repo.outputs.IMAGE }}@$DIGEST"
121+ done
122+ docker buildx imagetools create \
123+ --tag ${{ steps.repo.outputs.IMAGE }}:latest \
124+ --tag ${{ steps.repo.outputs.IMAGE }}:${{ steps.version.outputs.VERSION }} \
125+ $DIGEST_ARGS
126+
127+ - name : Inspect manifest
128+ run : docker buildx imagetools inspect ${{ steps.repo.outputs.IMAGE }}:${{ steps.version.outputs.VERSION }}
129+
130+ release :
131+ needs : merge
132+ runs-on : ubuntu-latest
133+ steps :
134+ - uses : actions/checkout@v4
58135
59136 - name : Create GitHub Release
60137 uses : softprops/action-gh-release@v2
0 commit comments