Skip to content

feat(bom): render divergent recipe version pins as BOM variants#1634

Merged
yuanchen8911 merged 8 commits into
NVIDIA:mainfrom
yuanchen8911:feat/bom-variant-rows
Jul 10, 2026
Merged

feat(bom): render divergent recipe version pins as BOM variants#1634
yuanchen8911 merged 8 commits into
NVIDIA:mainfrom
yuanchen8911:feat/bom-variant-rows

Conversation

@yuanchen8911

Copy link
Copy Markdown
Contributor

Summary

The container BOM (docs/user/container-images.md and the CycloneDX output of tools/bom) now lists version variants: every Helm chart version explicitly pinned by a base/overlay/mixin that differs from the component's registry default, with the images that version actually ships.

Motivation / Context

A recipe source that pins a component to a non-default chart version deploys images the BOM never mentioned — the doc only rendered each component at its registry defaultVersion. Issue #1611 (revised) scopes the fix to declaration-level discovery: derive variants directly from the explicit componentRefs[].version pins versus the registry defaults, with no shared exemption file and no effective-recipe resolution. Today that surfaces one variant: kube-prometheus-stack@83.7.0, pinned by aks.

Fixes: #1611
Related: #1615, #1616

Non-goals (documented in tools/bom/variants.go, per the revised #1611):

  • Effective-recipe resolution (inheritance chains, mixin composition, criteria matching) — that is the recipe resolver's job.
  • Source/chart coordinate divergence analysis — variants render at registry coordinates by definition (catalog parity).
  • ComponentRef type analysis beyond "is this declared pin a Helm pin" — explicitly non-Helm-typed refs are skipped.

Type of Change

  • New feature (non-breaking change that adds functionality)
  • Documentation update
  • Build/CI/tooling

Component(s) Affected

  • Docs/examples (docs/, examples/)
  • Other: pkg/bom, tools/bom, .github/workflows/merge-gate.yaml

Implementation Notes

Discovery (tools/bom/variants.go):

  • loadRecipeSources reads every overlay/mixin YAML under the same -repo-root recursively (nested overlay subdirectories are documented and supported), parsing into the canonical pkg/recipe types with the metadata store's exact semantics: mixins decode strictly (KnownFields(true), hard RecipeMixin kind check — a typo'd field fails closed just as it does at recipe load); overlays decode leniently exactly as the store does (empty kind is legacy RecipeMetadata, any other kind is skipped and never mined for pins — a field the resolver would ignore is equally invisible here, so the projection matches what deploys).
  • Fail-closed on ambiguity: malformed YAML, a missing metadata.name, duplicate source names, and whitespace-padded version pins are errors, never silent skips — a silently dropped source could hide a divergent pin.
  • Reads are confined to the recipes root via os.Root (an escaping symlink is rejected, not followed) and size-bounded; sources are opened O_NONBLOCK and validated regular via fstat on the opened handle, so a non-regular target (e.g. a symlinked FIFO, whose open would otherwise block until a writer appears) fails closed with no stat-to-open race window; the walk checks ctx per entry.
  • deriveVariants compares each explicit Helm pin against the registry defaultVersion and aggregates divergences by (component, version) with the sorted declaring source names. An explicit pin over an empty registry default still renders as a variant (external -repo-root registries may omit defaultVersion). The version-pin guard's versionPinExemptions plays no role: it is validation policy (whether a divergence is allowed), not deployment fact.
  • Variants render through the same surveyComponent path as default entries, at the variant version — so variant image sets are rendered, never copied, and mixed Helm+manifest components contribute their manifest images too.

Rendering (pkg/bom):

  • Additive entry points BuildBOMWithVariants / WriteMarkdownWithVariants; the legacy BuildBOM / WriteMarkdown surfaces are byte-identical to main (pinned by TestLegacyEntryPointsUnchanged), so external importers see no behavioral change. pkg/evidence/attestation is untouched.
  • Variant CycloneDX entries get version-qualified bom-refs (<meta.Name>/<comp>@<ver>) with aicr:variant:of and sorted aicr:variant:sources properties; variant images join the dependency graph. The checked builder fails closed on any duplicate bom-ref instead of emitting an ambiguous graph.
  • Markdown gains a ## Version variants table (distinct headers, so the Components table and its existing freshness gate are unaffected) and per-variant image sections; the section is omitted entirely when no variants exist. A caller-supplied rendering-fidelity note (Metadata.RenderFidelity) documents the catalog-parity limitation: charts are rendered with the shared recipes/components/<name>/values.yaml, per-recipe overlay overrides are not applied.

