From 8d7c0b47d27b6daf46da62d24b4340c1b79a5f78 Mon Sep 17 00:00:00 2001 From: Kevin Stellmacher Date: Fri, 16 Oct 2026 13:58:39 +0200 Subject: [PATCH 1/4] ci: publish per-architecture SBOMs as signed release assets Closes #30 Signed-off-by: Kevin Stellmacher --- .github/workflows/docker.yaml | 100 +++++++++++++++++++++++++++------- 1 file changed, 79 insertions(+), 21 deletions(-) diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml index 34a6ff8..958883d 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -2,6 +2,9 @@ name: Build/Publish Docker Image permissions: read-all +env: + PLATFORMS: linux/amd64,linux/arm64 + on: release: types: @@ -77,8 +80,13 @@ jobs: uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0 with: context: . - platforms: linux/amd64,linux/arm64 + platforms: ${{ env.PLATFORMS }} push: true + # BuildKit attaches one SBOM attestation per platform, which is what + # a multi-arch image needs. actions/attest can only ever name a + # single subject-digest, so it would have to pair one architecture's + # SBOM with the shared index digest. + sbom: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} target: ${{ matrix.image.target }} @@ -104,30 +112,80 @@ jobs: --yes \ "${tag}@${IMAGE_DIGEST}" done - - name: Extract first tag - id: first-tag + - name: Install syft + id: syft + uses: anchore/sbom-action/download-syft@3ad7283483fc7af8ff2b4ea19663c2d5ca935e26 # v0.24.2 + # Release assets, not attestations: the registry gets BuildKit's SPDX + # attestation per platform, the release gets CycloneDX files that the + # OCM pipeline can embed. syft resolves the index to the runner's own + # platform unless told otherwise, so ask for each one explicitly. + - name: Generate per-architecture SBOMs env: - TAGS: ${{ steps.meta.outputs.tags }} + SYFT: ${{ steps.syft.outputs.cmd }} + IMAGE: ghcr.io/${{ github.repository_owner }}/${{ matrix.image.name }} + DIGEST: ${{ steps.image.outputs.digest }} + NAME: ${{ matrix.image.name }} run: | - mapfile -t TAG_LIST <<< "$TAGS" - echo "tag=${TAG_LIST[0]}" >> "$GITHUB_OUTPUT" - - name: Generate SBOM - uses: anchore/sbom-action@3ad7283483fc7af8ff2b4ea19663c2d5ca935e26 # v0.24.2 - with: - image: ${{ steps.first-tag.outputs.tag }} - format: 'cyclonedx-json' - output-file: 'sbom.cyclonedx.json' - upload-release-assets: false - - name: Attest SBOM - uses: actions/attest-sbom@c604332985a26aa8cf1bdc465b92731239ec6b9e # v4.1.0 - with: - subject-name: ghcr.io/${{ github.repository_owner }}/${{ matrix.image.name }} - subject-digest: ${{ steps.image.outputs.digest }} - sbom-path: 'sbom.cyclonedx.json' - push-to-registry: true + set -euo pipefail + mkdir -p sboms + for p in ${PLATFORMS//,/ }; do + "$SYFT" scan --platform "$p" "${IMAGE}@${DIGEST}" \ + -o "cyclonedx-json=sboms/${NAME}-${p//\//-}.cyclonedx.json" + done - name: Attest provenance - uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 + uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 with: subject-name: ghcr.io/${{ github.repository_owner }}/${{ matrix.image.name }} subject-digest: ${{ steps.image.outputs.digest }} push-to-registry: true + create-storage-record: false + - name: Sign SBOMs + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') + run: | + set -euo pipefail + # The .sigstore.json suffix is what OpenSSF Scorecard's + # Signed-Releases check looks for. + for f in sboms/*.cyclonedx.json; do + cosign sign-blob --yes --new-bundle-format=true \ + --bundle "${f}.sigstore.json" "$f" + done + - name: Upload SBOMs for the release job + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: sboms-${{ matrix.image.name }} + path: sboms/ + retention-days: 1 + + # Immutable releases forbid uploading assets to an already-published release, + # so the SBOMs are attached while the release is still a draft. Publishing it + # stays a deliberate manual step (UI, or `gh release edit --draft=false`). + release: + name: Attach SBOMs to draft release + needs: buildAndPush + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Download SBOMs from the image jobs + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + with: + pattern: sboms-* + merge-multiple: true + path: sboms + - name: Create draft release with SBOMs attached + env: + GH_TOKEN: ${{ github.token }} + # No checkout in this job, so gh cannot infer the repository. + GH_REPO: ${{ github.repository }} + TAG: ${{ github.ref_name }} + run: | + set -euo pipefail + # Re-runs land on an existing draft; uploading to one is still + # allowed, only published releases are immutable. + if gh release view "$TAG" >/dev/null 2>&1; then + gh release upload "$TAG" sboms/* --clobber + else + gh release create "$TAG" sboms/* --draft --generate-notes + fi From 59175610ff22ed52f9a2773d328d60da6b69b0a5 Mon Sep 17 00:00:00 2001 From: Kevin Stellmacher Date: Fri, 16 Oct 2026 13:59:55 +0200 Subject: [PATCH 2/4] ci: adopt the release drafter from dependency-controller Refs #30 Signed-off-by: Kevin Stellmacher --- .github/release-drafter.yml | 60 ++++++++++++++++++++++++++ .github/workflows/release-drafter.yaml | 28 ++++++++++++ 2 files changed, 88 insertions(+) create mode 100644 .github/release-drafter.yml create mode 100644 .github/workflows/release-drafter.yaml diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 0000000..d89c9f4 --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,60 @@ +name-template: "$RESOLVED_VERSION" +tag-template: "$RESOLVED_VERSION" +version-template: "v$MAJOR.$MINOR.$PATCH" + +# Labels are applied by hand, so every category below is opt-in. Pull requests +# without a matching label are still listed, just without a section heading — +# their conventional-commit title prefix already says what they are. +categories: + - title: "Features" + when: + labels: + - "feat" + - "feature" + - "enhancement" + - title: "Bug Fixes" + when: + labels: + - "fix" + - "bugfix" + - "bug" + - title: "Maintenance" + when: + labels: + - "docs" + - "doc" + - "documentation" + - "chore" + - title: "Excluded" + type: pre-exclude + when: + labels: + - "skip-changelog" + - title: "Major Version" + type: version-resolver + semver-increment: major + when: + labels: + - "major" + - title: "Minor Version" + type: version-resolver + semver-increment: minor + when: + labels: + - "minor" + - title: "Patch Version" + type: version-resolver + semver-increment: patch + when: + labels: + - "patch" + +change-template: "- $TITLE @$AUTHOR (#$NUMBER)" +change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks. +sort-by: title +sort-direction: descending + +template: | + ## Changes + + $CHANGES diff --git a/.github/workflows/release-drafter.yaml b/.github/workflows/release-drafter.yaml new file mode 100644 index 0000000..4d7ed64 --- /dev/null +++ b/.github/workflows/release-drafter.yaml @@ -0,0 +1,28 @@ +name: Release Drafter + +on: + push: + branches: + - main + +permissions: + contents: read + +jobs: + # There is deliberately no pull_request trigger. Since v7 the root action only + # drafts releases and no longer autolabels, so a PR run would do nothing but + # re-create the draft release — which fails outright on PRs from forks, where + # GitHub forces GITHUB_TOKEN to read-only regardless of the permissions below. + # Changelog categories are driven by labels applied by hand. + update_release_draft: + permissions: + contents: write # write permission is required to create a github release + # Reading merged pull requests only works implicitly while the repo is + # public; grant it so the drafter keeps working if that ever changes. + pull-requests: read + runs-on: ubuntu-24.04 + steps: + # Drafts your next Release notes as Pull Requests are merged into "main" + - uses: release-drafter/release-drafter@34d80673e067bdc0c24568d3af899c216adcfaa9 # v7 + with: + commitish: main From 4726604048312b292718e71bc5cc72a1ed957d2b Mon Sep 17 00:00:00 2001 From: Kevin Stellmacher Date: Fri, 16 Oct 2026 14:00:21 +0200 Subject: [PATCH 3/4] chore: add codeowners file Signed-off-by: Kevin Stellmacher --- .github/CODEOWNERS | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..0f09a05 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,2 @@ +# These owners will be the default owners for everything in the repo. +* @opendefensecloud/cat \ No newline at end of file From 7694f5d83186fc6afca354c405a2e96bac8f9fa0 Mon Sep 17 00:00:00 2001 From: Kevin Stellmacher Date: Fri, 16 Oct 2026 14:04:34 +0200 Subject: [PATCH 4/4] ci: pin release-drafter to v7.10.0 Refs #30 Signed-off-by: Kevin Stellmacher --- .github/workflows/release-drafter.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-drafter.yaml b/.github/workflows/release-drafter.yaml index 4d7ed64..4390d14 100644 --- a/.github/workflows/release-drafter.yaml +++ b/.github/workflows/release-drafter.yaml @@ -23,6 +23,6 @@ jobs: runs-on: ubuntu-24.04 steps: # Drafts your next Release notes as Pull Requests are merged into "main" - - uses: release-drafter/release-drafter@34d80673e067bdc0c24568d3af899c216adcfaa9 # v7 + - uses: release-drafter/release-drafter@978bb0b30c5ee8cdd332ced253758157150d43f0 # v7.10.0 with: commitish: main