From ed9ed6da10ac94753663a0e89f5e8cdf1f12291a Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 08:43:34 +0000 Subject: [PATCH 1/2] ci(release)!: sign checksums.txt as a Sigstore bundle for cosign v3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cosign v3 turns the standardized bundle on by default, and that default is what broke the old signing config rather than any removed flag. Under --new-bundle-format the --output-signature/--output-certificate pair this repo passed is *silently ignored* — cosign says "deprecated when using --new-bundle-format and will be ignored" and exits fine — so the release would have published no signature at all. Worse, release.yml only runs on a v* tag, so no PR check could have caught it: the first symptom would have been a signature- less release under a tag that is immutable and cannot be re-cut. signs: now writes checksums.txt.bundle, and names --new-bundle-format explicitly even though v3 defaults to it, so a future default flip cannot repeat the same silent failure. The cosign-installer moves to v4.1.2, which installs v3; the tool version stays unpinned because the installer and the format move together. The bundle also carries the transparency-log inclusion proof, which upgrades the air-gapped procedure instead of merely preserving it. cosign v3 deprecates --offline in favour of a --trusted-root file, so SECURITY.md now documents capturing one with `cosign trusted-root create --with-default-services` on a connected host and verifying inside the enclave with no network at all. The old enclave-side command there also dropped the identity flags, verifying only that *somebody* had signed; the new one keeps them. prune-release-assets.yml keeps its assets by suffix, so its jq keep-list learns .bundle — without that the first prune would have deleted the one file the whole workflow exists to retain. Both spellings stay on the list, since releases up to v0.22.0 carry the detached pair and a v3 client still verifies those by falling back automatically. BREAKING CHANGE: the cosign signature over checksums.txt ships as checksums.txt.bundle instead of checksums.txt.sig + checksums.txt.pem. Verify with `cosign verify-blob checksums.txt --bundle checksums.txt.bundle` and the same two identity flags; this needs cosign v3+, or v2.6+ with an explicit --new-bundle-format. Releases up to v0.22.0 are unaffected and still verify with --certificate/--signature. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01DRBThE7bUn5AR4KsNi78yw --- .github/workflows/prune-release-assets.yml | 23 ++++++----- .github/workflows/release.yml | 15 ++++--- .goreleaser.yaml | 27 ++++++++----- AGENTS.md | 23 +++++++++-- README.md | 28 ++++++++----- SECURITY.md | 47 ++++++++++++++++------ 6 files changed, 115 insertions(+), 48 deletions(-) diff --git a/.github/workflows/prune-release-assets.yml b/.github/workflows/prune-release-assets.yml index 6f487c0..48413f5 100644 --- a/.github/workflows/prune-release-assets.yml +++ b/.github/workflows/prune-release-assets.yml @@ -8,7 +8,8 @@ name: prune-release-assets # Every release publishes ~6 archives at ~15.8 MB of binary each plus 4 # .rpm/.deb packages; that is the entire storage cost. Alongside them it # publishes a handful of kilobyte-sized files: checksums.txt, its keyless cosign -# signature (checksums.txt.sig) and certificate (checksums.txt.pem), and one +# signature (checksums.txt.bundle — and, on releases up to v0.22.0, the earlier +# detached checksums.txt.sig / checksums.txt.pem pair), and one # SPDX SBOM per archive/package. Those small files are the integrity and # supply-chain record an air-gapped / FSTEC-regulated consumer may have to # produce years after the binaries are gone — and because builds in this repo @@ -20,12 +21,15 @@ name: prune-release-assets # were kilobytes. # # So this workflow deletes ONLY *.tar.gz, *.zip, *.rpm, *.deb, and it keeps -# checksums.txt, *.sig, *.pem, the *_sboms.tar.gz bundle and any legacy -# *.spdx.json unconditionally (belt-and-braces: -# there is both a delete-list and an explicit always-keep list below). Do NOT +# checksums.txt, its signature (*.bundle, plus the legacy *.sig / *.pem pair), +# the *_sboms.tar.gz bundle and any legacy *.spdx.json unconditionally +# (belt-and-braces: there is both a delete-list and an explicit always-keep list +# below). Both signature spellings are on the keep-list because the two eras of +# releases carry different ones and this workflow runs across all of them. Do NOT # "simplify" this later into deleting every asset — that trades the entire audit # trail for zero additional bytes and is the one thing the design exists to -# prevent. +# prevent. Whenever the `signs:` block in .goreleaser.yaml changes the name it +# writes, the keep-list below has to learn the new suffix in the same commit. # # It never deletes a release and never touches a tag: tags and release notes are # the project's history, v* tags are immutable (a tag ruleset enforces it), and @@ -171,7 +175,7 @@ jobs: def always_keep: . == "checksums.txt" or endswith(".spdx.json") or endswith("_sboms.tar.gz") - or endswith(".sig") or endswith(".pem"); + or endswith(".bundle") or endswith(".sig") or endswith(".pem"); ($protected[0]) as $keepTags | .[] | select(.draft | not) # never touch drafts @@ -196,8 +200,8 @@ jobs: echo echo "> Pruned versions can no longer be installed via \`brew install ftarasenko/tap/koc\`," echo "> and any mirror that downloads koc binaries from GitHub on demand will 404 for them." - echo "> Tags, release notes, \`checksums.txt\`(+\`.sig\`/\`.pem\`) and the SPDX SBOMs are retained," - echo "> and builds are byte-reproducible from the tag." + echo "> Tags, release notes, \`checksums.txt\`(+ its \`.bundle\`, or \`.sig\`/\`.pem\` on older" + echo "> releases) and the SPDX SBOMs are retained, and builds are byte-reproducible from the tag." fi echo echo "Keep window: **$KEEP** most recent non-draft, non-prerelease releases; latest (\`${LATEST_TAG:-none}\`) always kept." @@ -295,7 +299,8 @@ jobs: printf '> **Build artifacts pruned to save storage.** The archives and packages\n' printf '> (`.tar.gz`, `.zip`, `.rpm`, `.deb`) for this release have been deleted. The tag,\n' printf '> these release notes, `checksums.txt` with its cosign signature\n' - printf '> (`checksums.txt.sig`/`.pem`) and the SPDX SBOMs are retained. Builds are\n' + printf '> (`checksums.txt.bundle`, or `.sig`/`.pem` on releases up to v0.22.0) and the\n' + printf '> SPDX SBOMs are retained. Builds are\n' printf '> byte-reproducible from tag `%s`, so the binaries can be rebuilt and verified\n' "$tag" printf '> against the retained `checksums.txt`. Consequence: `brew install` and any\n' printf '> on-demand mirror will no longer find binaries for this version — use a newer\n' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 040adae..0855283 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -91,11 +91,16 @@ jobs: # cosign and syft are required by the `signs:` and `sboms:` blocks in # .goreleaser.yaml. Both are official installers from the projects - # themselves. - # Pinned to full commit SHAs per repo convention. Both are still on their - # original major here; Dependabot's github-actions ecosystem proposes major - # bumps as reviewable PRs (cosign-installer v4 and sbom-action have both - # moved on), which is the right place to verify changed inputs. + # themselves, pinned to full commit SHAs per repo convention. + # + # cosign-installer v4 installs cosign v3 by default, and v3 turns the + # standardized Sigstore bundle on by default — which is why the `signs:` + # block writes checksums.txt.bundle rather than a detached .sig/.pem pair. + # The version is deliberately NOT pinned via `cosign-release`: the two + # move together, and letting the installer choose keeps the tool current + # with Dependabot's reviewable bumps. If that default ever has to be + # pinned, the floor is v3 — the v2 flags this repo used before are ignored + # under the new bundle format (see the note in .goreleaser.yaml). - name: Install cosign uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 474b085..40236fd 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -145,29 +145,38 @@ sboms: # Keyless cosign signature over checksums.txt. checksums.txt alone is not an # integrity story — whoever can swap an artifact in the release can swap the -# checksums with it — but a Sigstore-backed detached signature over it is: the -# signature roots the trust and the checksums chain to every artifact. +# checksums with it — but a Sigstore-backed signature over it is: the signature +# roots the trust and the checksums chain to every artifact. # -# Verify (needs the cert, the sig and the file; no key distribution): -# cosign verify-blob checksums.txt \ -# --certificate checksums.txt.pem --signature checksums.txt.sig \ +# Verify (the bundle carries signature, certificate and transparency-log +# inclusion proof, so it is the only extra file needed; no key distribution): +# cosign verify-blob checksums.txt --bundle checksums.txt.bundle \ # --certificate-identity-regexp 'https://github.com/ftarasenko/go-openstackclient/.*' \ # --certificate-oidc-issuer https://token.actions.githubusercontent.com # sha256sum -c checksums.txt # +# --new-bundle-format is cosign v3's default, but it is named explicitly here on +# purpose: the previous config passed --output-signature/--output-certificate, +# and when that default flipped from false to true in v3 those flags became +# silently IGNORED ("deprecated when using --new-bundle-format and will be +# ignored") rather than an error. The release then produced no .sig/.pem at all, +# and because release.yml only runs on a v* tag, nothing would have caught it +# before the tag was already burned. Stating the format keeps this block correct +# under either default, and it is what makes an air-gapped `--trusted-root` +# verification possible (that flag requires the new format) — see SECURITY.md. +# # Needs `id-token: write` in the release workflow (the OIDC token is what # Fulcio signs) plus cosign on PATH — both wired in release.yml. signs: - id: cosign-checksums cmd: cosign artifacts: checksum - signature: "${artifact}.sig" - certificate: "${artifact}.pem" + signature: "${artifact}.bundle" output: true args: - sign-blob - - "--output-signature=${signature}" - - "--output-certificate=${certificate}" + - "--new-bundle-format" + - "--bundle=${signature}" - "${artifact}" - "--yes" diff --git a/AGENTS.md b/AGENTS.md index 0f509ed..22f707c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -373,12 +373,14 @@ weakened by a check that happens to need a proxy. - `.github/workflows/prune-release-assets.yml` — dispatch (`dry_run` defaults to true) to reclaim storage by deleting `.tar.gz`/`.zip`/`.rpm`/`.deb` from releases outside a keep window, while **retaining** `checksums.txt`, its - signature/certificate and the SBOM bundle — note the bundle ends in `.tar.gz`, - so its always-keep guard, not the suffix filter, is what protects it. Because - builds are byte-reproducible from + cosign bundle (plus the older detached signature/certificate) and the SBOM + bundle — note the SBOM bundle ends in `.tar.gz`, so its always-keep guard, not + the suffix filter, is what protects it. Because builds are byte-reproducible from the tag, a pruned release stays verifiable and re-derivable; deleting the checksums would end that, which is why the retain list is redundant with the delete list on purpose. Pruned versions stop installing via the Homebrew cask. + Its keep-list is keyed on asset **suffix**, so changing what `signs:` writes + means editing that jq filter in the same commit. `checksums.txt` on its own is not an integrity story: whoever can swap an artifact in a release can swap the checksums with it. The cosign signature is what roots @@ -386,6 +388,21 @@ the trust, and the checksums then chain to every artifact. The verification command (no key distribution needed) is in `.goreleaser.yaml` next to the `signs:` block and in README "Prebuilt binaries". +**The signature is a Sigstore bundle (`checksums.txt.bundle`), not a detached +`.sig`/`.pem` pair.** cosign v3 made the standardized bundle the default, and the +migration was not optional: under that default the `--output-signature` / +`--output-certificate` flags the old config passed are *silently ignored* rather +than rejected, so the release would have published no signature at all — and +since `release.yml` only runs on a `v*` tag, no PR check could have caught it. +The bundle also carries the transparency-log inclusion proof, which is what makes +the air-gapped `--trusted-root` verification in SECURITY.md possible; cosign v3 +deprecated `--offline` in favour of exactly that. Three consequences to keep in +mind when touching this: verifying needs cosign v3+ (or v2.6+ with an explicit +`--new-bundle-format`); releases up to v0.22.0 still carry the old pair and a v3 +client falls back to it automatically; and `.goreleaser.yaml` names +`--new-bundle-format` explicitly so a future default flip cannot repeat the +failure silently. + **GoReleaser has no `before:` hooks, on purpose.** Hooks run inside the same workflow step as GoReleaser, and that step carries `HOMEBREW_TAP_TOKEN` — a PAT with write access to a second repository. A `go run ./cmd/koc completion …` hook diff --git a/README.md b/README.md index ea4ec8f..dd9752f 100644 --- a/README.md +++ b/README.md @@ -77,14 +77,13 @@ darwin/amd64, darwin/arm64, windows/amd64, windows/arm64** with a Alongside them: **`.rpm` and `.deb` packages** for the two linux architectures (for a local yum/apt mirror on an air-gapped node — these do install the shell completions), an **SPDX SBOM bundle** (`koc__sboms.tar.gz`, one -`*.spdx.json` per artifact inside), a keyless **cosign -signature** over `checksums.txt` (`checksums.txt.sig` + `.pem`), and a GitHub +`*.spdx.json` per artifact inside), a keyless **cosign signature** over +`checksums.txt` (`checksums.txt.bundle`), and a GitHub **build-provenance attestation**. Builds are byte-reproducible, so the checksums can be re-derived independently from the tag. ```sh -cosign verify-blob checksums.txt \ - --certificate checksums.txt.pem --signature checksums.txt.sig \ +cosign verify-blob checksums.txt --bundle checksums.txt.bundle \ --certificate-identity-regexp 'https://github.com/ftarasenko/go-openstackclient/.*' \ --certificate-oidc-issuer https://token.actions.githubusercontent.com sha256sum -c checksums.txt @@ -98,12 +97,23 @@ The two identity flags are the part that matters — without them you have check only that *somebody* signed the file. Verify the signature first, then let `sha256sum -c` carry that trust to each artifact. +`checksums.txt.bundle` is a [Sigstore bundle][sigstore-bundle]: it carries the +signature, the signing certificate and the transparency-log inclusion proof in +one file, so nothing else has to be downloaded alongside it. Reading it needs +**cosign v3 or newer** (where the format is the default), or cosign v2.6+ with an +explicit `--new-bundle-format`. Releases up to and including **v0.22.0** instead +carry a detached `checksums.txt.sig` + `.pem` pair; a v3 client still verifies +those — pass `--certificate`/`--signature` in place of `--bundle` and it falls +back to the legacy path automatically. + Both commands reach the network by default (`cosign` consults the public Rekor -log, `gh` calls the GitHub API), so on an isolated network capture a -`--bundle` on a connected host and verify from it with `cosign verify-blob ---bundle … --offline`. `sha256sum -c checksums.txt` is fully offline and is what -you gate the install on. See [SECURITY.md](SECURITY.md) for the air-gapped -procedure, the supported-version policy, and how to report a vulnerability. +log, `gh` calls the GitHub API). On an isolated network, verify the bundle +against a `--trusted-root` file captured on a connected host — no Rekor call, no +`--offline` flag. `sha256sum -c checksums.txt` is fully offline and is what you +gate the install on. See [SECURITY.md](SECURITY.md) for that procedure, the +supported-version policy, and how to report a vulnerability. + +[sigstore-bundle]: https://blog.sigstore.dev/cosign-3-0-available/ ## Build diff --git a/SECURITY.md b/SECURITY.md index b0dbd7b..f0090dd 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -75,8 +75,7 @@ checksums chain from it to each artifact. Verify the signature, then the artifact: ```sh -cosign verify-blob checksums.txt \ - --certificate checksums.txt.pem --signature checksums.txt.sig \ +cosign verify-blob checksums.txt --bundle checksums.txt.bundle \ --certificate-identity-regexp 'https://github.com/ftarasenko/go-openstackclient/.*' \ --certificate-oidc-issuer https://token.actions.githubusercontent.com @@ -88,6 +87,16 @@ itself, asserted by the certificate. The `--certificate-identity-regexp` and `--certificate-oidc-issuer` flags are the check that matters — without them you have verified only that *somebody* signed the file. +`checksums.txt.bundle` is a Sigstore bundle carrying the signature, the signing +certificate and the transparency-log inclusion proof together, so it is the only +file to fetch beside `checksums.txt` itself. Reading it needs **cosign v3+**, +where that format is the default, or cosign v2.6+ with an explicit +`--new-bundle-format`. Releases up to and including **v0.22.0** shipped a +detached `checksums.txt.sig` + `checksums.txt.pem` pair instead; substitute +`--certificate checksums.txt.pem --signature checksums.txt.sig` for `--bundle` +and a v3 client verifies those too — it detects the legacy layout and falls back +on its own. + Provenance — what built the artifact, from which commit and workflow: ```sh @@ -112,25 +121,37 @@ re-derive from a rebuild. ### Verifying without internet access The commands above are the reason the air-gap matters here: by default -`cosign verify-blob` consults the public Rekor transparency log and +`cosign verify-blob` fetches Sigstore's trust roots over TUF and `gh attestation verify` calls the GitHub API, so both fail on an isolated network. -For an air-gapped check, do the verification once on a connected host and carry -the result across, or verify offline with the transparency-log bundle: +The bundle already contains the signature, the certificate and the +transparency-log inclusion proof, so the only thing still missing inside the +enclave is the trust anchor those are checked against. Capture it once on a +connected host as a **trusted root** file, carry it across with the release +artifacts, and the verification runs with no network at all: ```sh -# On a connected host, capture the bundle alongside the artifacts: -cosign verify-blob checksums.txt \ - --certificate checksums.txt.pem --signature checksums.txt.sig \ - --certificate-identity-regexp 'https://github.com/ftarasenko/go-openstackclient/.*' \ - --certificate-oidc-issuer https://token.actions.githubusercontent.com \ - --bundle checksums.txt.bundle +# On a connected host, once per trust-root rotation (not per release): +cosign trusted-root create --with-default-services --out trusted_root.json -# Inside the enclave, verify from the bundle without contacting Rekor: -cosign verify-blob checksums.txt --bundle checksums.txt.bundle --offline +# Inside the enclave — no Rekor call, no TUF fetch: +cosign verify-blob checksums.txt --bundle checksums.txt.bundle \ + --trusted-root trusted_root.json \ + --certificate-identity-regexp 'https://github.com/ftarasenko/go-openstackclient/.*' \ + --certificate-oidc-issuer https://token.actions.githubusercontent.com ``` +Keep the identity flags on the enclave-side command. They are what bind the +signature to *this* repository's release workflow; a `--trusted-root` on its own +only proves the certificate came from Sigstore, which any GitHub workflow can +obtain. + +`--trusted-root` requires the new bundle format, which is why it works from +v0.23.0 onward. For an older release, the equivalent is to run the full +verification on the connected host and carry the *result* across, since the +detached `.sig`/`.pem` pair has no inclusion proof to check offline against. + `gh attestation verify` accepts `--bundle` for the same purpose; download the attestation bundle on the connected side and pass it in. From 43a0075127468eba32c80858f54383bcb53b1a77 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 08:52:14 +0000 Subject: [PATCH 2/2] docs(ci): correct comments that no longer describe the code beneath them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three comments drifted out of sync with their own code in the last two commits on master, each in a way that misleads rather than merely omits. The github-actions group bump moved attest-build-provenance to v4.2.2 and dependency-review-action to v5.0.0, but left the notes above them saying they were "held" on v2 and v4 pending a reviewable Dependabot PR. That PR was the bump itself, so both now describe the opposite of the pin they annotate — and a rationale that contradicts its own `uses:` line is worse than none, because the next reader cannot tell which half is stale. Each now records what the bump was checked against: v4 reduces attest-build-provenance to a wrapper over actions/attest with `subject-path` unchanged, and v5 of dependency-review-action changed only the runtime to node24, which ubuntu-latest already satisfies. The SBOM-bundle commit spliced its NOTE into the middle of an existing sentence in prune-release-assets, leaving "The always-keep list is redundant with the" to run straight into "NOTE the SBOM bundle is …", and stranding a closing paren with no opener. The block is rewritten to say the two things it was reaching for: the guard is redundant for files the suffix test never matches anyway, and the SBOM bundle is the lone exception because it genuinely ends in .tar.gz. It also now states why two spellings appear for both the SBOM and the signature — this workflow runs across releases from either era. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01DRBThE7bUn5AR4KsNi78yw --- .github/workflows/prune-release-assets.yml | 29 ++++++++++++++-------- .github/workflows/release.yml | 8 +++--- .github/workflows/supply-chain.yml | 6 ++--- 3 files changed, 26 insertions(+), 17 deletions(-) diff --git a/.github/workflows/prune-release-assets.yml b/.github/workflows/prune-release-assets.yml index 48413f5..8ca4e97 100644 --- a/.github/workflows/prune-release-assets.yml +++ b/.github/workflows/prune-release-assets.yml @@ -157,17 +157,24 @@ jobs: echo "protected tags: $(jq -r 'join(", ")' "$TMP/protected.json")" echo - # Heavy build outputs only. The always-keep list is redundant with the - # NOTE the SBOM bundle is koc__sboms.tar.gz — it DOES end in - # .tar.gz, so unlike the other retained files it is not naturally - # excluded by the suffix test and the always_keep guard is what saves - # it. Deleting it would leave a release whose checksums.txt still lists - # an SBOM nobody can fetch. Releases cut before the bundle landed have - # loose per-artifact documents instead, hence both patterns. - # (A per-artifact SBOM is named koc_X_os_arch.tar.gz.spdx.json, - # which does NOT end in .tar.gz) and that redundancy is deliberate: - # if someone ever loosens the delete-list, the audit-trail files still - # survive. + # Heavy build outputs only, with the always_keep list below as a second + # guard. + # + # For most retained files that guard is redundant with the suffix test: + # a per-artifact SBOM is named koc_X_os_arch.tar.gz.spdx.json, which + # does NOT end in .tar.gz, so `heavy` never matches it in the first + # place. The redundancy is deliberate — if someone later loosens the + # delete-list, the audit-trail files still survive. + # + # The SBOM bundle is the one exception: koc__sboms.tar.gz DOES end + # in .tar.gz, so always_keep is the only thing standing between it and + # deletion. Losing it would leave a release whose checksums.txt lists + # an SBOM nobody can fetch. + # + # Two spellings appear for both the SBOM and the signature because this + # workflow runs across every release: the bundle forms (_sboms.tar.gz, + # checksums.txt.bundle) from v0.23.0, and the loose forms (.spdx.json, + # checksums.txt.sig/.pem) on everything before it. jq -r --slurpfile protected "$TMP/protected.json" ' def heavy: endswith(".tar.gz") or endswith(".zip") diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0855283..26ec332 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -126,9 +126,11 @@ jobs: # proves what produced them. Verify with: # gh attestation verify koc__linux_amd64.tar.gz \ # --repo ftarasenko/go-openstackclient - # Pinned to a full commit SHA per repo convention. Held at v2 because the - # inputs below were written against it; v4 is current, and Dependabot will - # propose that bump as a reviewable PR. + # Pinned to a full commit SHA per repo convention. On v4, which upstream + # has reduced to a thin wrapper over actions/attest — `subject-path` still + # behaves as it did on v2, which is the only input used here. New work + # should reach for actions/attest directly; this stays until there is a + # reason to move it. - name: Attest build provenance uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 with: diff --git a/.github/workflows/supply-chain.yml b/.github/workflows/supply-chain.yml index 6bd4895..5520eb9 100644 --- a/.github/workflows/supply-chain.yml +++ b/.github/workflows/supply-chain.yml @@ -103,9 +103,9 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - # Pinned to a full commit SHA per repo convention. Held on v4 because the - # inputs below were written against it; v5 is current, and Dependabot will - # propose that bump as a reviewable PR. + # Pinned to a full commit SHA per repo convention. On v5, whose only + # breaking change was the node24 runtime (needs runner >= v2.327.1, which + # ubuntu-latest satisfies); both inputs below are unchanged from v4. - name: Dependency review uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0 with: