Skip to content

feat: submodule SBOM support — discovery annotation + attach-or-backfill - #296

Open
vpetersson wants to merge 4 commits into
masterfrom
feat/submodule-attach-or-backfill
Open

feat: submodule SBOM support — discovery annotation + attach-or-backfill#296
vpetersson wants to merge 4 commits into
masterfrom
feat/submodule-attach-or-backfill

Conversation

@vpetersson

@vpetersson vpetersson commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

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

  • Discovery detects lockfiles living in another repository: paths declared in .gitmodules (submodules) or directories with their own .git (vendored clones). DiscoveredLockfile gains nested_repo / nested_repo_kind.
  • The discover screen annotates those rows (e.g. 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.
  • Never attested: submodule/vendored matrix rows get attest: false with an explicit "Deliberately NOT signed" annotation in the emitted YAML, and the attest step is gated with if: ${{ 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_PATH mode)

When a matrix row carries submodule_path, the action:

  1. Resolves the pin to a version — gitlink SHA from the parent tree (git ls-tree, no submodule init needed), then an exact version-shaped tag on that commit via git ls-remote --tags against the .gitmodules URL (local-tag fallback, vendored-clone support). No tag → 7-char short SHA, matching trunk-strategy COMPONENT_VERSIONs. Exact match only — no git describe nearest-tag guessing.
  2. Attach: if the component already has an SBOM at that (version, format) — via 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 the PRODUCT_RELEASE(s).
  3. Backfill: otherwise the normal pipeline runs with COMPONENT_VERSION overridden to the pin-derived version, so the upload lands where the next release's lookup finds it. DUPLICATE_ARTIFACT races (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_sbom
  • submodule.py (new): pin + version resolution; remote calls run with the parent repo as cwd so actions/checkout credentials apply to private submodules
  • cli/main.py: submodule preflight before Step 1; Step 6 + finalization extracted into shared helpers; duplicate-upload recovery; SUBMODULE_PATH env/CLI option + validation (requires LOCK_FILE + sbomify destination; empty string = disabled)
  • Wizard: discovery detection, discover-screen UX, emitter plumbing (submodule_path rows, SUBMODULE_PATH env, submodules: recursive checkout, attest gating). Non-submodule workflows emit byte-identical to before
  • README: SUBMODULE_PATH documented; action.yml unchanged (Docker action, step env: flows through)

Verification

  • Full suite: 2469 passed; ruff lint + format clean. New tests: real-git submodule fixtures (tag pin, SHA pin, uninitialized clone, vendored clone), API lookup incl. legacy-backend re-check, config validation, emitter shape, Textual discover-screen behavior.
  • E2E against stage (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

vpetersson and others added 4 commits July 20, 2026 15:16
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 vpetersson changed the title feat: attach-or-backfill submodule SBOMs at release time feat: submodule SBOM support — discovery annotation + attach-or-backfill Jul 21, 2026
@vpetersson
vpetersson changed the base branch from feat/wizard-nested-repo-annotation to master July 21, 2026 13:29
@vpetersson
vpetersson requested a review from Copilot July 21, 2026 13:30

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_PATH plus 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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants