From b318ec56df8a480ee1bf2aa9f668c1b5f3e64c21 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 4 Aug 2026 17:59:08 +0900 Subject: [PATCH 1/2] fix(ci): snapshot dependencies on default-branch pushes dependency-review compares the base and head dependency graphs. SBOM Generation only ran on pull_request and release, so main never received a dependency snapshot and every PR's comparison reported: The number of snapshots compared for the base SHA (0) and the head SHA (1) do not match With no base snapshot the action treats the entire dependency set as newly added, so it re-flags every pre-existing vulnerability on every PR instead of only the ones the PR actually introduces. That is why an untouched lock file can block an unrelated PR. Adds a push trigger on the default branches so those commits get a real snapshot. The existing job conditions already handle it: cancel-closed-pr-runs is pull_request-gated and generate-sbom runs for any non-pull_request event. anchore/sbom-action only uploads release assets on release events, so upload-release-assets is a no-op here. Also switches the concurrency group's final fallback from github.ref to github.sha. That fallback was previously unreachable (pull_request and release both matched earlier terms); reached by push it would make two commits landing close together cancel each other, and a cancelled push run leaves that commit without a snapshot -- reopening the same base-side gap. Co-Authored-By: Claude --- .github/workflows/sbom-generation.yml | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/.github/workflows/sbom-generation.yml b/.github/workflows/sbom-generation.yml index b62f0b3d3..b56a261be 100644 --- a/.github/workflows/sbom-generation.yml +++ b/.github/workflows/sbom-generation.yml @@ -19,9 +19,19 @@ # NOTE: contents: write is required for release-asset upload and for the # dependency submission API. Fork PR heads run without write and simply skip # those side effects; the artifact is still produced. +# +# NOTE on the push trigger: it exists so the DEFAULT BRANCH has a dependency +# snapshot. dependency-review compares base...head in the dependency graph; with +# PR-only runs the base commit never has one, so every comparison reports "the +# number of snapshots compared for the base SHA (0) and the head SHA (1) do not +# match" and the whole dependency set reads as newly added. That re-flags +# pre-existing vulnerabilities on every PR instead of only the ones the PR adds. +# Snapshotting pushes to the default branch gives the comparison a real base. name: SBOM Generation on: + push: + branches: [main, master, develop] pull_request: types: [opened, synchronize, reopened, ready_for_review, closed] branches: [main, master, develop] @@ -29,7 +39,10 @@ on: types: [published] concurrency: - group: sbom-generation-${{ github.event.pull_request.base.repo.full_name || github.repository }}-${{ github.event.pull_request.number || github.event.release.tag_name || github.ref }} + # Final fallback is the SHA, not the ref, so two pushes landing close together + # do not cancel each other: a cancelled push run leaves that commit without a + # snapshot, which is exactly the base-side gap this trigger exists to close. + group: sbom-generation-${{ github.event.pull_request.base.repo.full_name || github.repository }}-${{ github.event.pull_request.number || github.event.release.tag_name || github.sha }} cancel-in-progress: true permissions: From 1b5dbebbc93fd4b9aafedafb22ee1679ed4ee7de Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 4 Aug 2026 18:09:06 +0900 Subject: [PATCH 2/2] test(ci): pin default-branch SBOM snapshot contract --- tests/test_sbom_generation_push_contract.py | 54 +++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 tests/test_sbom_generation_push_contract.py diff --git a/tests/test_sbom_generation_push_contract.py b/tests/test_sbom_generation_push_contract.py new file mode 100644 index 000000000..011be9737 --- /dev/null +++ b/tests/test_sbom_generation_push_contract.py @@ -0,0 +1,54 @@ +"""Contracts for default-branch dependency snapshot generation.""" + +from __future__ import annotations + +from pathlib import Path + + +WORKFLOW = Path(".github/workflows/sbom-generation.yml") + + +def _workflow_text() -> str: + """Return the centrally versioned SBOM workflow as UTF-8 text.""" + + return WORKFLOW.read_text(encoding="utf-8") + + +def test_sbom_workflow_snapshots_supported_default_branch_pushes() -> None: + """Default-branch commits must receive dependency graph snapshots.""" + + workflow = _workflow_text() + + assert "on:\n push:\n branches: [main, master, develop]\n" in workflow + assert " pull_request:\n" in workflow + assert " release:\n" in workflow + assert "dependency-snapshot: true" in workflow + + +def test_sbom_push_concurrency_is_bound_to_the_commit_sha() -> None: + """A later default-branch push must not cancel another commit's snapshot.""" + + workflow = _workflow_text() + group_line = next( + line.strip() for line in workflow.splitlines() if line.strip().startswith("group:") + ) + + assert "github.event.release.tag_name || github.sha" in group_line + assert "github.event.release.tag_name || github.ref" not in group_line + + +def test_sbom_job_conditions_keep_push_runs_active_and_closed_prs_inert() -> None: + """Pushes run the snapshot job while closed-PR events only cancel stale work.""" + + workflow = _workflow_text() + + assert ( + "if: github.event_name == 'pull_request' && github.event.action == 'closed'" + in workflow + ) + assert ( + "if: github.event_name != 'pull_request' || github.event.action != 'closed'" + in workflow + ) + assert "generate-sbom:\n" in workflow + assert " contents: write\n" in workflow