Repository navigation
598 lines (541 loc) · 26.1 KB
/
Copy pathdocker-riverproui.yaml
File metadata and controls
598 lines (541 loc) · 26.1 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: Docker - riverproui
on:
push:
branches:
- "master"
tags:
- "riverproui/v*"
pull_request:
branches:
- "master"
workflow_dispatch:
inputs:
ref:
description: "Release ref to operate on (format: riverproui/vX.Y.Z)"
required: true
verify_only:
description: "Verification/prefetch only (skip rebuild and pushes)"
required: false
type: boolean
default: false
ecr_manifest_digest:
description: "Optional ECR index digest (sha256:...) to verify index without ECR access"
required: false
type: string
force_prefetch:
description: "Force run prefetch/verification even if ref is not a release tag"
required: false
type: boolean
default: false
env:
ECR_REGION: us-east-2
jobs:
build-riverproui:
name: "Build image: riverproui (${{ matrix.docker_platform }})"
runs-on: ${{ matrix.runner }}
# Skip this job when manually dispatched with verify_only=true
if: ${{ !(github.event_name == 'workflow_dispatch' && inputs.verify_only) }}
env:
ECR_ACCOUNT_ID: ${{ secrets.ECR_CACHE_AWS_ACCOUNT_ID }}
ECR_ROLE_ARN: ${{ secrets.ECR_CACHE_ROLE_ARN }}
strategy:
matrix:
include:
- docker_platform: linux/amd64
runner: ubuntu-24.04
- docker_platform: linux/arm64
runner: ubuntu-24.04-arm
permissions:
contents: read
id-token: write
steps:
- name: Checkout
uses: actions/checkout@v7.0.1
with:
ref: ${{ inputs.ref || github.ref }}
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ env.ECR_ROLE_ARN }}
aws-region: ${{ env.ECR_REGION }}
role-session-name: GitHubActions
- name: Login to Amazon ECR
uses: aws-actions/amazon-ecr-login@v2
- name: Prepare
run: |
platform=${{ matrix.docker_platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Docker meta for Pro
id: meta-pro
uses: docker/metadata-action@v6
with:
images: ${{ format('{0}.dkr.ecr.{1}.amazonaws.com/riverqueue/riverproui', env.ECR_ACCOUNT_ID, env.ECR_REGION) }}
labels: |
org.opencontainers.image.source=https://github.com/riverqueue/riverui
org.opencontainers.image.description=River UI Pro is a web-based user interface for River, with pro features.
org.opencontainers.image.licenses=commercial
- name: Build & push (by digest)
id: build-pro
uses: docker/build-push-action@v7
with:
context: .
file: Dockerfile.pro
pull: true
platforms: ${{ matrix.docker_platform }}
provenance: true
labels: ${{ steps.meta-pro.outputs.labels }}
cache-from: type=registry,ref=${{ env.ECR_ACCOUNT_ID }}.dkr.ecr.${{ env.ECR_REGION }}.amazonaws.com/riverqueue/riverproui:cache-${{ env.PLATFORM_PAIR }}
cache-to: type=registry,ref=${{ env.ECR_ACCOUNT_ID }}.dkr.ecr.${{ env.ECR_REGION }}.amazonaws.com/riverqueue/riverproui:cache-${{ env.PLATFORM_PAIR }},mode=max
outputs: type=image,name=${{ env.ECR_ACCOUNT_ID }}.dkr.ecr.${{ env.ECR_REGION }}.amazonaws.com/riverqueue/riverproui,push-by-digest=true,name-canonical=true,push=true
secrets: |
"riverpro_credential=${{ secrets.RIVERPRO_GO_MOD_CREDENTIAL }}"
- name: Export digest
run: |
mkdir -p /tmp/digests
digest="${{ steps.build-pro.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@v7
with:
name: pro-digests-${{ env.PLATFORM_PAIR }}
path: /tmp/digests/*
if-no-files-found: error
# Keep platform digests longer to allow safe reruns without rebuilds
retention-days: 14
merge-riverproui:
name: "Merge manifests: riverproui"
runs-on: ubuntu-latest
needs:
- build-riverproui
# Skip this job when manually dispatched with verify_only=true
if: ${{ !(github.event_name == 'workflow_dispatch' && inputs.verify_only) }}
permissions:
contents: read
id-token: write
attestations: write
env:
ECR_ACCOUNT_ID: ${{ secrets.ECR_CACHE_AWS_ACCOUNT_ID }}
ECR_ROLE_ARN: ${{ secrets.ECR_CACHE_ROLE_ARN }}
TAG: ${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || github.ref_name }}
SHA_TAG: ${{ format('sha-{0}', github.sha) }}
outputs:
manifest_digest: ${{ steps.manifest_digest.outputs.digest }}
tag: ${{ steps.compute_tag.outputs.tag }}
immutable_tags: ${{ steps.export_immutable_tags.outputs.tags }}
mutable_tags: ${{ steps.tag_mutable.outputs.tags }}
steps:
- name: Checkout full history (no tags yet)
uses: actions/checkout@v7.0.1
with:
ref: ${{ inputs.ref || github.ref }}
fetch-depth: 0 # full history
# keep fetch-tags off to avoid the conflict on tag events
- name: Fetch tags
run: git fetch --tags --force
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Set up ORAS CLI
uses: oras-project/setup-oras@v2
- name: Install Cosign
uses: sigstore/cosign-installer@v3
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ env.ECR_ROLE_ARN }}
aws-region: ${{ env.ECR_REGION }}
role-session-name: GitHubActions
- name: Login to Amazon ECR
uses: aws-actions/amazon-ecr-login@v2
- name: Download digests
uses: actions/download-artifact@v8
with:
path: /tmp/digests
pattern: pro-digests-*
merge-multiple: true
- name: Compute TAG
id: compute_tag
run: |
if [ -n "${{ inputs.ref }}" ]; then
REF="${{ inputs.ref }}"
if [[ "$REF" =~ ^refs/tags/riverproui/v ]]; then
TAG="${REF#refs/tags/riverproui/v}"
elif [[ "$REF" =~ ^riverproui/v ]]; then
TAG="${REF#riverproui/v}"
elif [[ "$REF" =~ ^v[0-9]+\.[0-9]+\.[0-9]+ ]]; then
TAG="${REF#v}"
else
TAG="$REF"
fi
elif [[ "${{ github.ref }}" =~ ^refs/tags/riverproui/v[0-9]+\.[0-9]+\.[0-9]+(-[a-z0-9\.-]+)?$ ]]; then
TAG="${GITHUB_REF_NAME#riverproui/v}"
elif [ "${{ github.event_name }}" = "pull_request" ]; then
TAG="pr-${{ github.event.pull_request.number }}"
else
TAG="${{ github.ref_name }}"
fi
echo "tag=$TAG" >> $GITHUB_OUTPUT
echo "TAG=$TAG" >> $GITHUB_ENV
- name: Prepare ECR vars
run: |
ECR_IMAGE="${ECR_ACCOUNT_ID}.dkr.ecr.${ECR_REGION}.amazonaws.com/riverqueue/riverproui"
echo "ECR_IMAGE=${ECR_IMAGE}" >> $GITHUB_ENV
- name: Docker meta
id: meta
uses: docker/metadata-action@v6
with:
images: ${{ env.ECR_IMAGE }}
tags: |
type=raw,value=${{ env.TAG }}
type=ref,event=branch
type=ref,event=pr
type=sha,pattern=sha-{{sha}}
type=semver,pattern={{version}},match=riverproui/v(.*)
- name: Create manifest list and push immutable tags
working-directory: /tmp/digests
run: |
set -euo pipefail
# Deterministic ordering of platform digests (avoid non-deterministic glob order)
mapfile -t DIGEST_FILES < <(ls -1 | sort)
if crane manifest "$ECR_IMAGE:$TAG" > /tmp/current-index.json 2>/dev/null; then
mapfile -t HAVE < <(jq -r '.manifests[].digest | sub("^sha256:", "")' /tmp/current-index.json | sort)
mapfile -t WANT < <(printf '%s\n' "${DIGEST_FILES[@]}" | sort)
if [ "${HAVE[*]-}" = "${WANT[*]-}" ]; then
echo "Index $ECR_IMAGE:$TAG already has the same platform digests; skipping create."
exit 0
fi
fi
docker buildx imagetools create \
--annotation "index:org.opencontainers.image.source=https://github.com/riverqueue/riverui" \
--annotation "index:org.opencontainers.image.description=River UI Pro is a web-based user interface for River, with pro features." \
--annotation "index:org.opencontainers.image.licenses=commercial" \
`echo "${{ steps.meta.outputs.tags }}" | xargs -n1 echo -t` \
$(printf "$ECR_IMAGE@sha256:%s " "${DIGEST_FILES[@]}")
- name: Export immutable tags (short) for downstream jobs
id: export_immutable_tags
run: |
TAGS=$(jq -r '.tags[] | split(":") | .[1]' <<< "$DOCKER_METADATA_OUTPUT_JSON")
echo "tags<<EOF" >> $GITHUB_OUTPUT
echo "$TAGS" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Tag mutable on release if latest in series
id: tag_mutable
if: startsWith(github.ref, 'refs/tags/riverproui/v') || (github.event_name == 'workflow_dispatch' && (startsWith(inputs.ref, 'refs/tags/riverproui/v') || startsWith(inputs.ref, 'riverproui/v') || startsWith(inputs.ref, 'v')))
working-directory: /tmp/digests
run: |
STRIPPED_VERSION="${GITHUB_REF_NAME#riverproui/v}"
MAJOR="${STRIPPED_VERSION%%.*}"
MINOR_PATCH="${STRIPPED_VERSION#${MAJOR}.}"
MINOR="${MINOR_PATCH%%.*}"
declare -a mutable_tags=()
GLOBAL_LATEST=$(git tag --list 'riverproui/v*' --sort=-v:refname | grep -E '^riverproui/v[0-9]+\.[0-9]+\.[0-9]+(-[a-z0-9\.-]+)?$' | head -n1)
if [ "$GLOBAL_LATEST" = "riverproui/v${STRIPPED_VERSION}" ]; then
mutable_tags+=("latest")
fi
LATEST_IN_MAJOR=$(git tag --list "riverproui/v${MAJOR}.*" --sort=-v:refname | head -n1)
if [ "$LATEST_IN_MAJOR" = "riverproui/v${STRIPPED_VERSION}" ]; then
mutable_tags+=("v${MAJOR}")
fi
LATEST_IN_MINOR=$(git tag --list "riverproui/v${MAJOR}.${MINOR}.*" --sort=-v:refname | head -n1)
if [ "$LATEST_IN_MINOR" = "riverproui/v${STRIPPED_VERSION}" ]; then
mutable_tags+=("v${MAJOR}.${MINOR}")
fi
for tag in "${mutable_tags[@]}"; do
set -euo pipefail
NEW_DIGEST=$(crane digest "$ECR_IMAGE:$TAG")
CURRENT_DIGEST=$(crane digest "$ECR_IMAGE:$tag" 2>/dev/null || true)
if [ -n "$CURRENT_DIGEST" ] && [ "$CURRENT_DIGEST" = "$NEW_DIGEST" ]; then
echo "Mutable tag $tag already points to $NEW_DIGEST; skipping."
continue
fi
mapfile -t DIGEST_FILES < <(ls -1 | sort)
docker buildx imagetools create \
--annotation "index:org.opencontainers.image.source=https://github.com/riverqueue/riverui" \
--annotation "index:org.opencontainers.image.description=River UI Pro is a web-based user interface for River, with pro features." \
--annotation "index:org.opencontainers.image.licenses=commercial" \
-t "$ECR_IMAGE:$tag" \
$(printf "$ECR_IMAGE@sha256:%s " "${DIGEST_FILES[@]}")
done
if [ ${#mutable_tags[@]} -gt 0 ]; then
echo "tags<<EOF" >> $GITHUB_OUTPUT
printf "%s\n" "${mutable_tags[@]}" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
fi
- name: Install crane
uses: imjasonh/setup-crane@v0.6
- name: Compute manifest digest for attestation
id: manifest_digest
run: |
DIGEST=$(crane digest "$ECR_IMAGE:$TAG")
echo "digest=$DIGEST" >> $GITHUB_OUTPUT
- name: Generate build provenance attestation (not pushed)
id: attest
uses: actions/attest-build-provenance@v4.1.1
with:
push-to-registry: false
subject-digest: ${{ steps.manifest_digest.outputs.digest }}
subject-name: riverqueue.com/riverproui
- name: Resolve platform manifest digests
id: platform_digests
run: |
crane manifest "$ECR_IMAGE:$TAG" > /tmp/index.json
AMD64_DIGEST=$(jq -r '.manifests[] | select(.platform.os=="linux" and .platform.architecture=="amd64") .digest' /tmp/index.json)
ARM64_DIGEST=$(jq -r '.manifests[] | select(.platform.os=="linux" and .platform.architecture=="arm64") .digest' /tmp/index.json)
echo "amd64=$AMD64_DIGEST" >> "$GITHUB_OUTPUT"
echo "arm64=$ARM64_DIGEST" >> "$GITHUB_OUTPUT"
- name: Extract DSSE envelope from bundle
run: |
jq '.dsseEnvelope' "${{ steps.attest.outputs.bundle-path }}" > /tmp/dsse.json
- name: Decode DSSE payload to predicate (SLSA v1 statement)
run: |
jq -r '.payload' /tmp/dsse.json | base64 -d > /tmp/predicate.json
test "$(jq -r '.predicateType' /tmp/predicate.json)" = "https://slsa.dev/provenance/v1"
- name: Push Cosign attestations (index + per-platform)
env:
COSIGN_EXPERIMENTAL: "true"
COSIGN_REPOSITORY: ${{ env.ECR_ACCOUNT_ID }}.dkr.ecr.${{ env.ECR_REGION }}.amazonaws.com/riverqueue/riverproui
ECR_IMAGE: ${{ env.ECR_ACCOUNT_ID }}.dkr.ecr.${{ env.ECR_REGION }}.amazonaws.com/riverqueue/riverproui
run: |
# Idempotency guard: skip if a bundle is already present for this subject.
for subject in "${{ steps.manifest_digest.outputs.digest }}" "${{ steps.platform_digests.outputs.amd64 }}" "${{ steps.platform_digests.outputs.arm64 }}"; do
existing=$(oras discover --format json "${ECR_IMAGE}@${subject}" | jq -r '.manifests[]? | select(.artifactType=="application/vnd.dev.sigstore.bundle.v0.3+json" or .artifactType=="application/vnd.dev.sigstore.bundle+json;version=0.3") | .digest' | head -n1)
if [ -n "$existing" ]; then
echo "Bundle already exists for ${subject} (manifest $existing). Skipping cosign attest."
continue
fi
cosign attest \
--predicate /tmp/predicate.json \
--type https://slsa.dev/provenance/v1 \
--yes \
--new-bundle-format \
"${ECR_IMAGE}@${subject}"
done
prefetch-riverproui-through-live-registry:
# Prefetch only for release semver tags
name: "Prefetch riverproui via live registry"
runs-on: ubuntu-latest
needs:
- merge-riverproui
env:
IMAGE_NAME: riverqueue.com/riverproui
MANIFEST_DIGEST: ${{ needs.merge-riverproui.outputs.manifest_digest }}
TAG: ${{ needs.merge-riverproui.outputs.tag }}
IMMUTABLE_TAGS: ${{ needs.merge-riverproui.outputs.immutable_tags }}
MUTABLE_TAGS: ${{ needs.merge-riverproui.outputs.mutable_tags }}
# Only run on release tag events (refs/tags/riverproui/vX.Y.Z),
# or when manually forced via workflow_dispatch: force_prefetch=true.
# For workflow_dispatch, inputs.ref should be in the form 'riverproui/vX.Y.Z'.
if: startsWith(github.ref, 'refs/tags/riverproui/v') || (github.event_name == 'workflow_dispatch' && (startsWith(inputs.ref, 'riverproui/v') || inputs.force_prefetch))
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@v7.0.1
with:
ref: ${{ inputs.ref || github.ref }}
fetch-depth: 0 # full history
# keep fetch-tags off to avoid the conflict on tag events
- name: Fetch tags
run: git fetch --tags --force
# Compute TAG when running via workflow_dispatch. Expect inputs.ref to be 'riverproui/vX.Y.Z'.
- name: Compute TAG for workflow_dispatch
if: github.event_name == 'workflow_dispatch'
run: |
REF="${{ inputs.ref }}"
if [[ "$REF" =~ ^riverproui/v ]]; then
TAG="${REF#riverproui/v}"
elif [[ "$REF" =~ ^refs/tags/riverproui/v ]]; then
TAG="${REF#refs/tags/riverproui/}"
else
echo "inputs.ref must be of the form 'riverproui/vX.Y.Z' unless force_prefetch=true" >&2
if [ "${{ inputs.force_prefetch }}" != "true" ]; then
exit 1
fi
# Best-effort fallback: try to strip common prefixes
TAG="${REF#refs/tags/}"
TAG="${TAG#v}"
fi
echo "TAG=$TAG" >> $GITHUB_ENV
- name: Force refresh pushed tags
run: |
pushed_tags="$IMMUTABLE_TAGS"
if [ -n "$MUTABLE_TAGS" ]; then
if [ -n "$pushed_tags" ]; then
pushed_tags="$pushed_tags,$MUTABLE_TAGS"
else
pushed_tags="$MUTABLE_TAGS"
fi
fi
# In verify_only mode or when upstream jobs are skipped, fallback to current TAG
if [ -z "$pushed_tags" ] && [ -n "$TAG" ]; then
pushed_tags="$TAG"
fi
IFS=',' read -r -a tags <<< "$pushed_tags"
declare -a unique_tags=()
for t in "${tags[@]}"; do
if [[ ! " ${unique_tags[*]} " =~ " ${t} " ]]; then
unique_tags+=("$t")
fi
done
for tag in "${unique_tags[@]}"; do
if [ -n "$tag" ]; then
echo "Force refreshing tag: $tag"
curl -f -u river:"${{ secrets.RIVERPRO_GO_MOD_CREDENTIAL }}" \
-H "X-Force-Fetch-From-Upstream: ${{ secrets.FORCE_FETCH_SECRET }}" \
-H "Accept: application/vnd.docker.distribution.manifest.list.v2+json" \
"https://riverqueue.com/v2/riverproui/manifests/${tag}" -o /dev/null
curl -f -u river:"${{ secrets.RIVERPRO_GO_MOD_CREDENTIAL }}" \
-H "X-Force-Fetch-From-Upstream: ${{ secrets.FORCE_FETCH_SECRET }}" \
-H "Accept: application/vnd.oci.image.index.v1+json" \
"https://riverqueue.com/v2/riverproui/manifests/${tag}" -o /dev/null
fi
done
- name: Login to live registry
uses: docker/login-action@v4.5.2
with:
registry: riverqueue.com
username: river
password: ${{ secrets.RIVERPRO_GO_MOD_CREDENTIAL }}
- name: Install crane
uses: imjasonh/setup-crane@v0.6
- name: Set up ORAS CLI
uses: oras-project/setup-oras@v2
- name: Install Cosign
uses: sigstore/cosign-installer@v3
- name: Resolve manifest digest
id: resolve-digest
run: |
DIGEST=$(crane digest "$IMAGE_NAME:$TAG")
echo "digest=$DIGEST" >> $GITHUB_OUTPUT
- name: Fetch index manifest with crane (Docker preferred)
run: crane manifest "$IMAGE_NAME:$TAG" > /tmp/index-manifest-docker.json
- name: Fetch index manifest with oras (OCI preferred)
run: oras manifest fetch --output /tmp/index-manifest-oci.json "$IMAGE_NAME:$TAG"
- name: Extract platform manifest digests
id: platform-digests
run: |
AMD64_DIGEST=$(jq -r '.manifests[] | select(.platform.architecture == "amd64" and .platform.os == "linux") | .digest' /tmp/index-manifest-docker.json)
ARM64_DIGEST=$(jq -r '.manifests[] | select(.platform.architecture == "arm64" and .platform.os == "linux") | .digest' /tmp/index-manifest-docker.json)
echo "amd64_digest=$AMD64_DIGEST" >> $GITHUB_OUTPUT
echo "arm64_digest=$ARM64_DIGEST" >> $GITHUB_OUTPUT
- name: Prefetch buildx attestation manifests referenced by index (legacy non-referrers)
env:
AUTH_USER: river
AUTH_PASSWORD: ${{ secrets.RIVERPRO_GO_MOD_CREDENTIAL }}
FORCE_FETCH_SECRET: ${{ secrets.FORCE_FETCH_SECRET }}
REGISTRY_MANIFEST_URL: https://riverqueue.com/v2/riverproui/manifests
run: |
bash scripts/ci/prefetch-buildx-attestation-manifests.sh /tmp/index-manifest-docker.json
- name: Fetch amd64 manifest with crane (Docker media type)
run: crane manifest "$IMAGE_NAME@${{ steps.platform-digests.outputs.amd64_digest }}" > /tmp/amd64-manifest-docker.json
- name: Fetch amd64 manifest with oras (OCI media type)
run: oras manifest fetch --output /tmp/amd64-manifest-oci.json "$IMAGE_NAME@${{ steps.platform-digests.outputs.amd64_digest }}"
- name: Fetch arm64 manifest with crane (Docker media type)
run: crane manifest "$IMAGE_NAME@${{ steps.platform-digests.outputs.arm64_digest }}" > /tmp/arm64-manifest-docker.json
- name: Fetch arm64 manifest with oras (OCI media type)
run: oras manifest fetch --output /tmp/arm64-manifest-oci.json "$IMAGE_NAME@${{ steps.platform-digests.outputs.arm64_digest }}"
- name: Prefetch image blobs (configs + layers)
run: |
# Blobs are identical across media types; fetch once per platform.
for m in /tmp/amd64-manifest-docker.json /tmp/arm64-manifest-docker.json; do
cfg=$(jq -r '.config.digest // empty' "$m")
if [ -n "$cfg" ]; then
oras blob fetch --output /dev/null "$IMAGE_NAME@$cfg"
fi
jq -r '.layers[]?.digest // empty' "$m" | while read -r ld; do
[ -n "$ld" ] && oras blob fetch --output /dev/null "$IMAGE_NAME@$ld"
done
done
# Prefetch referrers for index (ECR digest if provided) and per-arch manifests.
# If neither MANIFEST_DIGEST (from merge job) nor ecr_manifest_digest (manual input)
# is available, we skip index referrer prefetch rather than fail.
- name: Prefetch all referrers content
env:
AUTH_USER: river
AUTH_PASSWORD: ${{ secrets.RIVERPRO_GO_MOD_CREDENTIAL }}
FORCE_FETCH_SECRET: ${{ secrets.FORCE_FETCH_SECRET }}
REGISTRY_MANIFEST_URL: https://riverqueue.com/v2/riverproui/manifests
REGISTRY_REFERRERS_URL: https://riverqueue.com/v2/riverproui/referrers
run: |
# Include both the ECR-derived index digest (attestations live here)
# and the live registry's index digest (may differ but could gain referrers).
FALLBACK="${{ inputs.ecr_manifest_digest }}"
INDEX_SUBJECT="${MANIFEST_DIGEST:-$FALLBACK}"
subjects=()
if [ -n "$INDEX_SUBJECT" ]; then
subjects+=("$INDEX_SUBJECT")
else
echo "Note: no index subject digest provided; skipping index referrers prefetch."
fi
subjects+=("${{ steps.resolve-digest.outputs.digest }}" "${{ steps.platform-digests.outputs.amd64_digest }}" "${{ steps.platform-digests.outputs.arm64_digest }}")
bash scripts/ci/prefetch-referrers-content.sh "${subjects[@]}"
# Verify index (if digest is available) and per-arch attestations against live registry.
# Index-level attestation is bound to the ECR index digest; provide it via MANIFEST_DIGEST
# or the ecr_manifest_digest input. If missing, we skip index verification.
- name: Verify image attestation via Workers (registry-based)
env:
COSIGN_EXPERIMENTAL: "true"
run: |
# Verify against the ECR-derived index digest (MANIFEST_DIGEST).
# The live tag's index digest may differ and typically has no bundles.
FALLBACK="${{ inputs.ecr_manifest_digest }}"
INDEX_SUBJECT="${MANIFEST_DIGEST:-$FALLBACK}"
if [ -n "$INDEX_SUBJECT" ]; then
cosign verify-attestation \
--type https://slsa.dev/provenance/v1 \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-identity-regexp '^https://github.com/riverqueue/riverui/.*' \
--new-bundle-format \
"$IMAGE_NAME@$INDEX_SUBJECT" \
| tee /tmp/cosign-verify-registry-index.txt
else
echo "Skipping index-level verification: no MANIFEST_DIGEST or ecr_manifest_digest provided."
fi
cosign verify-attestation \
--type https://slsa.dev/provenance/v1 \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-identity-regexp '^https://github.com/riverqueue/riverui/.*' \
--new-bundle-format \
"$IMAGE_NAME@${{ steps.platform-digests.outputs.amd64_digest }}" \
| tee /tmp/cosign-verify-registry-amd64.txt
cosign verify-attestation \
--type https://slsa.dev/provenance/v1 \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-identity-regexp '^https://github.com/riverqueue/riverui/.*' \
--new-bundle-format \
"$IMAGE_NAME@${{ steps.platform-digests.outputs.arm64_digest }}" \
| tee /tmp/cosign-verify-registry-arm64.txt
# Offline verification of Sigstore bundle: only attempt for index if digest provided.
- name: Verify Sigstore bundle against subject bytes (offline crypto)
env:
AUTH_USER: river
AUTH_PASSWORD: ${{ secrets.RIVERPRO_GO_MOD_CREDENTIAL }}
FORCE_FETCH_SECRET: ${{ secrets.FORCE_FETCH_SECRET }}
REGISTRY_MANIFEST_URL: https://riverqueue.com/v2/riverproui/manifests
REGISTRY_REFERRERS_URL: https://riverqueue.com/v2/riverproui/referrers
run: |
FALLBACK="${{ inputs.ecr_manifest_digest }}"
INDEX_SUBJECT="${MANIFEST_DIGEST:-$FALLBACK}"
if [ -n "$INDEX_SUBJECT" ]; then
bash scripts/ci/verify-sigstore-bundles-offline.sh "$INDEX_SUBJECT"
else
echo "Skipping offline index bundle verification: no MANIFEST_DIGEST or ecr_manifest_digest provided."
fi
bash scripts/ci/verify-sigstore-bundles-offline.sh \
"${{ steps.platform-digests.outputs.amd64_digest }}" \
"${{ steps.platform-digests.outputs.arm64_digest }}"
- name: Upload debug artifacts
if: failure() || cancelled()
uses: actions/upload-artifact@v7
with:
name: prefetch-debug
path: |
/tmp/index-manifest-docker.json
/tmp/index-manifest-oci.json
/tmp/subject.json
/tmp/bundle.json
/tmp/cosign-verify-registry-index.txt
/tmp/cosign-verify-registry-amd64.txt
/tmp/cosign-verify-registry-arm64.txt
retention-days: 1