feat: submodule SBOM support — discovery annotation + attach-or-backfill - #296
Open
vpetersson wants to merge 4 commits into
Open
feat: submodule SBOM support — discovery annotation + attach-or-backfill#296vpetersson wants to merge 4 commits into
vpetersson wants to merge 4 commits into
Conversation
Lockfiles that live inside a git submodule (declared in .gitmodules) or a vendored clone (a directory with its own .git) belong to another repository and are better tracked from there. The discover screen now annotates those rows with the enclosing repo path and leaves them deselected by default, with a note explaining why. Detection parses .gitmodules directly (works without a git binary and for declared-but-uninitialized submodules) and checks ancestors for a .git entry — a file for submodule checkouts, a directory for vendored clones. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
An attest-build-provenance signature carries the Sigstore identity of the workflow that ran it. For lockfiles inside a submodule or vendored checkout, that identity is the parent repo — but the SBOM describes code owned by another repository, so the attestation would fail verification against the repo the code actually comes from. When attestation is enabled, every matrix row now carries an 'attest' boolean and the attest step is gated on it; nested-repo rows get attest: false. This is also the per-entry hook the upcoming attach-or-backfill submodule flow will need. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Each attest: false row now carries an explicit 'Deliberately NOT signed' comment naming the nested repo and why, so the generated YAML documents the decision where it takes effect, not only at the attest step. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New SUBMODULE_PATH mode for the action: the component is a git submodule pinned in the parent tree. At run time the action resolves the pin to a version — an exact version tag on the pinned commit (resolved via git ls-remote, no submodule checkout needed), else the 7-char short SHA, matching what trunk-strategy workflows publish. Exact match only; no nearest-tag guessing. If the submodule's component already has an SBOM at that (version, format) — looked up via the new GET filters from sbomify#1176 — the action skips generation and upload entirely and just tags the existing SBOM into the configured product release(s). Otherwise it generates from the submodule's lockfile and uploads under the pin-derived version (backfill), so the next release with the same pin takes the attach path. A DUPLICATE_ARTIFACT on upload (two parents racing to backfill the same pin) recovers by re-looking up the winner and attaching it. - sbomify_api: list_component_sboms / find_component_sbom, with client-side re-checking of the filters for backends that predate the server-side ones - submodule.py: pin + version resolution (gitlink from the parent tree, .gitmodules URL via git config, ls-remote tag matching with local-tag fallback, vendored-clone support) - pipeline: preflight before Step 1, Step 6 + finalization extracted into reusable helpers, duplicate-upload recovery in submodule mode - wizard emitter: submodule rows carry submodule_path, the env block forwards SUBMODULE_PATH, and checkout pulls submodules for the backfill path - README: document SUBMODULE_PATH Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
vpetersson
changed the base branch from
feat/wizard-nested-repo-annotation
to
master
July 21, 2026 13:29
Contributor
There was a problem hiding this comment.
Pull request overview
Adds end-to-end support for lockfiles that live in git submodules or vendored repositories, spanning wizard discovery/UX and runtime “attach-or-backfill” behavior so the action can reuse an already-published SBOM for the submodule’s pinned version or generate/upload one when missing.
Changes:
- Wizard: detect nested repos (submodule/vendored), annotate those lockfiles, and default-deselect them; emit matrix/env plumbing for
SUBMODULE_PATHplus attestation gating/opt-out. - Runtime: introduce submodule pin → version resolution (tag-at-pin else short SHA), preflight SBOM lookup via API, attach existing SBOM when present, otherwise backfill; recover from duplicate-artifact races.
- API client: add component-SBOM listing + exact-match helper used by the attach path.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| tests/test_wizard_textual.py | Asserts discover screen default-deselect + annotation/note for nested-repo lockfiles. |
| tests/test_wizard_emitter.py | Validates YAML emission for per-row attest, submodule plumbing, and checkout submodules settings. |
| tests/test_wizard_discovery.py | Covers .gitmodules parsing and vendored .git detection for nested-repo annotation. |
| tests/test_submodule.py | Exercises git pin resolution and submodule-mode pipeline behaviors. |
| tests/test_sbomify_api.py | Tests list_component_sboms and find_component_sbom behavior including legacy-backend recheck. |
| tests/test_config.py | Adds config validation expectations for SUBMODULE_PATH including empty-string disable behavior. |
| sbomify_action/submodule.py | New implementation for resolving pinned submodule state to a version string via tags/short SHA. |
| sbomify_action/sbomify_api.py | Adds component SBOM listing and exact-match finder. |
| sbomify_action/cli/wizard/state.py | Extends DiscoveredLockfile with nested_repo and nested_repo_kind. |
| sbomify_action/cli/wizard/screens/discover.py | Annotates nested-repo rows and defaults them to deselected; shows an explanatory note. |
| sbomify_action/cli/wizard/discovery.py | Implements .gitmodules + embedded .git detection and annotates discovered lockfiles. |
| sbomify_action/cli/wizard/ci_emitter.py | Emits submodule_path, SUBMODULE_PATH, recursive submodule checkout, and attest gating/annotations. |
| sbomify_action/cli/main.py | Adds SUBMODULE_PATH config/CLI option, preflight attach-or-backfill flow, and duplicate-upload recovery. |
| README.md | Documents SUBMODULE_PATH requirements and behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| applies to the remote call. Annotated tags are resolved through | ||
| their peeled (``^{}``) entries, which override the tag-object SHA. | ||
| """ | ||
| out = _run_git(["ls-remote", "--tags", url], cwd=repo_root, timeout=_REMOTE_TIMEOUT) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
End-to-end git-submodule support, wizard through runtime. (Supersedes #295 — the two halves depend on each other, so they ship as one PR. Backend lookup filters landed in sbomify/sbomify#1176 and are verified on stage.)
Wizard / discovery side
.gitmodules(submodules) or directories with their own.git(vendored clones).DiscoveredLockfilegainsnested_repo/nested_repo_kind.extern/lib/Cargo.lock (rust) (submodule: extern/lib)) and leaves them deselected by default, with a note explaining why — they belong to another repo and are better tracked from there.attest: falsewith an explicit "Deliberately NOT signed" annotation in the emitted YAML, and the attest step is gated withif: ${{ matrix.attest }}. An attestation from the parent repo's workflow would carry the wrong Sigstore identity for code owned by another repository.Runtime side (
SUBMODULE_PATHmode)When a matrix row carries
submodule_path, the action:git ls-tree, no submodule init needed), then an exact version-shaped tag on that commit viagit ls-remote --tagsagainst the.gitmodulesURL (local-tag fallback, vendored-clone support). No tag → 7-char short SHA, matching trunk-strategyCOMPONENT_VERSIONs. Exact match only — nogit describenearest-tag guessing.GET /components/{id}/sboms?version=&format=, re-checked client-side for older backends — generation and upload are skipped and the existing SBOM is tagged into thePRODUCT_RELEASE(s).COMPONENT_VERSIONoverridden to the pin-derived version, so the upload lands where the next release's lookup finds it.DUPLICATE_ARTIFACTraces (two parents backfilling the same pin) recover by re-looking up the winner.Per-format independence falls out of the matrix design: attach CycloneDX and backfill SPDX in the same release with no special casing.
Changes
sbomify_api.py:list_component_sboms/find_component_sbomsubmodule.py(new): pin + version resolution; remote calls run with the parent repo as cwd so actions/checkout credentials apply to private submodulescli/main.py: submodule preflight before Step 1; Step 6 + finalization extracted into shared helpers; duplicate-upload recovery;SUBMODULE_PATHenv/CLI option + validation (requiresLOCK_FILE+ sbomify destination; empty string = disabled)submodule_pathrows,SUBMODULE_PATHenv,submodules: recursivecheckout, attest gating). Non-submodule workflows emit byte-identical to beforeSUBMODULE_PATHdocumented;action.ymlunchanged (Docker action, stepenv:flows through)Verification
stage.sbomify.com, with #1176 deployed): tag-pin backfill → attach; SHA-pin backfill → attach (idempotent re-tag); per-format miss (CDX present, SPDX absent) → SPDX backfill via syft in the Docker image built from this branch; raw server-side filter checks; releases with both formats coexisting.🤖 Generated with Claude Code