add delete confirmations for jobs #348
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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@v4 | |
| 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@v3 | |
| - name: Docker meta for Pro | |
| id: meta-pro | |
| uses: docker/metadata-action@v5 | |
| 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@v6 | |
| 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@v4 | |
| 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@v4 | |
| 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@v3 | |
| - name: Set up ORAS CLI | |
| uses: oras-project/setup-oras@v1 | |
| - 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@v4 | |
| 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@v5 | |
| 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.4 | |
| - 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@v2 | |
| 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@v4 | |
| 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@v3 | |
| with: | |
| registry: riverqueue.com | |
| username: river | |
| password: ${{ secrets.RIVERPRO_GO_MOD_CREDENTIAL }} | |
| - name: Install crane | |
| uses: imjasonh/setup-crane@v0.4 | |
| - name: Set up ORAS CLI | |
| uses: oras-project/setup-oras@v1 | |
| - 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@v4 | |
| 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 |