From bc5e4b5d54a399427cf87863dcaf5126051f07d0 Mon Sep 17 00:00:00 2001 From: Atif Ali Date: Mon, 30 Mar 2026 08:15:13 +0000 Subject: [PATCH] ci: fix broken GitHub attestations and update SBOM tooling (#23763) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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. --- .github/actions/install-syft/action.yaml | 4 +- .github/workflows/ci.yaml | 38 +++++++++++------ .github/workflows/release.yaml | 54 ++++++++++++++---------- 3 files changed, 58 insertions(+), 38 deletions(-) diff --git a/.github/actions/install-syft/action.yaml b/.github/actions/install-syft/action.yaml index 7357cdc08e..0f8a440801 100644 --- a/.github/actions/install-syft/action.yaml +++ b/.github/actions/install-syft/action.yaml @@ -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" diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index cdc127dfe4..17e0a7ba16 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -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: | { diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 0e06cf2549..62fd93a4ce 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -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: | {