Freshness gate:

  • TestCommittedBOMVariantsMatchRecipePins gates the committed variants table bidirectionally: a new divergent pin without a variant row fails, and a stale row without a backing pin fails. The table parser is strict (heading-anchored, delimiter-validated, malformed rows are errors) so a mangled doc cannot pass vacuously.
  • The bom-freshness merge-gate job runs both freshness tests and positively asserts a PASS line for each, so a rename or skip cannot silently drop either gate.

Testing

make qualify           # full gate, passing at the pushed SHA
go test -race ./pkg/bom/... ./tools/bom/...
make bom-docs          # regenerated docs/user/container-images.md (committed)
  • Coverage vs origin/main: pkg/bom 95.5% → 95.8% (+0.3%), tools/bom 79.5% → 81.8% (+2.3%).
  • Legacy compatibility is pinned by test: WriteMarkdownWriteMarkdownWithVariants(nil) byte-for-byte; legacy BuildBOM keeps its historical duplicate tolerance. All pre-existing pkg/bom and tools/bom tests pass unchanged.

Risk Assessment

  • Low — Isolated change, well-tested, easy to revert

Rollout notes: Additive only. The legacy pkg/bom Go API and its output are byte-identical; tools/bom output changes are confined to the new variants table, the fidelity note, and variant entries in the CycloneDX document. Reverting restores the previous BOM shape with no migration.

Checklist

  • Tests pass locally (make test with -race)
  • Linter passes (make lint)
  • I did not skip/disable tests to make CI green
  • I added/updated tests for new functionality
  • I updated docs if user-facing behavior changed
  • Changes follow existing patterns in the codebase
  • Commits are cryptographically signed (git commit -S) — GPG signing info

@yuanchen8911 yuanchen8911 added the theme/supply-chain SLSA, SBOM, Sigstore, and provenance verification label Jul 7, 2026
@yuanchen8911 yuanchen8911 changed the title feat(bom): render divergent recipe version pins as BOM variants WIP: feat(bom): render divergent recipe version pins as BOM variants Jul 7, 2026
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR adds recipe-derived BOM variants for divergent Helm pins, rendering variant images through CycloneDX and Markdown outputs. It adds checked BOM reference validation, render-fidelity metadata, strict recipe-source loading, bidirectional freshness tests, CI execution checks, variant-focused test coverage, and regenerated container image documentation.

Estimated code review effort: 4 (Complex) | ~75 minutes

Suggested labels: area/recipes, area/tests, theme/recipes, theme/validation

