@@ -125,7 +125,7 @@ jobs:
125125 fi
126126
127127 build :
128- name : Build artifacts
128+ name : Build package
129129 runs-on : ubuntu-latest
130130 needs : plan
131131 permissions :
@@ -134,7 +134,6 @@ jobs:
134134 tarball_name : ${{ steps.pack.outputs.filename }}
135135 env :
136136 SCRIPTS : ${{ github.workspace }}/ci/scripts/src
137- SOURCE_SHA : ${{ needs.plan.outputs.source_sha }}
138137 VERSION : ${{ needs.plan.outputs.version }}
139138 steps :
140139 - name : Checkout workflow scripts
@@ -190,14 +189,59 @@ jobs:
190189 TARBALL : ${{ runner.temp }}/npm/${{ steps.pack.outputs.filename }}
191190 run : node "$SCRIPTS/release-verify-tarball.ts" "$TARBALL" "$VERSION"
192191
192+ - name : Upload npm package artifact
193+ uses : actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
194+ with :
195+ name : zero-npm-${{ needs.plan.outputs.version }}
196+ if-no-files-found : error
197+ retention-days : 7
198+ path : ${{ runner.temp }}/npm/${{ steps.pack.outputs.filename }}
199+
200+ build-docker :
201+ name : Build Docker image (${{ matrix.arch }})
202+ runs-on : ${{ matrix.runner }}
203+ needs : [plan, build]
204+ strategy :
205+ fail-fast : false
206+ matrix :
207+ include :
208+ - arch : amd64
209+ platform : linux/amd64
210+ runner : ubuntu-24.04
211+ - arch : arm64
212+ platform : linux/arm64
213+ runner : ubuntu-24.04-arm
214+ permissions :
215+ contents : read
216+ env :
217+ ARCH : ${{ matrix.arch }}
218+ PLATFORM : ${{ matrix.platform }}
219+ SOURCE_SHA : ${{ needs.plan.outputs.source_sha }}
220+ TARBALL : ${{ needs.build.outputs.tarball_name }}
221+ VERSION : ${{ needs.plan.outputs.version }}
222+ steps :
223+ - name : Checkout release source
224+ uses : actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
225+ with :
226+ path : source
227+ ref : ${{ needs.plan.outputs.source_sha }}
228+ persist-credentials : false
229+
230+ - name : Download npm package artifact
231+ uses : actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
232+ with :
233+ name : zero-npm-${{ needs.plan.outputs.version }}
234+ path : ${{ runner.temp }}/npm
235+
193236 - name : Set up Docker Buildx
194237 uses : docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
195238
196239 - name : Build Docker image archive
197240 env :
198- TARBALL : ${{ steps.pack.outputs.filename }}
199- TARBALL_PATH : ${{ runner.temp }}/npm/${{ steps.pack.outputs.filename }}
200- IMAGE_ARCHIVE : ${{ runner.temp }}/docker/zero-image.tar
241+ IMAGE_ARCHIVE : ${{ runner.temp }}/docker/zero-image-${{ matrix.arch }}.tar
242+ IMAGE_DIGEST : ${{ runner.temp }}/docker/zero-image-${{ matrix.arch }}.digest
243+ IMAGE_METADATA : ${{ runner.temp }}/docker/zero-image-${{ matrix.arch }}.json
244+ TARBALL_PATH : ${{ runner.temp }}/npm/${{ needs.build.outputs.tarball_name }}
201245 run : |
202246 set -euo pipefail
203247 mkdir -p source/packages/zero/pkgs "$(dirname "$IMAGE_ARCHIVE")"
@@ -209,37 +253,38 @@ jobs:
209253 build_contexts+=(--build-context monogo=source/go)
210254 fi
211255 docker buildx build \
212- --platform linux/amd64,linux/arm64 \
256+ --platform "$PLATFORM" \
213257 "${build_contexts[@]}" \
214258 --build-arg ZERO_VERSION="$VERSION" \
215259 --build-arg ZERO_PACKAGE="$TARBALL" \
216260 --label org.opencontainers.image.revision="$SOURCE_SHA" \
217261 --label org.opencontainers.image.version="$VERSION" \
262+ --metadata-file "$IMAGE_METADATA" \
218263 --sbom=true \
219264 --provenance=mode=max \
220265 --output=type=oci,dest="$IMAGE_ARCHIVE" \
221266 source/packages/zero
222-
223- - name : Upload npm package artifact
224- uses : actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
225- with :
226- name : zero-npm-${{ needs.plan.outputs.version }}
227- if-no-files-found : error
228- retention-days : 7
229- path : ${{ runner.temp }}/npm/${{ steps.pack.outputs.filename }}
267+ digest=$(jq -r '."containerimage.digest"' "$IMAGE_METADATA")
268+ if [[ ! "$digest" =~ ^sha256:[0-9a-f]{64}$ ]]; then
269+ echo "Invalid $ARCH image digest: $digest" >&2
270+ exit 1
271+ fi
272+ printf '%s\n' "$digest" > "$IMAGE_DIGEST"
230273
231274 - name : Upload Docker image artifact
232275 uses : actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
233276 with :
234- name : zero-docker-${{ needs.plan.outputs.version }}
277+ name : zero-docker-${{ needs.plan.outputs.version }}-${{ matrix.arch }}
235278 if-no-files-found : error
236279 retention-days : 7
237- path : ${{ runner.temp }}/docker/zero-image.tar
280+ path : |
281+ ${{ runner.temp }}/docker/zero-image-${{ matrix.arch }}.tar
282+ ${{ runner.temp }}/docker/zero-image-${{ matrix.arch }}.digest
238283
239284 publish-npm :
240285 name : Publish npm package
241286 runs-on : ubuntu-latest
242- needs : [plan, build]
287+ needs : [plan, build, build-docker ]
243288 environment :
244289 name : zero-release-npm
245290 permissions :
@@ -305,7 +350,7 @@ jobs:
305350 publish-docker :
306351 name : Publish Docker images
307352 runs-on : ubuntu-latest
308- needs : [plan, build, archive-cloudzero-source]
353+ needs : [plan, build-docker , archive-cloudzero-source]
309354 environment :
310355 name : zero-release-docker
311356 permissions :
@@ -315,31 +360,33 @@ jobs:
315360 MODE : ${{ needs.plan.outputs.mode }}
316361 VERSION : ${{ needs.plan.outputs.version }}
317362 steps :
318- - name : Download Docker image artifact
363+ - name : Download amd64 Docker image artifact
319364 uses : actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
320365 with :
321- name : zero-docker-${{ needs.plan.outputs.version }}
366+ name : zero-docker-${{ needs.plan.outputs.version }}-amd64
322367 path : dist/docker
323368
324- - name : Push GHCR Docker image
325- uses : docker://quay.io/containers/skopeo:v1.22.2-immutable@sha256:ca4fd94dba8cab15cf79c4c156bfc26d28e2265411294e9bba87756942e739ad
326- with :
327- args : >-
328- copy --all
329- --dest-creds ${{ github.actor }}:${{ github.token }}
330- oci-archive:dist/docker/zero-image.tar
331- docker://ghcr.io/rocicorp/zero:${{ needs.plan.outputs.version }}
332-
333- - name : Push GHCR head tag
334- # The version signature covers this tag because both resolve to one digest.
335- if : needs.plan.outputs.mode == 'head'
336- uses : docker://quay.io/containers/skopeo:v1.22.2-immutable@sha256:ca4fd94dba8cab15cf79c4c156bfc26d28e2265411294e9bba87756942e739ad
369+ - name : Download arm64 Docker image artifact
370+ uses : actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
337371 with :
338- args : >-
339- copy --all
340- --dest-creds ${{ github.actor }}:${{ github.token }}
341- oci-archive:dist/docker/zero-image.tar
342- docker://ghcr.io/rocicorp/zero:head
372+ name : zero-docker-${{ needs.plan.outputs.version }}-arm64
373+ path : dist/docker
374+
375+ - name : Read Docker image digests
376+ id : digests
377+ run : |
378+ set -euo pipefail
379+ for arch in amd64 arm64; do
380+ digest=$(<"dist/docker/zero-image-$arch.digest")
381+ if [[ ! "$digest" =~ ^sha256:[0-9a-f]{64}$ ]]; then
382+ echo "Invalid $arch image digest: $digest" >&2
383+ exit 1
384+ fi
385+ echo "$arch=$digest" >> "$GITHUB_OUTPUT"
386+ done
387+
388+ - name : Set up Docker Buildx
389+ uses : docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
343390
344391 - name : Install cosign
345392 uses : sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
@@ -362,27 +409,81 @@ jobs:
362409 with :
363410 username : rocicorp
364411
365- - name : Push Docker Hub image
412+ - name : Push GHCR amd64 image by digest
366413 uses : docker://quay.io/containers/skopeo:v1.22.2-immutable@sha256:ca4fd94dba8cab15cf79c4c156bfc26d28e2265411294e9bba87756942e739ad
367414 env :
368415 REGISTRY_AUTH_FILE : ${{ runner.temp }}/docker-auth/config.json
369416 with :
370417 args : >-
371- copy --all
372- oci-archive:dist/docker/zero-image.tar
373- docker://docker .io/rocicorp/zero: ${{ needs.plan .outputs.version }}
418+ copy --all --preserve-digests
419+ oci-archive:dist/docker/zero-image-amd64 .tar
420+ docker://ghcr .io/rocicorp/zero@ ${{ steps.digests .outputs.amd64 }}
374421
375- - name : Push Docker Hub head tag
376- # The version signature covers this tag because both resolve to one digest.
377- if : needs.plan.outputs.mode == 'head'
422+ - name : Push GHCR arm64 image by digest
378423 uses : docker://quay.io/containers/skopeo:v1.22.2-immutable@sha256:ca4fd94dba8cab15cf79c4c156bfc26d28e2265411294e9bba87756942e739ad
379424 env :
380425 REGISTRY_AUTH_FILE : ${{ runner.temp }}/docker-auth/config.json
381426 with :
382427 args : >-
383- copy --all
384- oci-archive:dist/docker/zero-image.tar
385- docker://docker.io/rocicorp/zero:head
428+ copy --all --preserve-digests
429+ oci-archive:dist/docker/zero-image-arm64.tar
430+ docker://ghcr.io/rocicorp/zero@${{ steps.digests.outputs.arm64 }}
431+
432+ - name : Push Docker Hub amd64 image by digest
433+ uses : docker://quay.io/containers/skopeo:v1.22.2-immutable@sha256:ca4fd94dba8cab15cf79c4c156bfc26d28e2265411294e9bba87756942e739ad
434+ env :
435+ REGISTRY_AUTH_FILE : ${{ runner.temp }}/docker-auth/config.json
436+ with :
437+ args : >-
438+ copy --all --preserve-digests
439+ oci-archive:dist/docker/zero-image-amd64.tar
440+ docker://docker.io/rocicorp/zero@${{ steps.digests.outputs.amd64 }}
441+
442+ - name : Push Docker Hub arm64 image by digest
443+ uses : docker://quay.io/containers/skopeo:v1.22.2-immutable@sha256:ca4fd94dba8cab15cf79c4c156bfc26d28e2265411294e9bba87756942e739ad
444+ env :
445+ REGISTRY_AUTH_FILE : ${{ runner.temp }}/docker-auth/config.json
446+ with :
447+ args : >-
448+ copy --all --preserve-digests
449+ oci-archive:dist/docker/zero-image-arm64.tar
450+ docker://docker.io/rocicorp/zero@${{ steps.digests.outputs.arm64 }}
451+
452+ - name : Create multi-platform Docker image tags
453+ env :
454+ AMD64_DIGEST : ${{ steps.digests.outputs.amd64 }}
455+ ARM64_DIGEST : ${{ steps.digests.outputs.arm64 }}
456+ DOCKER_CONFIG : ${{ runner.temp }}/docker-auth
457+ run : |
458+ set -euo pipefail
459+ for image in ghcr.io/rocicorp/zero docker.io/rocicorp/zero; do
460+ tags=(--tag "$image:$VERSION")
461+ if [ "$MODE" = 'head' ]; then
462+ # The version signature covers head because both use one digest.
463+ tags+=(--tag "$image:head")
464+ fi
465+ docker buildx imagetools create \
466+ "${tags[@]}" \
467+ "$image@$AMD64_DIGEST" \
468+ "$image@$ARM64_DIGEST"
469+ done
470+
471+ - name : Verify multi-platform Docker image tags
472+ env :
473+ DOCKER_CONFIG : ${{ runner.temp }}/docker-auth
474+ run : |
475+ set -euo pipefail
476+ for image in \
477+ "ghcr.io/rocicorp/zero:$VERSION" \
478+ "docker.io/rocicorp/zero:$VERSION"; do
479+ platforms=$(docker buildx imagetools inspect "$image" --raw |
480+ jq -r '.manifests[].platform | select(.os == "linux") | .architecture' |
481+ sort -u | paste -sd, -)
482+ if [ "$platforms" != 'amd64,arm64' ]; then
483+ echo "Expected $image to contain linux/amd64 and linux/arm64, got: $platforms" >&2
484+ exit 1
485+ fi
486+ done
386487
387488 - name : Sign Docker images
388489 env :
0 commit comments