Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions .github/actions/build/attest-artifact/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
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
subjectPath:
description: "Path/glob to file artifacts (blob mode only)"
required: false
subjectPrefix:
description: "Registry/org prefix for OCI artifacts, e.g. quay.io/strimzi (oci and sbom modes)"
required: false
imageName:
description: "Image name, e.g. operator (oci and sbom modes)"
required: false
subjectDigest:
description: "Image digest, e.g. sha256:abc123 (oci and sbom modes)"
required: false
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:
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 }}

runs:
using: "composite"
steps:
- name: Attest file artifact
id: attest-blob
if: ${{ inputs.mode == 'blob' && inputs.dryRun != 'true' }}
uses: actions/attest@f7c74d28b9d84cb8768d0b8ca14a4bac6ef463e6 # v4.2.0
with:
subject-path: ${{ inputs.subjectPath }}

- name: Attest OCI artifact provenance
id: attest-oci
if: ${{ inputs.mode == 'oci' && inputs.dryRun != 'true' }}
uses: actions/attest@f7c74d28b9d84cb8768d0b8ca14a4bac6ef463e6 # v4.2.0
with:
subject-name: ${{ inputs.subjectPrefix }}/${{ inputs.imageName }}
subject-digest: ${{ inputs.subjectDigest }}

- name: Attest SBOM for OCI artifact
id: attest-sbom
if: ${{ inputs.mode == 'sbom' && inputs.dryRun != 'true' }}
uses: actions/attest@f7c74d28b9d84cb8768d0b8ca14a4bac6ef463e6 # v4.2.0
with:
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.subjectPath }}"
else
echo "Subject: ${{ inputs.subjectPrefix }}/${{ inputs.imageName }}@${{ inputs.subjectDigest }}"
fi
if [ "${{ inputs.mode }}" = "sbom" ]; then
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}"
echo ""
echo "To verify this attestation, run:"
if [ "${{ inputs.mode }}" = "blob" ]; then
echo " gh attestation verify <artifact-file> --repo ${{ github.repository }}"
else
echo " gh attestation verify oci://${{ inputs.subjectPrefix }}/${{ inputs.imageName }}@${{ inputs.subjectDigest }} --repo ${{ github.repository }}"
fi
echo "::endgroup::"
21 changes: 20 additions & 1 deletion .github/actions/build/publish-helm-chart/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -44,5 +48,20 @@ 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 }}
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
subjectPrefix: ${{ inputs.containerRegistry }}/${{ inputs.containerOrg }}
imageName: ${{ inputs.helmChartName }}
subjectDigest: ${{ steps.helm-push.outputs.digest }}
dryRun: ${{ inputs.attestDryRun }}
25 changes: 25 additions & 0 deletions .github/actions/build/push-containers/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -131,6 +136,26 @@ 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/}"
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}]')
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
Expand Down
44 changes: 43 additions & 1 deletion .github/actions/build/release-artifacts/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -27,13 +31,51 @@ 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<<EOF"
echo "$FILES"
echo "EOF"
} >> "$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' && steps.find-archives.outputs.found == 'true' }}
id: attest
uses: ./.github/actions/build/attest-artifact
with:
mode: blob
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' && inputs.attestDryRun != 'true' }}
shell: bash
run: cp "${{ steps.attest.outputs.bundlePath }}" "./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
Expand Down
20 changes: 19 additions & 1 deletion .github/workflows/reusable-test-integrations.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand All @@ -349,6 +353,18 @@ 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-release-artifacts:
name: Release Artifacts
needs:
Expand Down Expand Up @@ -395,6 +411,7 @@ jobs:
with:
artifactSuffix: "${{ inputs.artifactSuffix }}"
releaseVersion: ${{ inputs.releaseVersion }}
attestDryRun: "true"

- name: Verify release outputs
run: |
Expand Down Expand Up @@ -476,3 +493,4 @@ jobs:
helmChartName: ${{ inputs.helmChartName}}
releaseVersion: ${{ inputs.releaseVersion }}
artifactSuffix: ${{ inputs.artifactSuffix }}
attestDryRun: "true"
3 changes: 2 additions & 1 deletion .github/workflows/test-integrations.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading
Loading