Suggested reviewers: mchmarny, lockwobr

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: rendering divergent recipe version pins as BOM variants.
Description check ✅ Passed The description is directly related to the variant-rendering BOM changes and supporting docs, tests, and CI updates.
Linked Issues check ✅ Passed The PR matches #1611 by deriving recipe-based variants, preserving default refs, adding variant tables, and gating freshness bidirectionally.
Out of Scope Changes check ✅ Passed The added docs, tests, and workflow updates appear necessary to support the BOM variant feature and are not unrelated scope.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pkg/bom/bom.go`:
- Around line 114-121: Add a regression assertion for the serialized CycloneDX
output produced by BuildBOM, not just the markdown and duplicate-count checks in
TestLegacyEntryPointsUnchanged. Use BuildBOM and the encoded BOM serialization
path to compare the full byte-stable output, verifying component/dependency
ordering and image dedup remain unchanged for the legacy entry point.

In `@tools/bom/freshness_test.go`:
- Around line 465-486: In parseBOMVariantsTable, the generic blank-row skip
currently allows all-empty table rows like | | | | | to be ignored before
validation, so remove that early continue and let the existing empty-cell checks
reject malformed rows. Update the row handling around expectSeparator/cell
validation to ensure blank version rows surface an error, and add a
TestParseBOMVariantsTable case covering the all-blank row to lock in the
behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: 2dadc471-b53d-442a-ae1e-512100a4ff33

📥 Commits

Reviewing files that changed from the base of the PR and between e1bd87a and df55122.

📒 Files selected for processing (14)
  • .github/workflows/merge-gate.yaml
  • docs/contributor/recipe.md
  • docs/user/container-images.md
  • pkg/bom/bom.go
  • pkg/bom/markdown.go
  • pkg/bom/variants.go
  • pkg/bom/variants_test.go
  • tools/bom/freshness_parse_test.go
  • tools/bom/freshness_test.go
  • tools/bom/main.go
  • tools/bom/main_test.go
  • tools/bom/variants.go
  • tools/bom/variants_test.go
  • tools/bom/variants_unix_test.go

Comment thread pkg/bom/bom.go
Comment thread tools/bom/freshness_test.go
@yuanchen8911
yuanchen8911 force-pushed the feat/bom-variant-rows branch from df55122 to 287705f Compare July 10, 2026 16:44
@yuanchen8911
yuanchen8911 marked this pull request as ready for review July 10, 2026 16:45
@yuanchen8911
yuanchen8911 requested review from a team as code owners July 10, 2026 16:45
@yuanchen8911
yuanchen8911 requested a review from mchmarny July 10, 2026 16:45
@yuanchen8911 yuanchen8911 changed the title WIP: feat(bom): render divergent recipe version pins as BOM variants feat(bom): render divergent recipe version pins as BOM variants Jul 10, 2026
@yuanchen8911
yuanchen8911 force-pushed the feat/bom-variant-rows branch from 287705f to bfac8fb Compare July 10, 2026 16:52

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tools/bom/variants.go`:
- Around line 264-315: Make deriveVariants cancellation-aware by adding the
existing context parameter and checking ctx.Done() while iterating over sources
and refs, returning ctx.Err() when cancelled. Update its caller(s), including
the relevant command flow, to pass the context through without changing variant
aggregation behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: 4be67163-ff6f-456f-8d51-4f9608df63fe

📥 Commits

Reviewing files that changed from the base of the PR and between df55122 and bfac8fb.

📒 Files selected for processing (14)
  • .github/workflows/merge-gate.yaml
  • docs/contributor/recipe.md
  • docs/user/container-images.md
  • pkg/bom/bom.go
  • pkg/bom/markdown.go
  • pkg/bom/variants.go
  • pkg/bom/variants_test.go
  • tools/bom/freshness_parse_test.go
  • tools/bom/freshness_test.go
  • tools/bom/main.go
  • tools/bom/main_test.go
  • tools/bom/variants.go
  • tools/bom/variants_test.go
  • tools/bom/variants_unix_test.go

Comment thread tools/bom/variants.go
@yuanchen8911
yuanchen8911 force-pushed the feat/bom-variant-rows branch from bfac8fb to 08621e3 Compare July 10, 2026 17:26

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
tools/bom/variants.go (1)

264-341: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

deriveVariants still isn't cancellation-aware.

This traverses arbitrary -repo-root declarations (potentially large/nested overlay trees) but has no context.Context parameter and never checks for cancellation, unlike loadRecipeSources/walkYAML which already do. This was flagged in a previous review round and the fix was not applied to this function.

