ci: fix broken GitHub attestations and update SBOM tooling (#23763)

## Problem

GitHub SLSA provenance attestations have been silently failing on
**every release** since they were introduced. Confirmed across all 10+
release runs checked (v2.29.2 through v2.31.6).

The `actions/attest` action requires `subject-digest` (a `sha256:...`
hash) to identify the artifact being attested, but the workflow only
provided `subject-name` (the image tag like
`ghcr.io/coder/coder:v2.31.6`). This caused every attestation step to
error with:

```
Error: One of subject-path, subject-digest, or subject-checksums must be provided
```

The failures were masked by `continue-on-error: true` and only surfaced
as `##[warning]` annotations that nobody noticed. Enterprise customers
doing `gh attestation verify` would find no provenance records for any
of our Docker images.

> [!NOTE]
> The cosign SBOM attestation (separate step) has been working correctly
the entire time — it uses a different mechanism (`cosign attest --type
spdxjson`) that does not require the same inputs. This fix is
specifically for the GitHub-native SLSA provenance attestations.

## Fix

**Add `subject-digest` to all `actions/attest` steps** (release.yaml +
ci.yaml):
- Base image: capture digest from `depot/build-push-action` output
- Main image: resolve digest via `docker buildx imagetools inspect
--raw` after push
- Latest image: same approach
- Use `subject-name` without tag per the [actions/attest
docs](https://github.com/actions/attest#container-image)

**Update `anchore/sbom-action`** from v0.18.0 to v0.24.0 (node24
support, ahead of the [June 2
deadline](https://github.blog/changelog/2025-09-19-deprecation-of-node-20-on-github-actions-runners/)).

All changes remain non-blocking for the release process
(`continue-on-error: true` preserved).

> 🤖 This PR was created with the help of Coder Agents, and is reviewed
by a human.
This commit is contained in:
Atif Ali
2026-03-30 08:15:13 +00:00
committed by GitHub
parent 13dfc9a9bb
commit bc5e4b5d54
3 changed files with 58 additions and 38 deletions
+2 -2
View File
@@ -5,6 +5,6 @@ runs:
using: "composite"
steps:
- name: Install syft
uses: anchore/sbom-action/download-syft@f325610c9f50a54015d37c8d16cb3b0e2c8f4de0 # v0.18.0
uses: anchore/sbom-action/download-syft@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0
with:
syft-version: "v1.20.0"
syft-version: "v1.26.1"
+25 -13
View File
@@ -1316,20 +1316,30 @@ jobs:
"${IMAGE}"
done
# GitHub attestation provides SLSA provenance for the Docker images, establishing a verifiable
# record that these images were built in GitHub Actions with specific inputs and environment.
# This complements our existing cosign attestations which focus on SBOMs.
#
# We attest each tag separately to ensure all tags have proper provenance records.
# TODO: Consider refactoring these steps to use a matrix strategy or composite action to reduce duplication
# while maintaining the required functionality for each tag.
- name: Resolve Docker image digests for attestation
id: docker_digests
if: github.ref == 'refs/heads/main'
continue-on-error: true
env:
IMAGE_BASE: ghcr.io/coder/coder-preview
BUILD_TAG: ${{ steps.build-docker.outputs.tag }}
run: |
set -euxo pipefail
main_digest=$(docker buildx imagetools inspect --raw "${IMAGE_BASE}:main" | sha256sum | awk '{print "sha256:"$1}')
echo "main_digest=${main_digest}" >> "$GITHUB_OUTPUT"
latest_digest=$(docker buildx imagetools inspect --raw "${IMAGE_BASE}:latest" | sha256sum | awk '{print "sha256:"$1}')
echo "latest_digest=${latest_digest}" >> "$GITHUB_OUTPUT"
version_digest=$(docker buildx imagetools inspect --raw "${IMAGE_BASE}:${BUILD_TAG}" | sha256sum | awk '{print "sha256:"$1}')
echo "version_digest=${version_digest}" >> "$GITHUB_OUTPUT"
- name: GitHub Attestation for Docker image
id: attest_main
if: github.ref == 'refs/heads/main'
if: github.ref == 'refs/heads/main' && steps.docker_digests.outputs.main_digest != ''
continue-on-error: true
uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0
with:
subject-name: "ghcr.io/coder/coder-preview:main"
subject-name: ghcr.io/coder/coder-preview
subject-digest: ${{ steps.docker_digests.outputs.main_digest }}
predicate-type: "https://slsa.dev/provenance/v1"
predicate: |
{
@@ -1362,11 +1372,12 @@ jobs:
- name: GitHub Attestation for Docker image (latest tag)
id: attest_latest
if: github.ref == 'refs/heads/main'
if: github.ref == 'refs/heads/main' && steps.docker_digests.outputs.latest_digest != ''
continue-on-error: true
uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0
with:
subject-name: "ghcr.io/coder/coder-preview:latest"
subject-name: ghcr.io/coder/coder-preview
subject-digest: ${{ steps.docker_digests.outputs.latest_digest }}
predicate-type: "https://slsa.dev/provenance/v1"
predicate: |
{
@@ -1399,11 +1410,12 @@ jobs:
- name: GitHub Attestation for version-specific Docker image
id: attest_version
if: github.ref == 'refs/heads/main'
if: github.ref == 'refs/heads/main' && steps.docker_digests.outputs.version_digest != ''
continue-on-error: true
uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0
with:
subject-name: "ghcr.io/coder/coder-preview:${{ steps.build-docker.outputs.tag }}"
subject-name: ghcr.io/coder/coder-preview
subject-digest: ${{ steps.docker_digests.outputs.version_digest }}
predicate-type: "https://slsa.dev/provenance/v1"
predicate: |
{
+31 -23
View File
@@ -302,6 +302,7 @@ jobs:
# This uses OIDC authentication, so no auth variables are required.
- name: Build base Docker image via depot.dev
id: build_base_image
if: steps.image-base-tag.outputs.tag != ''
uses: depot/build-push-action@5f3b3c2e5a00f0093de47f657aeaefcedff27d18 # v1.17.0
with:
@@ -349,20 +350,14 @@ jobs:
env:
IMAGE_TAG: ${{ steps.image-base-tag.outputs.tag }}
# GitHub attestation provides SLSA provenance for Docker images, establishing a verifiable
# record that these images were built in GitHub Actions with specific inputs and environment.
# This complements our existing cosign attestations (which focus on SBOMs) by adding
# GitHub-specific build provenance to enhance our supply chain security.
#
# TODO: Consider refactoring these attestation steps to use a matrix strategy or composite action
# to reduce duplication while maintaining the required functionality for each distinct image tag.
- name: GitHub Attestation for Base Docker image
id: attest_base
if: ${{ !inputs.dry_run && steps.image-base-tag.outputs.tag != '' }}
if: ${{ !inputs.dry_run && steps.build_base_image.outputs.digest != '' }}
continue-on-error: true
uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0
with:
subject-name: ${{ steps.image-base-tag.outputs.tag }}
subject-name: ghcr.io/coder/coder-base
subject-digest: ${{ steps.build_base_image.outputs.digest }}
predicate-type: "https://slsa.dev/provenance/v1"
predicate: |
{
@@ -413,7 +408,6 @@ jobs:
# being pushed so will automatically push them.
make push/build/coder_"$version"_linux.tag
# Save multiarch image tag for attestation
multiarch_image="$(./scripts/image_tag.sh)"
echo "multiarch_image=${multiarch_image}" >> "$GITHUB_OUTPUT"
@@ -424,12 +418,14 @@ jobs:
# version in the repo, also create a multi-arch image as ":latest" and
# push it
if [[ "$(git tag | grep '^v' | grep -vE '(rc|dev|-|\+|\/)' | sort -r --version-sort | head -n1)" == "v$(./scripts/version.sh)" ]]; then
latest_target="$(./scripts/image_tag.sh --version latest)"
# shellcheck disable=SC2046
./scripts/build_docker_multiarch.sh \
--push \
--target "$(./scripts/image_tag.sh --version latest)" \
--target "${latest_target}" \
$(cat build/coder_"$version"_linux_{amd64,arm64,armv7}.tag)
echo "created_latest_tag=true" >> "$GITHUB_OUTPUT"
echo "latest_target=${latest_target}" >> "$GITHUB_OUTPUT"
else
echo "created_latest_tag=false" >> "$GITHUB_OUTPUT"
fi
@@ -450,7 +446,6 @@ jobs:
echo "Generating SBOM for multi-arch image: ${MULTIARCH_IMAGE}"
syft "${MULTIARCH_IMAGE}" -o spdx-json > "coder_${VERSION}_sbom.spdx.json"
# Attest SBOM to multi-arch image
echo "Attesting SBOM to multi-arch image: ${MULTIARCH_IMAGE}"
cosign clean --force=true "${MULTIARCH_IMAGE}"
cosign attest --type spdxjson \
@@ -472,13 +467,32 @@ jobs:
"${latest_tag}"
fi
- name: Resolve Docker image digests for attestation
id: docker_digests
if: ${{ !inputs.dry_run }}
continue-on-error: true
env:
MULTIARCH_IMAGE: ${{ steps.build_docker.outputs.multiarch_image }}
LATEST_TARGET: ${{ steps.build_docker.outputs.latest_target }}
run: |
set -euxo pipefail
if [[ -n "${MULTIARCH_IMAGE}" ]]; then
multiarch_digest=$(docker buildx imagetools inspect --raw "${MULTIARCH_IMAGE}" | sha256sum | awk '{print "sha256:"$1}')
echo "multiarch_digest=${multiarch_digest}" >> "$GITHUB_OUTPUT"
fi
if [[ -n "${LATEST_TARGET}" ]]; then
latest_digest=$(docker buildx imagetools inspect --raw "${LATEST_TARGET}" | sha256sum | awk '{print "sha256:"$1}')
echo "latest_digest=${latest_digest}" >> "$GITHUB_OUTPUT"
fi
- name: GitHub Attestation for Docker image
id: attest_main
if: ${{ !inputs.dry_run }}
if: ${{ !inputs.dry_run && steps.docker_digests.outputs.multiarch_digest != '' }}
continue-on-error: true
uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0
with:
subject-name: ${{ steps.build_docker.outputs.multiarch_image }}
subject-name: ghcr.io/coder/coder
subject-digest: ${{ steps.docker_digests.outputs.multiarch_digest }}
predicate-type: "https://slsa.dev/provenance/v1"
predicate: |
{
@@ -509,20 +523,14 @@ jobs:
}
push-to-registry: true
# Get the latest tag name for attestation
- name: Get latest tag name
id: latest_tag
if: ${{ !inputs.dry_run && steps.build_docker.outputs.created_latest_tag == 'true' }}
run: echo "tag=$(./scripts/image_tag.sh --version latest)" >> "$GITHUB_OUTPUT"
# If this is the highest version according to semver, also attest the "latest" tag
- name: GitHub Attestation for "latest" Docker image
id: attest_latest
if: ${{ !inputs.dry_run && steps.build_docker.outputs.created_latest_tag == 'true' }}
if: ${{ !inputs.dry_run && steps.docker_digests.outputs.latest_digest != '' }}
continue-on-error: true
uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0
with:
subject-name: ${{ steps.latest_tag.outputs.tag }}
subject-name: ghcr.io/coder/coder
subject-digest: ${{ steps.docker_digests.outputs.latest_digest }}
predicate-type: "https://slsa.dev/provenance/v1"
predicate: |
{