-
Notifications
You must be signed in to change notification settings - Fork 0
598 lines (557 loc) · 26.2 KB
/
Copy pathbuild-deploy.yml
File metadata and controls
598 lines (557 loc) · 26.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
name: 🐬 Builds ArchLinux Images for DockerHub 🐬
#REF: https://docs.docker.com/build/ci/github-actions/multi-platform/
#SECRETS: DOCKERHUB_USERNAME || DOCKERHUB_TOKEN
#URL: https://hub.docker.com/r/pkgforge/archlinux/tags
#For: https://github.com/pkgforge/devscripts/tree/main/Github/Runners
#------------------------------------------------------------------------------------#
# One job per architecture builds and pushes by digest. No tag is created there.
# A merge job with needs: over the whole matrix creates every tag from those
# digests, so a run that lost one architecture publishes nothing at all.
#
# Run it with dry_run to build against a scratch repository and create no public
# tag. That is how a branch is tested without a consumer seeing anything.
#------------------------------------------------------------------------------------#
on:
workflow_dispatch:
inputs:
dry_run:
description: Push to a scratch repository and create no public tag
type: boolean
default: false
dry_run_hub:
description: With dry_run, also copy to the Docker Hub scratch repository
type: boolean
default: false
# ⛔ The rollback guard's only way through. A stale but validly signed
# package set is what an on-path attacker on a plain http ARM mirror can
# serve, and signatures do not catch it. A legitimate upstream revert
# exists too, so the override is a named, deliberate act rather than a
# silent pass. HISTORY/arm-rollback.md.
allow_anchor_downgrade:
description: Publish even though an anchor version went backwards
type: boolean
default: false
schedule:
- cron: "30 05 * * *" # 08:30 PM UTC everyday
defaults:
run:
shell: bash
permissions:
contents: read
# A superseded scheduled run is cancelled rather than racing the newer one.
concurrency:
group: build-deploy-${{ github.ref }}
cancel-in-progress: true
env:
GHCR_IMAGE: ghcr.io/${{ github.repository_owner }}/archlinux
HUB_IMAGE: pkgforge/archlinux
SCRATCH_IMAGE: ghcr.io/${{ github.repository_owner }}/archlinux-ci
# The Docker Hub side of a dry run. Copying across registries is the one part
# of the publish that a GHCR-only dry run cannot reach, so it needs a scratch
# repository of its own on the other registry.
HUB_SCRATCH_IMAGE: pkgforge/archlinux-ci
#------------------------------------------------------------------------------------#
jobs:
#----------------------------------------------------------------------------------#
# Resolve every input once. Each architecture then builds against the same
# version, the same commit and an anchor pinned before any build started, so a
# run is reproducible as a unit and not only job by job.
#----------------------------------------------------------------------------------#
resolve:
name: Resolve inputs
runs-on: ubuntu-latest
outputs:
version: ${{ steps.inputs.outputs.version }}
build_date: ${{ steps.inputs.outputs.build_date }}
anchors: ${{ steps.inputs.outputs.anchors }}
target_image: ${{ steps.inputs.outputs.target_image }}
hub_image: ${{ steps.inputs.outputs.hub_image }}
publish_hub: ${{ steps.inputs.outputs.publish_hub }}
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Resolve
id: inputs
env:
DRY_RUN: ${{ inputs.dry_run }}
DRY_RUN_HUB: ${{ inputs.dry_run_hub }}
run: |
set -euo pipefail
version="$(date -u +'%Y.%m.%d')"
build_date="$(date -u +'%Y-%m-%dT%H:%M:%SZ')"
# ⛔ dry_run_hub only makes sense inside a dry run. Without this guard
# a run with dry_run_hub alone would take the real branch and read as
# though it had tested something.
if [ "${DRY_RUN_HUB:-false}" = "true" ] && [ "${DRY_RUN:-false}" != "true" ]; then
echo "dry_run_hub needs dry_run. On its own it would publish to the real repositories." >&2
exit 1
fi
if [ "${DRY_RUN:-false}" = "true" ]; then
target_image="$SCRATCH_IMAGE"
hub_image="$HUB_SCRATCH_IMAGE"
if [ "${DRY_RUN_HUB:-false}" = "true" ]; then
publish_hub=true
echo "::notice::dry run into ${SCRATCH_IMAGE} and ${HUB_SCRATCH_IMAGE}, exercising the cross-registry copy, creating no public tag"
else
publish_hub=false
echo "::notice::dry run, building into ${SCRATCH_IMAGE} and creating no public tag"
fi
else
target_image="$GHCR_IMAGE"
hub_image="$HUB_IMAGE"
publish_hub=true
fi
{
echo "version=$version"
echo "build_date=$build_date"
echo "target_image=$target_image"
echo "hub_image=$hub_image"
echo "publish_hub=$publish_hub"
} >> "$GITHUB_OUTPUT"
# The anchor names the pinned tag family. The ports are not in
# lockstep, so it is resolved per architecture.
anchors='{'
sep=''
for arch in amd64 arm64 armv7 loong64 riscv64 ppc ppc64 ppc64le; do
anchor="$(scripts/resolve-anchor "$arch")"
anchors="${anchors}${sep}\"${arch}\":\"${anchor}\""
sep=','
echo "anchor for $arch: $anchor"
done
anchors="${anchors}}"
jq -e . <<< "$anchors" > /dev/null
echo "anchors=$anchors" >> "$GITHUB_OUTPUT"
# ⛔ A build whose anchor went backwards is refused here, before anything
# is built or pushed. 20 of the 46 shipped mirrors are plain http, all on
# the two ARM ports, because the mirror Arch Linux ARM recommends offers
# no https. An on-path attacker there cannot forge a package, and can
# serve a stale but validly signed set. Signatures do not catch that.
#
# The floor is what this repository already published, read from the
# public tag list with no token and no state file. FLOOR_IMAGE is the
# real repository even in a dry run: the floor is a fact about the
# packages, not about where this run intends to push.
- name: Refuse a build whose anchor went backwards
env:
ANCHORS: ${{ steps.inputs.outputs.anchors }}
FLOOR_IMAGE: ${{ env.GHCR_IMAGE }}
ALLOW_ANCHOR_DOWNGRADE: ${{ inputs.allow_anchor_downgrade && '1' || '0' }}
CONTAINER_RUNTIME: docker
run: scripts/check-anchor-floor "$ANCHORS"
- name: Check the repository before building it
run: tests/run.sh static
#----------------------------------------------------------------------------------#
# Build each architecture and push it by digest. No tag is created here.
#
# The Dockerfile runs pacman on the builder's own architecture and only the
# last few steps are emulated, so QEMU is set up but carries little of the
# work. Cache is keyed per architecture: one shared key thrashes.
#
# There is no runner debloat step. Each job now builds one architecture rather
# than all of them in one job, so the disk pressure that step existed for is
# much lower. ⚠ Runner disk use has not been measured on this workflow.
#----------------------------------------------------------------------------------#
build:
name: Build ${{ matrix.docker_arch }}
needs: resolve
runs-on: ${{ matrix.runner }}
permissions:
contents: read
packages: write
strategy:
# One run reports every broken architecture. The merge job still refuses
# unless all of them passed.
fail-fast: false
matrix:
include:
- docker_arch: amd64
platform: linux/amd64
runner: ubuntu-latest
# Set the ARM64_RUNNER repository variable to a native arm64 label to
# take this target out of emulation. ⚠ Whether this repository has
# access to those runners is unverified, so the default keeps QEMU.
- docker_arch: arm64
platform: linux/arm64
runner: ${{ vars.ARM64_RUNNER || 'ubuntu-latest' }}
- docker_arch: armv7
platform: linux/arm/v7
runner: ubuntu-latest
# The loong64 port is published by the Linux Club of Peking
# University, not by Arch, so it carries its own keyring. Both
# feasibility gates are measured in HISTORY/loong64.md.
- docker_arch: loong64
platform: linux/loong64
runner: ubuntu-latest
- docker_arch: riscv64
platform: linux/riscv64
runner: ubuntu-latest
# The three PowerPC ports are published by ArchPOWER, which carries
# its own keyring and lays its repositories out differently from
# every other port: the core equivalent is named base, there is no
# extra, and `any` architecture packages are in a second database.
# HISTORY/powerpc.md has the measurements.
- docker_arch: ppc
platform: linux/ppc
runner: ubuntu-latest
- docker_arch: ppc64
platform: linux/ppc64
runner: ubuntu-latest
- docker_arch: ppc64le
platform: linux/ppc64le
runner: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Set up QEMU
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
# ⛔ The action does not cover big endian PowerPC, and nothing says so.
# tonistiigi/binfmt, which it runs, ships nine emulators: aarch64, arm,
# i386, loongarch64, mips64, mips64el, ppc64le, riscv64 and s390x.
# qemu-ppc and qemu-ppc64 are not among them. Measured 2026-08-28 by
# listing the image's own filesystem.
#
# The last stage of the Dockerfile runs pacman-key and locale-gen as
# target architecture binaries, so without a handler those two jobs fail
# at "exec format error" partway through a build that already downloaded
# every package.
#
# Ubuntu's own qemu-user-static ships both. update-binfmts imports the
# handler the package describes rather than a magic string written here.
- name: Register the big endian PowerPC emulators
if: matrix.docker_arch == 'ppc' || matrix.docker_arch == 'ppc64'
env:
DOCKER_ARCH: ${{ matrix.docker_arch }}
run: |
set -euo pipefail
handler="qemu-${DOCKER_ARCH}"
# ⛔ Not apt. Ubuntu's qemu-user-static ships the emulators and no
# binfmt descriptions at all: /usr/share/binfmts holds one entry,
# python3.13, and update-binfmts --import qemu-ppc answers "couldn't
# find information about 'qemu-ppc' to import" and exits 2. Measured
# 2026-08-28 on this workflow and in a Debian trixie container.
#
# This image registers every emulator it ships, with the F flag, which
# is the thing that has to be true. Pinned by digest like every other
# image here, and scripts/check-image-pins reads the marker.
# tag: latest
docker run --rm --privileged \
docker.io/multiarch/qemu-user-static@sha256:fe60359c92e86a43cc87b3d906006245f77bfc0565676b80004cc666e4feb9f0 \
--reset -p yes
# ⛔ Asserted, not assumed. A registration that did nothing leaves the
# build to fail much later, inside a buildx step, with a message about
# exec format that names no architecture.
reg="/proc/sys/fs/binfmt_misc/${handler}"
if [ ! -f "$reg" ]; then
echo "${handler} is not registered after the binfmt image ran" >&2
echo "registered handlers:" >&2
ls /proc/sys/fs/binfmt_misc >&2
exit 1
fi
cat "$reg"
# ⚠ The F flag is what makes the handler usable from inside a
# container. Without it the kernel opens the interpreter in the
# container's own filesystem, where the emulator does not exist, and
# buildkit's target stage cannot run a single target binary.
if ! awk '/^flags:/ && $2 ~ /F/ { found = 1 } END { exit !found }' "$reg"; then
echo "${handler} is registered without the F flag" >&2
echo "the emulator would have to exist inside the build container, and it does not" >&2
exit 1
fi
echo "::notice::${handler} registered with the F flag"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
- name: Login to GHCR
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push by digest
id: build
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with:
context: ${{ github.workspace }}
platforms: ${{ matrix.platform }}
build-args: |
IMAGE_VERSION=${{ needs.resolve.outputs.version }}
SOURCE_COMMIT=${{ github.sha }}
BUILD_DATE=${{ needs.resolve.outputs.build_date }}
outputs: type=image,name=${{ needs.resolve.outputs.target_image }},push-by-digest=true,name-canonical=true,push=true
cache-from: type=gha,scope=${{ matrix.docker_arch }}
cache-to: type=gha,mode=max,scope=${{ matrix.docker_arch }}
provenance: mode=max
sbom: true
# ⛔ The databases the build resolved against, taken from the build rather
# than fetched again. A package superseded upstream between the two is
# installed in the image and absent from every current database, so the
# evidence has a hole and the build fails. Scheduled run 33094128354 lost
# amd64 to exactly that: its build step began at 16:37:48Z and the second
# read of core.db landed at 16:38:51Z, 63 seconds of window, and
# ca-certificates-mozilla moved from 3.127-1 to 3.128-1 inside it.
#
# The bootstrap stage is already in the builder from the step above, so
# this exports files rather than installing anything again. A cache miss
# would resolve against newer databases, and gen-evidence still names any
# package it cannot account for, so the check is unchanged either way.
# HISTORY/evidence-race.md.
- name: Export the databases the build resolved against
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with:
context: ${{ github.workspace }}
platforms: ${{ matrix.platform }}
target: dbsnapshot
build-args: |
IMAGE_VERSION=${{ needs.resolve.outputs.version }}
outputs: type=local,dest=/tmp/dbsnapshot
cache-from: type=gha,scope=${{ matrix.docker_arch }}
- name: Record the evidence for what was pushed
env:
IMAGE: ${{ needs.resolve.outputs.target_image }}@${{ steps.build.outputs.digest }}
PLATFORM: ${{ matrix.platform }}
DOCKER_ARCH: ${{ matrix.docker_arch }}
CONTAINER_RUNTIME: docker
SOURCE_COMMIT: ${{ github.sha }}
BUILD_DATE: ${{ needs.resolve.outputs.build_date }}
# Read where the build left them. gen-evidence refuses to start when
# this directory is missing a database for an enabled repository, so
# an export that silently produced nothing cannot pass as a fetch.
DB_SNAPSHOT: /tmp/dbsnapshot
run: |
set -euo pipefail
# --platform is required. provenance and sbom wrap the pushed
# manifest in an index, so a bare pull selects the runner's own
# architecture and fails with "no matching manifest for linux/amd64".
docker pull --platform "$PLATFORM" "$IMAGE"
mkdir -p /tmp/evidence
# Which package, which version, which size, which checksum, and when
# it was built. A record saying the build succeeded is not evidence.
scripts/gen-evidence "$DOCKER_ARCH" "$IMAGE" "$PLATFORM" \
"/tmp/evidence/evidence-${DOCKER_ARCH}.json"
- name: Test what was actually pushed
env:
IMAGE: ${{ needs.resolve.outputs.target_image }}@${{ steps.build.outputs.digest }}
PLATFORM: ${{ matrix.platform }}
CONTAINER_RUNTIME: docker
EVIDENCE: /tmp/evidence/evidence-${{ matrix.docker_arch }}.json
run: |
set -euo pipefail
# The image suite inspects without starting, so an architecture whose
# bootstrap installed nothing still fails with a readable assertion
# rather than a runc error.
tests/run.sh image
- name: Upload the evidence
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: evidence-${{ matrix.docker_arch }}
path: /tmp/evidence/evidence-${{ matrix.docker_arch }}.json
if-no-files-found: error
retention-days: 90
- name: Export the digest
env:
DIGEST: ${{ steps.build.outputs.digest }}
run: |
set -euo pipefail
if [ -z "$DIGEST" ]; then
echo "the build produced no digest" >&2
exit 1
fi
mkdir -p /tmp/digests
printf '%s' "$DIGEST" > "/tmp/digests/${{ matrix.docker_arch }}"
- name: Upload the digest
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: digest-${{ matrix.docker_arch }}
path: /tmp/digests/${{ matrix.docker_arch }}
if-no-files-found: error
retention-days: 1
#----------------------------------------------------------------------------------#
# Create every tag from the digests the matrix produced.
#
# needs: over the whole matrix is what makes a partial failure publish nothing.
# There is no if: always() here on purpose: one architecture failing means no
# tag moves, rather than an index with a hole in it.
#
# The per architecture tags are created first and the index tags last, so
# :latest only moves after everything else has succeeded.
#----------------------------------------------------------------------------------#
publish:
name: Create tags
needs: [resolve, build]
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Download the digests
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
path: /tmp/digests
pattern: digest-*
merge-multiple: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
- name: Login to GHCR
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to Docker Hub
if: needs.resolve.outputs.publish_hub == 'true'
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Create the per architecture tags
env:
TARGET_IMAGE: ${{ needs.resolve.outputs.target_image }}
HUB_TARGET: ${{ needs.resolve.outputs.hub_image }}
PUBLISH_HUB: ${{ needs.resolve.outputs.publish_hub }}
VERSION: ${{ needs.resolve.outputs.version }}
ANCHORS: ${{ needs.resolve.outputs.anchors }}
run: |
set -euo pipefail
images=("$TARGET_IMAGE")
if [ "$PUBLISH_HUB" = "true" ]; then
images+=("$HUB_TARGET")
fi
for arch in amd64 arm64 armv7 loong64 riscv64 ppc ppc64 ppc64le; do
digest_file="/tmp/digests/$arch"
if [ ! -s "$digest_file" ]; then
echo "no digest for $arch, refusing to tag a partial release" >&2
exit 1
fi
digest="$(cat "$digest_file")"
anchor="$(jq -er --arg a "$arch" '.[$a]' <<< "$ANCHORS")"
mapfile -t tags < <(scripts/tag-names arch "$arch" "$VERSION" "$anchor" "${images[@]}")
args=()
for t in "${tags[@]}"; do
args+=(-t "$t")
done
echo "==> $arch $digest"
printf ' %s\n' "${tags[@]}"
docker buildx imagetools create "${args[@]}" "$TARGET_IMAGE@$digest"
done
- name: Create the index tags
env:
TARGET_IMAGE: ${{ needs.resolve.outputs.target_image }}
HUB_TARGET: ${{ needs.resolve.outputs.hub_image }}
PUBLISH_HUB: ${{ needs.resolve.outputs.publish_hub }}
VERSION: ${{ needs.resolve.outputs.version }}
run: |
set -euo pipefail
images=("$TARGET_IMAGE")
if [ "$PUBLISH_HUB" = "true" ]; then
images+=("$HUB_TARGET")
fi
sources=()
for arch in amd64 arm64 armv7 loong64 riscv64 ppc ppc64 ppc64le; do
sources+=("$TARGET_IMAGE@$(cat "/tmp/digests/$arch")")
done
mapfile -t tags < <(scripts/tag-names index "$VERSION" "${images[@]}")
args=()
for t in "${tags[@]}"; do
args+=(-t "$t")
done
printf ' %s\n' "${tags[@]}"
docker buildx imagetools create "${args[@]}" "${sources[@]}"
- name: Verify what was published
env:
TARGET_IMAGE: ${{ needs.resolve.outputs.target_image }}
HUB_TARGET: ${{ needs.resolve.outputs.hub_image }}
PUBLISH_HUB: ${{ needs.resolve.outputs.publish_hub }}
VERSION: ${{ needs.resolve.outputs.version }}
run: |
set -euo pipefail
# ⛔ Both registries are verified, not just the one the digests were
# staged on. Copying across registries is a separate operation that can
# fail on its own, and until this step covered it a Docker Hub tag that
# never arrived, or arrived without every platform, ended a green run.
verify_index() {
local image="$1" dated got expect
# The dated index tag carries a leading v. scripts/tag-names is the
# authority on the spelling, so it is asked rather than
# reconstructed: inspecting a tag the publish job never created
# would fail every run.
dated="$(scripts/tag-names index "$VERSION" "$image" | grep -v ':latest$')"
echo "verifying $dated"
# The index must carry every platform the matrix built. An index with
# a hole in it is the failure this whole topology exists to prevent.
got="$(docker buildx imagetools inspect "$dated" --raw \
| jq -r '[ .manifests[].platform
| select(.os != "unknown")
| .architecture + (if .variant then "/" + .variant else "" end) ]
| sort | join(" ")')"
echo "index platforms: $got"
# armv7 renders as arm/v7. A bare match on arm would also match
# arm64, so the assertion would pass with armv7 missing and mean
# nothing.
for expect in amd64 arm64 loong64 riscv64 ppc ppc64 ppc64le arm/v7; do
if ! grep -qw -- "$expect" <<< "$got"; then
echo "the index for $dated is missing $expect" >&2
echo "it carries: $got" >&2
echo "the digests were staged on ${TARGET_IMAGE}. If that index is" >&2
echo "complete and this one is not, the cross-registry copy is what failed." >&2
exit 1
fi
done
echo "::notice::published $dated with platforms $got"
}
verify_index "$TARGET_IMAGE"
if [ "$PUBLISH_HUB" = "true" ]; then
verify_index "$HUB_TARGET"
else
echo "::notice::publish_hub is false, so no Docker Hub tag was created or verified"
fi
#----------------------------------------------------------------------------------#
# The other half of the publish watchdog.
#
# ⛔ freshness-publish.yml watches whether this workflow's schedule is still
# firing. Nothing would watch whether that one's is, so this runs the same
# check and the two become each other's witness. A schedule that stops
# produces no run, so the only place its silence can be seen is a run of
# something else.
#
# ⛔ needs: nothing, and nothing needs it. A broken watchdog must not be able
# to stop a publish: the run goes red while every tag is still created. That
# is the correct shape for an alarm that is not a gate.
#
# ⚠ What this cannot see is itself. If the schedule on this workflow stops,
# this job stops with it, which is exactly why the check also lives in a
# workflow on a different schedule.
#----------------------------------------------------------------------------------#
watchdog:
name: Check the schedules are still firing
runs-on: ubuntu-latest
permissions:
contents: read
actions: read
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Every schedule in the repository
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -uo pipefail
scripts/check-schedules-fired
rc=$?
case "$rc" in
0) echo "::notice::every schedule has fired within its own tolerance" ;;
3)
echo "::error::at least one schedule has stopped firing"
echo "⛔ The publish itself is unaffected: this job gates nothing." >&2
exit 1
;;
*) echo "the schedule check could not run" >&2; exit 1 ;;
esac