🔧 Proposed fix
-func deriveVariants(reg *registry, sources []recipeSource) ([]bom.VariantResult, error) {
+func deriveVariants(ctx context.Context, reg *registry, sources []recipeSource) ([]bom.VariantResult, error) {
 	byName := make(map[string]component, len(reg.Components))
 	for _, c := range reg.Components {
 		byName[c.Name] = c
 	}

 	agg := map[variantKey]map[string]struct{}{}
 	for _, src := range sources {
 		for _, ref := range src.Refs {
+			if ctxErr := ctx.Err(); ctxErr != nil {
+				return nil, errors.Wrap(errors.ErrCodeTimeout, "deriving BOM variants canceled", ctxErr)
+			}
 			pin := ref.Version

Also update the caller in tools/bom/main.go (deriveVariants(reg, sources)deriveVariants(ctx, reg, sources)) and the test call sites in tools/bom/variants_test.go.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tools/bom/variants.go` around lines 264 - 341, Make deriveVariants
cancellation-aware by adding a context.Context parameter, checking ctx.Err()
while traversing sources and refs, and returning the cancellation error
promptly. Update its caller in main and every test invocation to pass the
existing context, preserving the current aggregation behavior.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pkg/bom/bom.go`:
- Around line 312-369: Extract the shared Helm property construction from
componentProperties and variantComponent into a helper that assembles
aicr:helm:repository, aicr:helm:chart, aicr:helm:version, and
aicr:helm:namespace using the existing component fields and omission rules.
Update both functions to call the helper, while keeping componentProperties’
Kustomize-specific property names and behavior intact.

In `@tools/bom/freshness_test.go`:
- Around line 402-490: Ensure parseBOMVariantsTable validates expectSeparator
before handling any non-table line and after the scan ends, returning the
existing invalid-delimiter error when a matched header is not immediately
followed by a valid delimiter row; do not silently reset
inTable/expectSeparator. Update the loop logic and add an end-of-function guard
so a header followed by blank/prose/heading or end-of-input is rejected.

---

Duplicate comments:
In `@tools/bom/variants.go`:
- Around line 264-341: Make deriveVariants cancellation-aware by adding a
context.Context parameter, checking ctx.Err() while traversing sources and refs,
and returning the cancellation error promptly. Update its caller in main and
every test invocation to pass the existing context, preserving the current
aggregation behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: c124cb74-b06f-4849-9f90-1566e0194eb3

📥 Commits

Reviewing files that changed from the base of the PR and between bfac8fb and 08621e3.

📒 Files selected for processing (14)
  • .github/workflows/merge-gate.yaml
  • docs/contributor/recipe.md
  • docs/user/container-images.md
  • pkg/bom/bom.go
  • pkg/bom/markdown.go
  • pkg/bom/variants.go
  • pkg/bom/variants_test.go
  • tools/bom/freshness_parse_test.go
  • tools/bom/freshness_test.go
  • tools/bom/main.go
  • tools/bom/main_test.go
  • tools/bom/variants.go
  • tools/bom/variants_test.go
  • tools/bom/variants_unix_test.go

Comment thread pkg/bom/bom.go
Comment thread tools/bom/freshness_test.go

@mchmarny mchmarny left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Solid design and execution. The declaration-level scope (pins vs registry defaults, no effective-recipe resolution) is the right cut for #1611, and the implementation holds its fail-closed promises: strict mixin decode matching the store's KnownFields semantics, duplicate-source and padded-pin rejection, os.Root-confined size-bounded reads with the O_NONBLOCK/fstat trick against blocking FIFOs, and a checked bom-ref identity set so variants can't produce an ambiguous CycloneDX graph. I verified the .yaml-only filter matches the store's, surveyVariant's version swap is safe (Helm is a value struct, no registry aliasing), and the bidirectional freshness gate plus the merge-gate PASS-line assertion for both tests closes the rename-to-no-op hole. The variants-table parser is appropriately paranoid and well pinned by TestParseBOMVariantsTable.

Two inline comments, both low severity (a scan-scope assumption vs the store's full-tree walk that deserves an explicit doc note, and a minor fail-closed inconsistency for direct in-tree FIFOs) — neither blocks merge. Core CI (Test, Lint, bom-freshness, CodeQL) is green; E2E was still running at review time.

Comment thread tools/bom/variants.go
Comment thread tools/bom/variants.go
Issue NVIDIA#1611: a base/overlay/mixin that pins a Helm component to a
version different from the registry default deploys images the
container BOM never mentioned. This adds declaration-level variant
discovery to tools/bom and variant-aware entry points to pkg/bom, so
every explicitly pinned divergent chart version appears in the BOM
alongside the registry default.

- tools/bom: loadRecipeSources reads every overlay/mixin under the
  same -repo-root recursively (os.Root-confined, size-bounded reads,
  canonical loader kind parity: mixins hard-require RecipeMixin,
  overlays accept legacy empty kind and skip foreign kinds); malformed
  sources, duplicate source names, and whitespace-padded pins fail
  closed. deriveVariants compares each explicit Helm componentRef pin
  against the registry defaultVersion and aggregates divergences by
  (component, version). Variants render through the same
  surveyComponent path at the variant version (catalog parity: shared
  per-component values, registry chart coordinates).
- pkg/bom: additive BuildBOMWithVariants/WriteMarkdownWithVariants.
  Variant entries get version-qualified bom-refs
  ("<name>/<comp>@<ver>") with aicr:variant:of/aicr:variant:sources
  properties; the checked builder fails closed on any duplicate
  bom-ref. Markdown gains a "Version variants" table and per-variant
  image sections (omitted when no variants exist) plus a
  caller-supplied rendering-fidelity note. Legacy BuildBOM and
  WriteMarkdown are byte-identical to before (pinned by test).
- Freshness: TestCommittedBOMVariantsMatchRecipePins gates the
  committed variants table bidirectionally (new divergent pin without
  a row fails; stale row without a backing pin fails); the merge-gate
  bom-freshness job runs and positively asserts both freshness tests.

Non-goals, per the revised NVIDIA#1611: effective-recipe resolution
(inheritance/composition), source/chart coordinate divergence, and
componentRef type analysis beyond "is this declared pin a Helm pin"
are outside the variants model; variants render at registry
coordinates by definition.

Fixes NVIDIA#1611

Signed-off-by: Yuan Chen <yuanchen97@gmail.com>
Add a CycloneDX serialization equivalence assertion to
TestLegacyEntryPointsUnchanged so BuildBOM and BuildBOMWithVariants(nil)
stay byte-stable across component/dependency ordering and image dedup.

Reject all-blank Version-variants data rows in the freshness parser's
gate instead of silently skipping them, with a covering test case.

Signed-off-by: Yuan Chen <yuanchen97@gmail.com>
parseBOMVariantsTable set expectSeparator when a header matched but only
validated the delimiter row when the following line began with a pipe. A
header followed by a blank line, prose, a new heading, or end-of-input
took the non-pipe branch (or ended the loop) and silently reset table
state, counting the table as present-but-empty with no error — violating
the parser's own "malformed rows are errors, not skips" contract.

Fail closed in both spots: reject a matched header not immediately
followed by a valid delimiter row, and guard the end-of-section case.
Add parser test cases for a header followed by prose and a header as the
last line of the section.

Signed-off-by: Yuan Chen <yuanchen97@gmail.com>
Add a scope note on sourceDirs explaining that the BOM variant loader
intentionally scans only overlays/ and mixins/, diverging from the canonical
metadata store (pkg/recipe/metadata_store.go) which walks the whole recipes/
tree. By convention every overlay lives under overlays/, so the two scopes
agree today; the note flags that a future layout change placing RecipeMetadata
elsewhere would require widening this list to avoid dropping a divergent pin.

Signed-off-by: Yuan Chen <yuanchen97@gmail.com>
@yuanchen8911
yuanchen8911 force-pushed the feat/bom-variant-rows branch from 103aa96 to 0bfb9f8 Compare July 10, 2026 19:03
@yuanchen8911
yuanchen8911 requested a review from mchmarny July 10, 2026 19:04
@yuanchen8911
yuanchen8911 enabled auto-merge (squash) July 10, 2026 20:18
@yuanchen8911
yuanchen8911 merged commit ddea27d into NVIDIA:main Jul 10, 2026
38 of 40 checks passed
mohityadav8 pushed a commit to mohityadav8/aicr that referenced this pull request Jul 14, 2026
…IA#1634)

Signed-off-by: Yuan Chen <yuanchen97@gmail.com>
Co-authored-by: Mark Chmarny <mchmarny@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/ci area/docs size/XL theme/supply-chain SLSA, SBOM, Sigstore, and provenance verification

Projects

None yet

Development

Successfully merging this pull request may close these issues.

chore(recipes): represent explicit per-source version variants in the BOM

2 participants