From 35cba7bccb3a919d8c182401b503dd8b290399e3 Mon Sep 17 00:00:00 2001 From: Jakub Stejskal Date: Mon, 20 Jul 2026 22:08:35 +0200 Subject: [PATCH 1/9] Add attestation action Signed-off-by: Jakub Stejskal --- .../actions/build/attest-artifact/action.yml | 54 ++++++++++++ .../build/publish-helm-chart/action.yml | 16 +++- .../actions/build/push-containers/action.yml | 24 ++++++ .../build/release-artifacts/action.yml | 21 ++++- .../workflows/reusable-test-integrations.yml | 85 ++++++++++++++++++- .github/workflows/test-integrations.yml | 3 +- 6 files changed, 199 insertions(+), 4 deletions(-) create mode 100644 .github/actions/build/attest-artifact/action.yml diff --git a/.github/actions/build/attest-artifact/action.yml b/.github/actions/build/attest-artifact/action.yml new file mode 100644 index 0000000..0bdb19e --- /dev/null +++ b/.github/actions/build/attest-artifact/action.yml @@ -0,0 +1,54 @@ +name: "Attest Artifact" +description: "Creates provenance or SBOM attestation for file or OCI artifacts using actions/attest" + +inputs: + mode: + description: "Attestation mode: blob (file artifacts), oci (container/Helm provenance), or sbom (SBOM attestation for OCI image)" + required: true + subject-path: + description: "Path/glob to file artifacts (blob mode only)" + required: false + subject-prefix: + description: "Registry/org prefix for OCI artifacts, e.g. quay.io/strimzi (oci and sbom modes)" + required: false + image-name: + description: "Image name, e.g. operator (oci and sbom modes)" + required: false + subject-digest: + description: "Image digest, e.g. sha256:abc123 (oci and sbom modes)" + required: false + sbom-path: + description: "Path to SPDX/CycloneDX SBOM JSON file (sbom mode only)" + required: false + +outputs: + bundle-path: + description: "Path to the generated attestation bundle" + value: ${{ steps.attest-blob.outputs.bundle-path || steps.attest-oci.outputs.bundle-path || steps.attest-sbom.outputs.bundle-path }} + +runs: + using: "composite" + steps: + - name: Attest file artifact + id: attest-blob + if: ${{ inputs.mode == 'blob' }} + uses: actions/attest@f7c74d28b9d84cb8768d0b8ca14a4bac6ef463e6 # v4.2.0 + with: + subject-path: ${{ inputs.subject-path }} + + - name: Attest OCI artifact provenance + id: attest-oci + if: ${{ inputs.mode == 'oci' }} + uses: actions/attest@f7c74d28b9d84cb8768d0b8ca14a4bac6ef463e6 # v4.2.0 + with: + subject-name: ${{ inputs.subject-prefix }}/${{ inputs.image-name }} + subject-digest: ${{ inputs.subject-digest }} + + - name: Attest SBOM for OCI artifact + id: attest-sbom + if: ${{ inputs.mode == 'sbom' }} + uses: actions/attest@f7c74d28b9d84cb8768d0b8ca14a4bac6ef463e6 # v4.2.0 + with: + subject-name: ${{ inputs.subject-prefix }}/${{ inputs.image-name }} + subject-digest: ${{ inputs.subject-digest }} + sbom-path: ${{ inputs.sbom-path }} diff --git a/.github/actions/build/publish-helm-chart/action.yml b/.github/actions/build/publish-helm-chart/action.yml index 5085265..7146180 100644 --- a/.github/actions/build/publish-helm-chart/action.yml +++ b/.github/actions/build/publish-helm-chart/action.yml @@ -44,5 +44,19 @@ runs: run: helm registry login -u ${{ inputs.registryUser }} -p ${{ inputs.registryPassword }} ${{ inputs.containerRegistry }} - name: Push Helm Chart to OCI registry + id: helm-push shell: bash - run: helm push ${{ inputs.helmChartName }}-${{ inputs.releaseVersion }}.tgz oci://${{ inputs.containerRegistry }}/${{ inputs.containerOrg }} \ No newline at end of file + run: | + PUSH_OUTPUT=$(helm push ${{ inputs.helmChartName }}-${{ inputs.releaseVersion }}.tgz oci://${{ inputs.containerRegistry }}/${{ inputs.containerOrg }} 2>&1) + echo "$PUSH_OUTPUT" + DIGEST=$(echo "$PUSH_OUTPUT" | grep "Digest:" | awk '{print $2}') + echo "digest=${DIGEST}" >> "$GITHUB_OUTPUT" + + - name: Attest Helm chart + if: ${{ github.event_name != 'pull_request' }} + uses: ./.github/actions/build/attest-artifact + with: + mode: oci + subject-prefix: ${{ inputs.containerRegistry }}/${{ inputs.containerOrg }} + image-name: ${{ inputs.helmChartName }} + subject-digest: ${{ steps.helm-push.outputs.digest }} \ No newline at end of file diff --git a/.github/actions/build/push-containers/action.yml b/.github/actions/build/push-containers/action.yml index 1d47db1..7020ad3 100644 --- a/.github/actions/build/push-containers/action.yml +++ b/.github/actions/build/push-containers/action.yml @@ -31,6 +31,11 @@ inputs: description: "Suffix of archive with images" required: true +outputs: + images: + description: "JSON array of {name, digest} for multi-image attestation via attest-containers.yml" + value: ${{ steps.discover-images.outputs.images }} + runs: using: "composite" steps: @@ -131,6 +136,25 @@ runs: DOCKER_ORG: ${{ inputs.containerOrg }} DOCKER_TAG: ${{ inputs.containerTag }} + # Generates a list of images and it's digest for attestation + - name: Discover container images for attestation + id: discover-images + shell: bash + run: | + images="[]" + if [ -d "./sbom" ]; then + for sbom_file in $(find ./sbom -name "*.json" -type f | sort); do + relative="${sbom_file#./sbom/}" + name=$(echo "$relative" | cut -d'/' -f3) + digest=$(basename "$sbom_file" .json) + images=$(echo "$images" | jq -c --arg n "$name" --arg d "$digest" --arg s "$relative" \ + '. + [{"name": $n, "digest": $d, "sbom": $s}]') + echo "Found image: ${name} @ ${digest} (${relative})" + done + fi + echo "images=${images}" >> "$GITHUB_OUTPUT" + echo "Discovered $(echo "$images" | jq 'length') image(s) for attestation" + - name: Create SBOM archive # This condition is required for properly running the tests # The keyless signing doesn't work on pull_requests events so this part will be tested only during push events diff --git a/.github/actions/build/release-artifacts/action.yml b/.github/actions/build/release-artifacts/action.yml index 2207cae..a90befa 100644 --- a/.github/actions/build/release-artifacts/action.yml +++ b/.github/actions/build/release-artifacts/action.yml @@ -27,13 +27,32 @@ runs: RELEASE_VERSION: ${{ inputs.releaseVersion }} MVN_ARGS: '-B -DskipTests' + # Creates attestation for release artifacts + - name: Attest release archives + if: ${{ github.event_name != 'pull_request' }} + id: attest + uses: ./.github/actions/build/attest-artifact + with: + mode: blob + subject-path: | + ./**/*${{ inputs.releaseVersion }}*.tar.gz + ./**/*${{ inputs.releaseVersion }}*.zip + ./**/*${{ inputs.releaseVersion }}*.tgz + + # Copy attestation provenance file in .intoto.jsonl format to add it into release tarball + - name: Copy attestation bundle + if: ${{ github.event_name != 'pull_request' }} + shell: bash + run: cp "${{ steps.attest.outputs.bundle-path }}" "./release-${{ inputs.releaseVersion }}.intoto.jsonl" + - name: Create release tarball shell: bash run: | find . -type f \( -iname "*${{ inputs.releaseVersion }}*.tar.gz" -o \ -iname "*${{ inputs.releaseVersion }}*.zip" -o \ -iname "*${{ inputs.releaseVersion }}*.tgz" -o \ - -iname "*${{ inputs.releaseVersion }}*.yaml" \) \ + -iname "*${{ inputs.releaseVersion }}*.yaml" -o \ + -iname "*.intoto.jsonl" \) \ -exec tar -rvf release-${{ inputs.artifactSuffix }}-${{ inputs.releaseVersion }}.tar {} \; - name: Upload release artifacts diff --git a/.github/workflows/reusable-test-integrations.yml b/.github/workflows/reusable-test-integrations.yml index f4b220f..826eb31 100644 --- a/.github/workflows/reusable-test-integrations.yml +++ b/.github/workflows/reusable-test-integrations.yml @@ -70,7 +70,8 @@ on: # Declare default permissions as read only permissions: contents: read - id-token: write # Required for OIDC keyless signing + id-token: write # Required for OIDC keyless signing + attestations: write # Required for actions/attest provenance jobs: test-build-binaries: @@ -305,6 +306,8 @@ jobs: - test-build-containers runs-on: ubuntu-latest timeout-minutes: 20 + outputs: + images: ${{ steps.push.outputs.images }} steps: - name: Checkout ${{ inputs.repo }} uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 @@ -341,6 +344,7 @@ jobs: uses: ./.github/actions/dependencies/install-syft - name: Push containers using push-containers action + id: push uses: ./.github/actions/build/push-containers with: registryUser: "strimzi" @@ -349,6 +353,72 @@ jobs: architectures: "amd64" artifactSuffix: ${{ inputs.artifactSuffix }} + - name: Verify images discovery output + if: ${{ github.event_name != 'pull_request' }} + run: | + IMAGES='${{ steps.push.outputs.images }}' + COUNT=$(echo "$IMAGES" | jq 'length') + if [ "$COUNT" -eq 0 ]; then + echo "❌ No images discovered for attestation" + exit 1 + fi + echo "✓ Discovered $COUNT image(s) for attestation:" + echo "$IMAGES" | jq '.' + + test-attest-containers: + name: Attest Containers - ${{ matrix.image.name }} + if: ${{ github.event_name != 'pull_request' && inputs.buildContainers == true }} + needs: test-push-containers + runs-on: ubuntu-latest + timeout-minutes: 10 + strategy: + fail-fast: false + matrix: + image: ${{ fromJson(needs.test-push-containers.outputs.images) }} + steps: + - name: Checkout ${{ inputs.repo }} + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + repository: ${{ inputs.repo }} + ref: ${{ inputs.ref }} + + - name: Checkout github-actions + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + repository: strimzi/github-actions + ref: ${{ inputs.githubActionsRef || github.sha }} + path: github-actions + + - name: Setup actions for testing + run: | + mkdir -p .github/actions + cp -r github-actions/.github/actions/* .github/actions/ + + - name: Download SBOM artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: SBOMs-${{ inputs.artifactSuffix }}-latest.tar.gz + + - name: Extract SBOMs + run: tar -xzf sbom.tar.gz + + - name: Attest container image provenance + uses: ./.github/actions/build/attest-artifact + with: + mode: oci + subject-prefix: registry.strimzi:443/strimzi + image-name: ${{ matrix.image.name }} + subject-digest: ${{ matrix.image.digest }} + + - name: Attest container SBOM + uses: ./.github/actions/build/attest-artifact + with: + mode: sbom + subject-prefix: registry.strimzi:443/strimzi + image-name: ${{ matrix.image.name }} + subject-digest: ${{ matrix.image.digest }} + sbom-path: ./${{ matrix.image.sbom }} + test-release-artifacts: name: Release Artifacts needs: @@ -414,6 +484,19 @@ jobs: tar -tf "$TARBALL" echo "✓ Release artifacts created successfully" + - name: Verify attestation bundle in tarball + if: ${{ github.event_name != 'pull_request' }} + run: | + TARBALL="release-${{ inputs.artifactSuffix }}-${{ inputs.releaseVersion }}.tar" + INTOTO_COUNT=$(tar -tf "$TARBALL" | grep -c '\.intoto\.jsonl$' || true) + if [ "$INTOTO_COUNT" -eq 0 ]; then + echo "❌ No .intoto.jsonl attestation found in release tarball" + tar -tf "$TARBALL" + exit 1 + fi + echo "✓ Found $INTOTO_COUNT attestation bundle(s) in release tarball:" + tar -tf "$TARBALL" | grep '\.intoto\.jsonl$' + # This check is currently only for Operators repo as yamls were missing in release tar ball - name: Verify YAML release artifacts in tarball if: ${{ inputs.clusterOperatorBuild == true }} diff --git a/.github/workflows/test-integrations.yml b/.github/workflows/test-integrations.yml index 6bc71e1..e97a41b 100644 --- a/.github/workflows/test-integrations.yml +++ b/.github/workflows/test-integrations.yml @@ -12,7 +12,8 @@ on: # Declare default permissions as read only permissions: contents: read - id-token: write # Required for OIDC keyless signing + id-token: write # Required for OIDC keyless signing + attestations: write # Required for actions/attest provenance # Cancel previous builds on the same branch/PR concurrency: From 3f34e0339b62451870fb33a584246ca5628cdb6a Mon Sep 17 00:00:00 2001 From: Jakub Stejskal Date: Mon, 20 Jul 2026 22:09:09 +0200 Subject: [PATCH 2/9] Add testing branch Signed-off-by: Jakub Stejskal --- .github/workflows/test-integrations.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test-integrations.yml b/.github/workflows/test-integrations.yml index e97a41b..cb04874 100644 --- a/.github/workflows/test-integrations.yml +++ b/.github/workflows/test-integrations.yml @@ -8,6 +8,7 @@ on: branches: - "main" - "release-*" + - "add-attestation-action" # Declare default permissions as read only permissions: From 4b979628d14a0fc4be6728cfef90c1721a1af362 Mon Sep 17 00:00:00 2001 From: Jakub Stejskal Date: Mon, 20 Jul 2026 22:48:48 +0200 Subject: [PATCH 3/9] Fix archives names and extract image names properly Signed-off-by: Jakub Stejskal --- .../actions/build/push-containers/action.yml | 3 +- .../build/release-artifacts/action.yml | 30 +++++++++++++++---- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/.github/actions/build/push-containers/action.yml b/.github/actions/build/push-containers/action.yml index 7020ad3..f445b71 100644 --- a/.github/actions/build/push-containers/action.yml +++ b/.github/actions/build/push-containers/action.yml @@ -145,7 +145,8 @@ runs: if [ -d "./sbom" ]; then for sbom_file in $(find ./sbom -name "*.json" -type f | sort); do relative="${sbom_file#./sbom/}" - name=$(echo "$relative" | cut -d'/' -f3) + after_org="${relative#*${{ inputs.containerRegistry }}/${{ inputs.containerOrg }}/}" + name=$(echo "$after_org" | cut -d'/' -f1) digest=$(basename "$sbom_file" .json) images=$(echo "$images" | jq -c --arg n "$name" --arg d "$digest" --arg s "$relative" \ '. + [{"name": $n, "digest": $d, "sbom": $s}]') diff --git a/.github/actions/build/release-artifacts/action.yml b/.github/actions/build/release-artifacts/action.yml index a90befa..b28cd7c 100644 --- a/.github/actions/build/release-artifacts/action.yml +++ b/.github/actions/build/release-artifacts/action.yml @@ -27,21 +27,39 @@ runs: RELEASE_VERSION: ${{ inputs.releaseVersion }} MVN_ARGS: '-B -DskipTests' + # Find release archives using case-insensitive search (Maven may change case, e.g. rc1 -> RC1) + - name: Find release archives for attestation + id: find-archives + shell: bash + run: | + FILES=$(find . -type f \( -iname "*${{ inputs.releaseVersion }}*.tar.gz" -o \ + -iname "*${{ inputs.releaseVersion }}*.zip" -o \ + -iname "*${{ inputs.releaseVersion }}*.tgz" \)) + if [ -n "$FILES" ]; then + echo "found=true" >> "$GITHUB_OUTPUT" + { + echo "files<> "$GITHUB_OUTPUT" + echo "Found $(echo "$FILES" | wc -l | tr -d ' ') release archive(s) for attestation" + else + echo "found=false" >> "$GITHUB_OUTPUT" + echo "No release archives found for attestation" + fi + # Creates attestation for release artifacts - name: Attest release archives - if: ${{ github.event_name != 'pull_request' }} + if: ${{ github.event_name != 'pull_request' && steps.find-archives.outputs.found == 'true' }} id: attest uses: ./.github/actions/build/attest-artifact with: mode: blob - subject-path: | - ./**/*${{ inputs.releaseVersion }}*.tar.gz - ./**/*${{ inputs.releaseVersion }}*.zip - ./**/*${{ inputs.releaseVersion }}*.tgz + subject-path: ${{ steps.find-archives.outputs.files }} # Copy attestation provenance file in .intoto.jsonl format to add it into release tarball - name: Copy attestation bundle - if: ${{ github.event_name != 'pull_request' }} + if: ${{ github.event_name != 'pull_request' && steps.find-archives.outputs.found == 'true' }} shell: bash run: cp "${{ steps.attest.outputs.bundle-path }}" "./release-${{ inputs.releaseVersion }}.intoto.jsonl" From 21e4ba43c3cb66f1a8a8ffb9d17a28713312b6ff Mon Sep 17 00:00:00 2001 From: Jakub Stejskal Date: Mon, 20 Jul 2026 23:33:26 +0200 Subject: [PATCH 4/9] Add attest check Signed-off-by: Jakub Stejskal --- .github/workflows/reusable-test-integrations.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/workflows/reusable-test-integrations.yml b/.github/workflows/reusable-test-integrations.yml index 826eb31..992c469 100644 --- a/.github/workflows/reusable-test-integrations.yml +++ b/.github/workflows/reusable-test-integrations.yml @@ -419,6 +419,18 @@ jobs: subject-digest: ${{ matrix.image.digest }} sbom-path: ./${{ matrix.image.sbom }} + - name: Verify attestations were created + env: + GH_TOKEN: ${{ github.token }} + run: | + DIGEST="${{ matrix.image.digest }}" + ATTESTATIONS=$(gh api "repos/${{ github.repository }}/attestations/${DIGEST}" --jq '.attestations | length') + if [ "$ATTESTATIONS" -eq 0 ]; then + echo "❌ No attestations found for ${DIGEST}" + exit 1 + fi + echo "✓ Found $ATTESTATIONS attestation(s) for ${{ matrix.image.name }} @ ${DIGEST}" + test-release-artifacts: name: Release Artifacts needs: From ba179cd3621fb72cc4e2e86f12ea14c3995b7e66 Mon Sep 17 00:00:00 2001 From: Jakub Stejskal Date: Tue, 21 Jul 2026 10:33:28 +0200 Subject: [PATCH 5/9] Add attestation summary and remove real attest Signed-off-by: Jakub Stejskal --- .../actions/build/attest-artifact/action.yml | 24 +++++++ .../workflows/reusable-test-integrations.yml | 66 ------------------- 2 files changed, 24 insertions(+), 66 deletions(-) diff --git a/.github/actions/build/attest-artifact/action.yml b/.github/actions/build/attest-artifact/action.yml index 0bdb19e..566561d 100644 --- a/.github/actions/build/attest-artifact/action.yml +++ b/.github/actions/build/attest-artifact/action.yml @@ -52,3 +52,27 @@ runs: subject-name: ${{ inputs.subject-prefix }}/${{ inputs.image-name }} subject-digest: ${{ inputs.subject-digest }} sbom-path: ${{ inputs.sbom-path }} + + - name: Attestation summary + shell: bash + run: | + echo "::group::Attestation Summary" + echo "Mode: ${{ inputs.mode }}" + if [ "${{ inputs.mode }}" = "blob" ]; then + echo "Subject: ${{ inputs.subject-path }}" + else + echo "Subject: ${{ inputs.subject-prefix }}/${{ inputs.image-name }}@${{ inputs.subject-digest }}" + fi + if [ "${{ inputs.mode }}" = "sbom" ]; then + echo "SBOM: ${{ inputs.sbom-path }}" + fi + URL="${{ steps.attest-blob.outputs.attestation-url }}${{ steps.attest-oci.outputs.attestation-url }}${{ steps.attest-sbom.outputs.attestation-url }}" + echo "Attestation URL: ${URL}" + echo "" + echo "To verify this attestation, run:" + if [ "${{ inputs.mode }}" = "blob" ]; then + echo " gh attestation verify --repo ${{ github.repository }}" + else + echo " gh attestation verify oci://${{ inputs.subject-prefix }}/${{ inputs.image-name }}@${{ inputs.subject-digest }} --repo ${{ github.repository }}" + fi + echo "::endgroup::" diff --git a/.github/workflows/reusable-test-integrations.yml b/.github/workflows/reusable-test-integrations.yml index 992c469..784eada 100644 --- a/.github/workflows/reusable-test-integrations.yml +++ b/.github/workflows/reusable-test-integrations.yml @@ -365,72 +365,6 @@ jobs: echo "✓ Discovered $COUNT image(s) for attestation:" echo "$IMAGES" | jq '.' - test-attest-containers: - name: Attest Containers - ${{ matrix.image.name }} - if: ${{ github.event_name != 'pull_request' && inputs.buildContainers == true }} - needs: test-push-containers - runs-on: ubuntu-latest - timeout-minutes: 10 - strategy: - fail-fast: false - matrix: - image: ${{ fromJson(needs.test-push-containers.outputs.images) }} - steps: - - name: Checkout ${{ inputs.repo }} - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - repository: ${{ inputs.repo }} - ref: ${{ inputs.ref }} - - - name: Checkout github-actions - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - repository: strimzi/github-actions - ref: ${{ inputs.githubActionsRef || github.sha }} - path: github-actions - - - name: Setup actions for testing - run: | - mkdir -p .github/actions - cp -r github-actions/.github/actions/* .github/actions/ - - - name: Download SBOM artifact - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: SBOMs-${{ inputs.artifactSuffix }}-latest.tar.gz - - - name: Extract SBOMs - run: tar -xzf sbom.tar.gz - - - name: Attest container image provenance - uses: ./.github/actions/build/attest-artifact - with: - mode: oci - subject-prefix: registry.strimzi:443/strimzi - image-name: ${{ matrix.image.name }} - subject-digest: ${{ matrix.image.digest }} - - - name: Attest container SBOM - uses: ./.github/actions/build/attest-artifact - with: - mode: sbom - subject-prefix: registry.strimzi:443/strimzi - image-name: ${{ matrix.image.name }} - subject-digest: ${{ matrix.image.digest }} - sbom-path: ./${{ matrix.image.sbom }} - - - name: Verify attestations were created - env: - GH_TOKEN: ${{ github.token }} - run: | - DIGEST="${{ matrix.image.digest }}" - ATTESTATIONS=$(gh api "repos/${{ github.repository }}/attestations/${DIGEST}" --jq '.attestations | length') - if [ "$ATTESTATIONS" -eq 0 ]; then - echo "❌ No attestations found for ${DIGEST}" - exit 1 - fi - echo "✓ Found $ATTESTATIONS attestation(s) for ${{ matrix.image.name }} @ ${DIGEST}" - test-release-artifacts: name: Release Artifacts needs: From 1c6127af57172ed78abc59c119a853007dc9a2e4 Mon Sep 17 00:00:00 2001 From: Jakub Stejskal Date: Tue, 21 Jul 2026 12:04:23 +0200 Subject: [PATCH 6/9] Add docs about attestation Signed-off-by: Jakub Stejskal --- README.md | 90 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/README.md b/README.md index c325c90..ea3b325 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ Actions for building, testing, and releasing Strimzi components. | `build/deploy-java` | Deploys Java artifacts to Maven Central | `projects` (required), `settingsPath` (required) | | `build/release-artifacts` | Builds release artifacts using Makefile | `releaseVersion` (required), `artifactSuffix` (required) | | `build/publish-helm-chart` | Publishes Helm Chart as OCI artifact | `releaseVersion` (required), `helmChartName` (required) | +| `build/attest-artifact` | Creates provenance or SBOM attestation for artifacts | `mode` (required: blob/oci/sbom), see [Artifact Attestation](#artifact-attestation) | > [!IMPORTANT] > Build actions do **not** install their own dependencies (Java, yq, Helm, Docker, Shellcheck, Syft, etc.). @@ -53,6 +54,94 @@ Actions for building, testing, and releasing Strimzi components. > The `build-binaries` action supports an `clusterOperatorBuild` input (default `false`) that enables Strimzi Kafka Operator specific build steps — Helm chart generation, CRD distribution, dashboard setup, documentation checks, and uncommitted changes verification. > Other repositories should leave this disabled. +### Artifact Attestation + +The `attest-artifact` action creates [SLSA build provenance](https://slsa.dev/provenance/v1) and [SBOM attestations](https://spdx.dev/Document/v2.3) using GitHub's Attestation API ([`actions/attest`](https://github.com/actions/attest)). +Attestations are signed with Sigstore (keyless, via GitHub OIDC) and stored in the GitHub Attestation API. +Consumers verify with `gh attestation verify`. + +The action supports three modes: + +| Mode | Use case | Key inputs | +|--------|-------------------------------------|---------------------------------------------------------| +| `blob` | Release archives (.tar.gz, .zip) | `subject-path` (glob) | +| `oci` | Container/Helm image provenance | `subject-prefix`, `image-name`, `subject-digest` | +| `sbom` | SBOM attestation for OCI image | `subject-prefix`, `image-name`, `subject-digest`, `sbom-path` | + +#### Built-in attestation + +The following actions include attestation automatically (on push events only, skipped on PRs): + +- **`release-artifacts`** — attests release archives in `blob` mode, includes `.intoto.jsonl` provenance bundle in the release tarball (required for [OpenSSF Scorecard Signed-Releases](https://github.com/ossf/scorecard/blob/main/docs/checks.md#signed-releases) check) +- **`publish-helm-chart`** — attests the Helm OCI artifact in `oci` mode after `helm push` + +#### Container image attestation + +Container images require a separate attestation job because each image needs its own `actions/attest` call. +The `push-containers` action discovers images from the SBOM directory and outputs a JSON array for use in a matrix job. + +**Required permissions** in the release workflow: + +```yaml +permissions: + contents: read + id-token: write # OIDC token for Sigstore signing + attestations: write # GitHub Attestation API +``` + +**Example** — add to your project's `release.yml` after the push-containers job: + +```yaml + attest-containers: + needs: push-containers + runs-on: ubuntu-latest + permissions: + id-token: write + attestations: write + strategy: + fail-fast: false + matrix: + image: ${{ fromJson(needs.push-containers.outputs.images) }} + steps: + - name: Download SBOM artifact + uses: actions/download-artifact@v4 + with: + name: SBOMs-operators-${{ env.RELEASE_VERSION }}.tar.gz + + - name: Extract SBOMs + run: tar -xzf sbom.tar.gz + + - name: Attest container provenance + uses: ./.github/actions/build/attest-artifact + with: + mode: oci + subject-prefix: quay.io/strimzi + image-name: ${{ matrix.image.name }} + subject-digest: ${{ matrix.image.digest }} + + - name: Attest container SBOM + uses: ./.github/actions/build/attest-artifact + with: + mode: sbom + subject-prefix: quay.io/strimzi + image-name: ${{ matrix.image.name }} + subject-digest: ${{ matrix.image.digest }} + sbom-path: ./${{ matrix.image.sbom }} +``` + +#### Verification + +```bash +# Release archives +gh attestation verify --repo strimzi/ + +# Container images +gh attestation verify oci://quay.io/strimzi/@ --repo strimzi/ + +# Helm charts +gh attestation verify oci://quay.io/strimzi-helm/@ --repo strimzi/ +``` + ### Security Actions Actions for security scanning of dependencies and container images. @@ -142,6 +231,7 @@ on: permissions: contents: read id-token: write + attestations: write concurrency: group: ${{ github.workflow }}-${{ github.ref }} From 2886e66bfe5cf81780cbed5565d8c6dc108b172e Mon Sep 17 00:00:00 2001 From: Jakub Stejskal Date: Tue, 21 Jul 2026 14:27:20 +0200 Subject: [PATCH 7/9] Add dryRun option for testing Signed-off-by: Jakub Stejskal --- .../actions/build/attest-artifact/action.yml | 43 +++++++++++-------- .../build/publish-helm-chart/action.yml | 11 +++-- .../build/release-artifacts/action.yml | 11 +++-- .../workflows/reusable-test-integrations.yml | 2 + README.md | 20 ++++----- 5 files changed, 52 insertions(+), 35 deletions(-) diff --git a/.github/actions/build/attest-artifact/action.yml b/.github/actions/build/attest-artifact/action.yml index 566561d..33dc44e 100644 --- a/.github/actions/build/attest-artifact/action.yml +++ b/.github/actions/build/attest-artifact/action.yml @@ -5,24 +5,28 @@ inputs: mode: description: "Attestation mode: blob (file artifacts), oci (container/Helm provenance), or sbom (SBOM attestation for OCI image)" required: true - subject-path: + subjectPath: description: "Path/glob to file artifacts (blob mode only)" required: false - subject-prefix: + subjectPrefix: description: "Registry/org prefix for OCI artifacts, e.g. quay.io/strimzi (oci and sbom modes)" required: false - image-name: + imageName: description: "Image name, e.g. operator (oci and sbom modes)" required: false - subject-digest: + subjectDigest: description: "Image digest, e.g. sha256:abc123 (oci and sbom modes)" required: false - sbom-path: + sbomPath: description: "Path to SPDX/CycloneDX SBOM JSON file (sbom mode only)" required: false + dryRun: + description: "Log what would be attested without creating real attestations. This should be changed only in case of testing." + required: false + default: "false" outputs: - bundle-path: + bundlePath: description: "Path to the generated attestation bundle" value: ${{ steps.attest-blob.outputs.bundle-path || steps.attest-oci.outputs.bundle-path || steps.attest-sbom.outputs.bundle-path }} @@ -31,40 +35,41 @@ runs: steps: - name: Attest file artifact id: attest-blob - if: ${{ inputs.mode == 'blob' }} + if: ${{ inputs.mode == 'blob' && inputs.dryRun != 'true' }} uses: actions/attest@f7c74d28b9d84cb8768d0b8ca14a4bac6ef463e6 # v4.2.0 with: - subject-path: ${{ inputs.subject-path }} + subject-path: ${{ inputs.subjectPath }} - name: Attest OCI artifact provenance id: attest-oci - if: ${{ inputs.mode == 'oci' }} + if: ${{ inputs.mode == 'oci' && inputs.dryRun != 'true' }} uses: actions/attest@f7c74d28b9d84cb8768d0b8ca14a4bac6ef463e6 # v4.2.0 with: - subject-name: ${{ inputs.subject-prefix }}/${{ inputs.image-name }} - subject-digest: ${{ inputs.subject-digest }} + subject-name: ${{ inputs.subjectPrefix }}/${{ inputs.imageName }} + subject-digest: ${{ inputs.subjectDigest }} - name: Attest SBOM for OCI artifact id: attest-sbom - if: ${{ inputs.mode == 'sbom' }} + if: ${{ inputs.mode == 'sbom' && inputs.dryRun != 'true' }} uses: actions/attest@f7c74d28b9d84cb8768d0b8ca14a4bac6ef463e6 # v4.2.0 with: - subject-name: ${{ inputs.subject-prefix }}/${{ inputs.image-name }} - subject-digest: ${{ inputs.subject-digest }} - sbom-path: ${{ inputs.sbom-path }} + subject-name: ${{ inputs.subjectPrefix }}/${{ inputs.imageName }} + subject-digest: ${{ inputs.subjectDigest }} + sbom-path: ${{ inputs.sbomPath }} - name: Attestation summary + if: ${{ inputs.dryRun != 'true' }} shell: bash run: | echo "::group::Attestation Summary" echo "Mode: ${{ inputs.mode }}" if [ "${{ inputs.mode }}" = "blob" ]; then - echo "Subject: ${{ inputs.subject-path }}" + echo "Subject: ${{ inputs.subjectPath }}" else - echo "Subject: ${{ inputs.subject-prefix }}/${{ inputs.image-name }}@${{ inputs.subject-digest }}" + echo "Subject: ${{ inputs.subjectPrefix }}/${{ inputs.imageName }}@${{ inputs.subjectDigest }}" fi if [ "${{ inputs.mode }}" = "sbom" ]; then - echo "SBOM: ${{ inputs.sbom-path }}" + echo "SBOM: ${{ inputs.sbomPath }}" fi URL="${{ steps.attest-blob.outputs.attestation-url }}${{ steps.attest-oci.outputs.attestation-url }}${{ steps.attest-sbom.outputs.attestation-url }}" echo "Attestation URL: ${URL}" @@ -73,6 +78,6 @@ runs: if [ "${{ inputs.mode }}" = "blob" ]; then echo " gh attestation verify --repo ${{ github.repository }}" else - echo " gh attestation verify oci://${{ inputs.subject-prefix }}/${{ inputs.image-name }}@${{ inputs.subject-digest }} --repo ${{ github.repository }}" + echo " gh attestation verify oci://${{ inputs.subjectPrefix }}/${{ inputs.imageName }}@${{ inputs.subjectDigest }} --repo ${{ github.repository }}" fi echo "::endgroup::" diff --git a/.github/actions/build/publish-helm-chart/action.yml b/.github/actions/build/publish-helm-chart/action.yml index 7146180..4c031bd 100644 --- a/.github/actions/build/publish-helm-chart/action.yml +++ b/.github/actions/build/publish-helm-chart/action.yml @@ -25,6 +25,10 @@ inputs: registryPassword: description: "Container registry password" required: true + attestDryRun: + description: "Run attestation in dry-run mode (log only, no real attestation)" + required: false + default: "false" runs: using: "composite" @@ -57,6 +61,7 @@ runs: uses: ./.github/actions/build/attest-artifact with: mode: oci - subject-prefix: ${{ inputs.containerRegistry }}/${{ inputs.containerOrg }} - image-name: ${{ inputs.helmChartName }} - subject-digest: ${{ steps.helm-push.outputs.digest }} \ No newline at end of file + subjectPrefix: ${{ inputs.containerRegistry }}/${{ inputs.containerOrg }} + imageName: ${{ inputs.helmChartName }} + subjectDigest: ${{ steps.helm-push.outputs.digest }} + dryRun: ${{ inputs.attestDryRun }} \ No newline at end of file diff --git a/.github/actions/build/release-artifacts/action.yml b/.github/actions/build/release-artifacts/action.yml index b28cd7c..20bd6c8 100644 --- a/.github/actions/build/release-artifacts/action.yml +++ b/.github/actions/build/release-artifacts/action.yml @@ -8,6 +8,10 @@ inputs: artifactSuffix: description: "Suffix of archive with images" required: true + attestDryRun: + description: "Run attestation in dry-run mode (log only, no real attestation)" + required: false + default: "false" runs: using: "composite" @@ -55,13 +59,14 @@ runs: uses: ./.github/actions/build/attest-artifact with: mode: blob - subject-path: ${{ steps.find-archives.outputs.files }} + subjectPath: ${{ steps.find-archives.outputs.files }} + dryRun: ${{ inputs.attestDryRun }} # Copy attestation provenance file in .intoto.jsonl format to add it into release tarball - name: Copy attestation bundle - if: ${{ github.event_name != 'pull_request' && steps.find-archives.outputs.found == 'true' }} + if: ${{ github.event_name != 'pull_request' && steps.find-archives.outputs.found == 'true' && inputs.attestDryRun != 'true' }} shell: bash - run: cp "${{ steps.attest.outputs.bundle-path }}" "./release-${{ inputs.releaseVersion }}.intoto.jsonl" + run: cp "${{ steps.attest.outputs.bundlePath }}" "./release-${{ inputs.releaseVersion }}.intoto.jsonl" - name: Create release tarball shell: bash diff --git a/.github/workflows/reusable-test-integrations.yml b/.github/workflows/reusable-test-integrations.yml index 784eada..3c3acdc 100644 --- a/.github/workflows/reusable-test-integrations.yml +++ b/.github/workflows/reusable-test-integrations.yml @@ -411,6 +411,7 @@ jobs: with: artifactSuffix: "${{ inputs.artifactSuffix }}" releaseVersion: ${{ inputs.releaseVersion }} + attestDryRun: "true" - name: Verify release outputs run: | @@ -505,3 +506,4 @@ jobs: helmChartName: ${{ inputs.helmChartName}} releaseVersion: ${{ inputs.releaseVersion }} artifactSuffix: ${{ inputs.artifactSuffix }} + attestDryRun: "true" diff --git a/README.md b/README.md index ea3b325..5f740b0 100644 --- a/README.md +++ b/README.md @@ -64,9 +64,9 @@ The action supports three modes: | Mode | Use case | Key inputs | |--------|-------------------------------------|---------------------------------------------------------| -| `blob` | Release archives (.tar.gz, .zip) | `subject-path` (glob) | -| `oci` | Container/Helm image provenance | `subject-prefix`, `image-name`, `subject-digest` | -| `sbom` | SBOM attestation for OCI image | `subject-prefix`, `image-name`, `subject-digest`, `sbom-path` | +| `blob` | Release archives (.tar.gz, .zip) | `subjectPath` (glob) | +| `oci` | Container/Helm image provenance | `subjectPrefix`, `imageName`, `subjectDigest` | +| `sbom` | SBOM attestation for OCI image | `subjectPrefix`, `imageName`, `subjectDigest`, `sbomPath` | #### Built-in attestation @@ -115,18 +115,18 @@ permissions: uses: ./.github/actions/build/attest-artifact with: mode: oci - subject-prefix: quay.io/strimzi - image-name: ${{ matrix.image.name }} - subject-digest: ${{ matrix.image.digest }} + subjectPrefix: quay.io/strimzi + imageName: ${{ matrix.image.name }} + subjectDigest: ${{ matrix.image.digest }} - name: Attest container SBOM uses: ./.github/actions/build/attest-artifact with: mode: sbom - subject-prefix: quay.io/strimzi - image-name: ${{ matrix.image.name }} - subject-digest: ${{ matrix.image.digest }} - sbom-path: ./${{ matrix.image.sbom }} + subjectPrefix: quay.io/strimzi + imageName: ${{ matrix.image.name }} + subjectDigest: ${{ matrix.image.digest }} + sbomPath: ./${{ matrix.image.sbom }} ``` #### Verification From fbf823bb1d50d7394f654761730233ef2dcdd866 Mon Sep 17 00:00:00 2001 From: Jakub Stejskal Date: Tue, 21 Jul 2026 14:50:02 +0200 Subject: [PATCH 8/9] Remove attestation verification Signed-off-by: Jakub Stejskal --- .github/workflows/reusable-test-integrations.yml | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/.github/workflows/reusable-test-integrations.yml b/.github/workflows/reusable-test-integrations.yml index 3c3acdc..27be203 100644 --- a/.github/workflows/reusable-test-integrations.yml +++ b/.github/workflows/reusable-test-integrations.yml @@ -431,19 +431,6 @@ jobs: tar -tf "$TARBALL" echo "✓ Release artifacts created successfully" - - name: Verify attestation bundle in tarball - if: ${{ github.event_name != 'pull_request' }} - run: | - TARBALL="release-${{ inputs.artifactSuffix }}-${{ inputs.releaseVersion }}.tar" - INTOTO_COUNT=$(tar -tf "$TARBALL" | grep -c '\.intoto\.jsonl$' || true) - if [ "$INTOTO_COUNT" -eq 0 ]; then - echo "❌ No .intoto.jsonl attestation found in release tarball" - tar -tf "$TARBALL" - exit 1 - fi - echo "✓ Found $INTOTO_COUNT attestation bundle(s) in release tarball:" - tar -tf "$TARBALL" | grep '\.intoto\.jsonl$' - # This check is currently only for Operators repo as yamls were missing in release tar ball - name: Verify YAML release artifacts in tarball if: ${{ inputs.clusterOperatorBuild == true }} From 8f198679a9cb4db743e73a21792170623baa4b5e Mon Sep 17 00:00:00 2001 From: Jakub Stejskal Date: Tue, 21 Jul 2026 15:03:25 +0200 Subject: [PATCH 9/9] Remove testing branch Signed-off-by: Jakub Stejskal --- .github/workflows/test-integrations.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/test-integrations.yml b/.github/workflows/test-integrations.yml index cb04874..e97a41b 100644 --- a/.github/workflows/test-integrations.yml +++ b/.github/workflows/test-integrations.yml @@ -8,7 +8,6 @@ on: branches: - "main" - "release-*" - - "add-attestation-action" # Declare default permissions as read only permissions: