Skip to content

docs(bazel): propose consolidating onto a single Bazel module#445

Open
balajinvda wants to merge 7 commits into
mainfrom
docs/bazel-consolidation-plan
Open

docs(bazel): propose consolidating onto a single Bazel module#445
balajinvda wants to merge 7 commits into
mainfrom
docs/bazel-consolidation-plan

Conversation

@balajinvda

@balajinvda balajinvda commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

TL;DR

Bazel arrived here service by service, so first-party code builds as 19 nested modules plus the root. That has a correctness cost: services consume in-repository libraries through published Go pseudoversions, so a library and its consumer can change together in one commit while Bazel still tests the consumer against the older published copy. Green build, broken code.

This adds the consolidation proposal and the tooling that keeps its numbers honest. Documentation and tooling only.

Additional Details

See #448 for the full proposal, inventory, phases, and CI analysis.

The tool exists because prose counts go stale silently: the first draft was invalidated within hours by an unrelated change. The document carries a measurement stamp.

Merging this commits to the analysis, not to the migration.

For the Reviewer

Closest attention on tools/ci/bazel-consolidation-inventory. Several defects were found by review; each now has a fixture test that fails when the defect is reintroduced:

  • A :(exclude) pathspec combined with a wildcard positive pathspec is silently ignored, so vendored files leaked into every first-party count.
  • An unanchored pattern could not match the root-level rules/oci/ copy.
  • Contents were read from the working tree while the output stamped HEAD.
  • || true on whole pipelines turned an unreadable file into a silent zero, and unchecked git exit statuses did the same.
  • Creating the scratch file before the cleanliness check let the tool dirty its own checkout.

The last one took two attempts: relying on set -e to propagate out of a function running as a pipeline stage does not work here. It now checks status explicitly, with a preflight covering every measurement.

For QA

Not applicable. No runtime or deployment behavior changes.

tools/ci/bazel-consolidation-inventory
tools/scripts/test/test-bazel-consolidation-inventory

Issues

Relates to #448

Checklist

  • I am familiar with the Contributing Guidelines.
  • I have signed off my commits for Developer Certificate of Origin (DCO) compliance.
  • New or existing tests cover these changes.
  • The documentation is up to date with these changes.

The repository builds as 19 independent Bazel modules, one per service. That
layout is inherited from the pre-monorepo history, when each service was its own
repository, and it is the direct cause of several recurring failures rather than
a style preference.

Documents the evidence measured on the current tree: configuration copied 19
times and drifting, rule versions identical across every module so the isolation
is unused, more than 20 unchosen Go dependency version conflicts so the
isolation is actively harmful, and 18 registry fetches per full matrix to obtain
3 distinct base images.

Proposes one module with services as packages, keeping release granularity per
service, and deriving CI scheduling from the build graph instead of a
hand-maintained subtree list. Includes phases, costs, the parallel-migration
constraint on the cloud-tasks labels an external project builds against, and
adjacent cleanups that share the same root cause.

Explicit non-goal: splitting the Java tree into standalone modules. It is
already in the target shape.

Co-authored-by: Balaji Ganesan <bganesan@nvidia.com>
Signed-off-by: Balaji Ganesan <bganesan@nvidia.com>
@balajinvda
balajinvda requested a review from a team as a code owner July 25, 2026 17:07
@balajinvda
balajinvda requested a review from apartha-nv July 25, 2026 17:07
@coderabbitai

coderabbitai Bot commented Jul 25, 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

Expands the proposal to consolidate first-party service Bazel modules into one root module, adds phased migration and CI controls, and introduces a tested inventory script for measuring Bazel, visibility, toolchain, and OCI configuration.

Changes

Bazel consolidation

Layer / File(s) Summary
Repository inventory measurements
tools/ci/bazel-consolidation-inventory, tools/scripts/test/test-bazel-consolidation-inventory
Adds inventory measurements and fixture-based tests covering tracked-file provenance, Bazel duplication, visibility patterns, toolchain versions, OCI pulls, digests, dirty worktrees, and failure handling.
Consolidation rationale and target model
docs/dev/bazel-consolidation.md
Defines the root Bazel module while preserving language artifacts, release ownership, Java workflows, and public interfaces; documents correctness concerns, duplication evidence, and visibility prerequisites.
Migration controls and phased execution
docs/dev/bazel-consolidation.md
Specifies hybrid CI requirements, migration risks, public-label constraints, component exit criteria, nested-module exceptions, the eight-phase rollout, cleanup items, non-goals, and open questions.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related issues

  • NVIDIA/nvcf issue 448 — Covers the same single-root Bazel consolidation proposal and migration controls.
  • NVIDIA/nvcf issue 449 — Covers the inventory script and nested-module exception contract.
  • NVIDIA/nvcf issue 450 — Covers visibility policy, Gazelle enforcement, and nested-module guards.
  • NVIDIA/nvcf issue 451 — Covers Bazel and Go SDK toolchain convergence.
  • NVIDIA/nvcf issue 452 — Covers centralization of OCI, platform, and stamping contracts.

Suggested reviewers: apartha-nv

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title follows Conventional Commits and accurately reflects the PR’s main Bazel consolidation proposal.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch docs/bazel-consolidation-plan

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

@github-actions

Copy link
Copy Markdown
Contributor

@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 `@docs/dev/bazel-consolidation.md`:
- Around line 132-135: Update the fenced code block containing the nvct_core
Bazel targets to specify an explicit language, using text or another appropriate
language identifier in the opening fence, while leaving the block contents
unchanged.
- Around line 76-89: The CI migration plan in the documentation incorrectly
describes scheduling as static and proposes replacing functionality already
present in the workflow. Update the rationale and the Phase 5 section to
acknowledge the existing changed-file labeling, bazel query rdeps logic, and
full-build fallback, and focus the remaining work on consolidating the
multi-module layout while preserving current CI 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: CHILL

Plan: Enterprise

Run ID: 48412079-3085-42c8-8a47-236d9a5c6126

📥 Commits

Reviewing files that changed from the base of the PR and between 1c29b4b and 030eeb4.

📒 Files selected for processing (1)
  • docs/dev/bazel-consolidation.md

Comment thread docs/dev/bazel-consolidation.md Outdated
Comment thread docs/dev/bazel-consolidation.md Outdated
@balajinvda

Copy link
Copy Markdown
Contributor Author

Thanks -- this is a better plan than what I wrote. I verified every factual claim against main 1c29b4b7 before revising, and all of them hold. Pushed in 34b206c3.

The correction that mattered

You are right that CI already combines changed-file mapping, rdeps, and a conservative full-build fallback. My claim that scheduling is "a hand-maintained approximation of the dependency graph" was wrong, and I proposed building something that exists. I read the static subtree list at the top of the workflow and did not read as far as the affected-target implementation.

The revision distinguishes two layers rather than simply retracting, because I think the distinction is load-bearing:

  1. Target selection inside a job already uses rdeps, with fallbacks for non-modify changes, global files, and unmodeled file types. Keep it.
  2. Job selection across the matrix is static, because 19 modules cannot be one query.

So consolidation does not add rdeps; it removes the need for layer 2. The document now says the hybrid must be extended during every phase, not deferred, and that the conservative fallback is a feature.

Accepted as written

  • Inventory refreshed. All 19 subtrees pin a version, 15 on 8.6.0 and 4 on 9.1.1. My own in-flight change landed and invalidated the draft within hours, so the document now carries a measurement stamp and ships tools/ci/bazel-consolidation-inventory to regenerate the numbers instead of trusting prose.
  • Scope corrected: one Bazel module does not mean one Go module, one Cargo workspace, or one lockfile per language. Component go.mod and Cargo workspaces stay; the target is one root MODULE.bazel.lock and one Bazel-resolved graph. This was the biggest conceptual error in the draft.
  • Label rebasing and visibility promoted to the primary risk, with the counts: 156 root-relative __subpackages__, 639 public-visibility occurrences, zero package_group definitions. I had not covered it at all.
  • Dropped "conflicts mostly resolve by taking the newer version."
  • Rust moves into the root module while retaining separate Cargo workspaces and crate hubs; merging is a later, separately justified step. cargo test --workspace --all-targets stays until Bazel declares every Cargo-discovered test.
  • NVCA gets its own phase (1261 vendored BUILD files), with ESS and BYOO as further exceptions.
  • OCI and stamping equivalence gates before any service moves, including how per-service versions stamp when one workspace-status command runs per invocation.
  • Fetch and compile savings qualified to one repository definition and one digest pin. Two people independently flagged that wording, which means it was genuinely misleading.
  • Phase 0 guard needs an allowlist for the intentional helper and vendored modules.

I adopted your phase order over mine. Visibility policy and OCI equivalence gates before any service moves is right, and I had both as afterthoughts.

One thing I did not change: the rules/oci and stamping duplication is described as needing a shared API with per-component configuration rather than mechanical deletion, which matches your point that those copies contain real behavioral differences.

Corrects the plan on the points raised in review, and adds
tools/ci/bazel-consolidation-inventory so the quoted measurements can be
regenerated instead of trusted.

The material correction is the CI section. The first draft claimed CI
scheduling is a hand-maintained approximation of the dependency graph and
proposed replacing it with bazel query rdeps. That was wrong: the workflow
already maps changed files to labels, queries reverse dependencies, and falls
back to a full build for changes it does not model. The revision distinguishes
the two layers, notes that only job selection across the matrix is static, and
says the existing hybrid must be extended during every phase rather than
replaced at the end.

Other corrections:
- Refreshed inventory. All 19 service subtrees now pin a Bazel version, 15 on
  8.6.0 and 4 on 9.1.1; the previous draft said four were missing. Added a
  measurement stamp, since an in-flight change invalidated the first draft
  within hours.
- Scoped the proposal: one Bazel module does not mean one Go module, one Cargo
  workspace, or one lockfile per language. Component go.mod and Cargo
  workspaces stay; the target is one root MODULE.bazel.lock.
- Added label rebasing and visibility as the primary migration risk, with
  counts: 156 root-relative __subpackages__ declarations, 639 public-visibility
  occurrences, and zero package_group definitions.
- Dropped the claim that dependency conflicts mostly resolve by taking the
  newer version. Kubernetes families carry conflicting replace directives and
  toolchains span major versions.
- Rust moves into the root module while retaining separate Cargo workspaces and
  crate hubs; merging them is a later, separately justified optimization.
- NVCA gets its own phase (1261 vendored BUILD files with //vendor labels that
  do not survive a root move), with ESS and BYOO as further exceptions.
- Added OCI and stamping equivalence gates before any service moves, and noted
  the nested-module guard needs an allowlist for the intentional helper and
  vendored modules.
- Qualified the fetch and compile savings: one repository definition and one
  digest pin, not one fetch across isolated runners.

Co-authored-by: Balaji Ganesan <bganesan@nvidia.com>
Signed-off-by: Balaji Ganesan <bganesan@nvidia.com>
@balajinvda
balajinvda requested a review from a team as a code owner July 25, 2026 22:43
@balajinvda

Copy link
Copy Markdown
Contributor Author

Correction to my previous comment: the revision is a140acb4, not 34b206c3. I had committed the revision on top of main rather than on this branch, so it was never pushed and the PR still showed the original draft when I said otherwise. Rebuilt on the branch head and pushed.

PR now shows 2 files, +277: the revised docs/dev/bazel-consolidation.md and tools/ci/bazel-consolidation-inventory.

@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/ci/bazel-consolidation-inventory`:
- Around line 16-71: The inventory tool lacks focused coverage for its parsing
and filtering behavior. Add fixture-based tests for the vendored-module
exclusion, trailing-comma oci.pull block parsing, and empty/no-match inputs,
covering the relevant commands and embedded Python logic in the inventory
entrypoint; alternatively document in the change why such tests are not
applicable.
🪄 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: CHILL

Plan: Enterprise

Run ID: e2f77516-6d74-4ca6-b4a1-0d82dc6ee7a3

📥 Commits

Reviewing files that changed from the base of the PR and between 030eeb4 and a140acb.

📒 Files selected for processing (2)
  • docs/dev/bazel-consolidation.md
  • tools/ci/bazel-consolidation-inventory

Comment thread tools/ci/bazel-consolidation-inventory Outdated
Second review pass. Six findings, all confirmed against the tree before
being acted on.

The inventory tool had a real bug that made its output wrong rather than
merely imprecise. It filtered vendored trees with a git ':(exclude)'
pathspec combined with a wildcard positive pathspec, a combination git
silently ignores. Every first-party count therefore included nvca's 1261
vendored BUILD files: the public-visibility count was 2721 instead of
639. A second defect used an unanchored '/rules/oci/' pattern that could
not match the root-level copy, since that path has no leading slash.
Both are now explicit anchored greps over git-tracked files, and both are
covered by fixture tests that fail when either defect is reintroduced.

The tool now also reads tracked files rather than the working tree, so
its measurements correspond to the commit it stamps, warns when the tree
is dirty, and computes every measurement into a variable before printing
so a failed measurement aborts instead of yielding partial output with a
success status.

Go SDK provenance is now reported separately. The 1.23.0 pin the plan
quoted comes only from a vendored module; first-party code spans 1.25.0
through 1.25.11.

OCI pull reporting was scoped to nvcr.io and undercounted. There are 23
declarations across 8 distinct images and 7 digests, not 18 and 3. The
one tag-only pull is now named in the output rather than asserted in
prose.

Doc corrections:

- nvsnap is excluded by the non-goals but was counted in the 19 modules
  converging on the root. The end state is now stated as 18 publicly
  buildable modules plus nvsnap as an allowlisted exception, and Phase 1
  and Phase 8 are scoped accordingly.
- The claim that one module makes "which subtrees" a non-question was too
  broad. Consolidation retires the static module-root list, but affected
  targets still need routing to Docker-host, Java component, Cargo
  parity, BYOO generation, and release lanes. Root-scoped Java already
  routes this way on component_kind and ci_lane.
- The phase invariant now requires every declared lane to pass, not only
  bazel build and test at the root.
- Added per-component exit criteria, with root-local //src/... label
  resolution as the criterion that cannot be waived.
- NVCA, ESS, and BYOO were named as special but never scheduled. They are
  now subphases 6a through 6c, each with an owner and the option to move
  to the exception allowlist rather than stay open.
- 639 public-visibility declarations with zero package_group definitions
  means a very broad expressed API surface, not the absence of one.
- Phase 7 now says to add and retain the Cargo parity lane. GitHub CI has
  no cargo test lane today.

Co-authored-by: Balaji Ganesan <bganesan@nvidia.com>
Signed-off-by: Balaji Ganesan <bganesan@nvidia.com>
@balajinvda

Copy link
Copy Markdown
Contributor Author

All six addressed in e8148d97. I verified each claim against the tree before acting; all six were correct.

4 is the one that mattered, and it was worse than reported

You flagged the inventory tool as needing "a small correctness pass." It had a bug that made its output wrong, not imprecise.

Vendor filtering used a git :(exclude) pathspec combined with a wildcard positive pathspec. Git silently ignores the exclusion in that combination:

$ git ls-files -- 'src/**/BUILD.bazel' ':(exclude)*/vendor/*' | wc -l
1846
$ git ls-files -- 'src/**/BUILD.bazel'                        | wc -l
1846        # identical; 1280 vendored paths survive

So every first-party count silently included nvca's 1261 vendored BUILD files. The public-visibility count was 2721, not 639. The exclusion works with a literal positive pathspec, which is why it looked fine in isolation.

A second defect: '**/rules/oci/*' cannot match the root-level rules/oci/ copy, because that path has no leading component. Six files were being dropped.

Both are now explicit anchored greps over git ls-files, and both are covered by fixture tests. I reintroduced each defect and confirmed the tests fail:

FAIL: excludes vendored public visibility: expected '... : 1', got '... : 2'
FAIL: includes root-level rules/oci and excludes vendored: expected '3 files'

Also fixed as you described: tracked files rather than the working tree, a dirty-tree warning on the stamp line, and every measurement computed into a variable before printing so a failed measurement aborts instead of producing partial output with exit 0. Tests live at tools/scripts/test/test-bazel-consolidation-inventory per tools/AGENTS.md.

Two measurements changed as a result:

  • Go SDK. Confirmed 1.23.0 comes only from src/compute-plane-services/nvca/vendor/cel.dev/expr/MODULE.bazel. First-party is 1.25.0 through 1.25.11. Now reported on separate lines.
  • OCI pulls. The old count was scoped to nvcr.io and undercounted. There are 23 declarations across 8 distinct images and 7 digests, not 18 and 3. The tool now names the single tag-only pull rather than leaving it as a prose assertion.

The rest

  1. nvsnap. Correct, and the inconsistency was mine. Adopted your wording. Phase 1's allowlist now names nvsnap explicitly and Phase 8 is scoped to leave allowlisted exceptions in place.

  2. Lane selection. Adopted your wording. I verified the Java descriptor path routes on component_kind and ci_lane, which is exactly the counterexample to my claim. The phase invariant now requires every declared lane to pass, not just root build and test.

  3. Exit criteria. Added as its own section ahead of the phases, with root-local //src/... label resolution marked as the criterion that cannot be waived, since it is the correctness goal the whole proposal rests on. NVCA, ESS, and BYOO are now subphases 6a through 6c, each with an owner and an explicit option to move to the exception allowlist rather than remain open indefinitely.

  4. Both corrected. On the first: 639 public declarations with zero package_group is a very broad expressed surface, and the document now says consolidation must inventory it before restricting it, because the breadth is load-bearing for consumers. On the second, confirmed no workflow runs cargo test, so Phase 7 says add and retain.

  5. PR description rewritten.

@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 `@docs/dev/bazel-consolidation.md`:
- Around line 159-161: Update the Go-label correctness criterion in the
consolidation document to apply only to in-scope, non-exception components,
while explicitly preserving the allowlisted vendored and nested-module
exceptions such as nvsnap and the NVCA vendored tree. Define the exit evidence
required for those exceptions instead of requiring them to resolve to root-local
//src/... labels.

In `@tools/scripts/test/test-bazel-consolidation-inventory`:
- Around line 40-45: Update make_tempdir and both command-substitution call
sites so the created temporary directory is recorded in TEMP_DIRS in the parent
shell, not a subshell. Preserve the existing EXIT-trap cleanup behavior and
ensure callers still receive the directory path.
🪄 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: CHILL

Plan: Enterprise

Run ID: 7588276e-644c-489f-9e9a-972d744f21ba

📥 Commits

Reviewing files that changed from the base of the PR and between a140acb and e8148d9.

📒 Files selected for processing (3)
  • docs/dev/bazel-consolidation.md
  • tools/ci/bazel-consolidation-inventory
  • tools/scripts/test/test-bazel-consolidation-inventory
🚧 Files skipped from review as they are similar to previous changes (1)
  • tools/ci/bazel-consolidation-inventory

Comment thread docs/dev/bazel-consolidation.md
Comment thread tools/scripts/test/test-bazel-consolidation-inventory
… contract

Third review pass.

The inventory tool stamped HEAD while reading file contents from the working
tree, so an uncommitted edit to a tracked file changed the measurements without
changing the stamp. Verified: touching one tracked BUILD.bazel moved the
public-visibility count from 639 to 640 with the stamp unchanged. The tool now
refuses to run on a dirty tree and offers --allow-dirty, which stamps the output
as a working-tree measurement instead of a commit.

The fixture suite leaked two temporary directories per run. make_tempdir was
called through command substitution, so it ran in a subshell and its append to
TEMP_DIRS never reached the parent shell's cleanup trap. It now assigns to a
variable. Verified zero leaked directories after the fix.

Separately, the tool only scanned paths under src/, so it never read the root
BUILD.bazel. That is where the repository's first package_group appears, which
means the plan's headline "zero package_group definitions" evidence was measured
somewhere it could not observe a change. Now repository-wide, which also moves
the public-visibility count from 639 to 644.

Both new behaviors have fixture coverage: a modified tracked file must be
rejected, and --allow-dirty must both stamp differently and actually observe the
uncommitted occurrence.

Doc corrections:

- BYOO already builds through a declared Bazel genrule. Phase 6c said the
  generator must become one. The real problem is that the action uses host Go
  with local and no-sandbox, so it is neither hermetic nor remotely cacheable.
- Added an exception contract. Retaining a nested module now requires an owner,
  the retained CI entry point, the first-party dependency-source strategy,
  residual correctness risk, and a dated revisit trigger. An entry missing any of
  these is unfinished work, not an exception.
- Reconciled the exception list with the summary: moving NVCA, ESS, or BYOO to
  the allowlist lowers the count of converging modules and keeps that component's
  lockfile and module-root CI lane, so the summary is updated when it happens.
- Scoped the root-local-label criterion to migrated components, without waiving
  it for vendored first-party code. NVCA has 80 BUILD files referring to vendored
  copies of NVCF Go libraries, which is the false-green condition this proposal
  exists to remove.

Co-authored-by: Balaji Ganesan <bganesan@nvidia.com>
Signed-off-by: Balaji Ganesan <bganesan@nvidia.com>
@balajinvda
balajinvda force-pushed the docs/bazel-consolidation-plan branch from e8148d9 to 39aa7e5 Compare July 26, 2026 02:22
@balajinvda

Copy link
Copy Markdown
Contributor Author

All five blockers addressed in 39aa7e56, plus the BYOO correction. Branch force-pushed; head is now 39aa7e56.

Tracking issues opened as requested: #448 (epic, carries the plan), #449 through #452 (phases 1 to 4), #453 through #455 (adjacent cleanup). All labeled ci and assigned. The ci label did not exist and was created.

3 and 4 were both real, and 4 was mine to have caught

The tempdir leak reproduces exactly as described: make_tempdir was called through command substitution, so it ran in a subshell and the TEMP_DIRS append never reached the parent trap. Measured two leaked directories per run, zero after the fix. It now assigns to a variable instead of echoing.

On the dirty-tree stamp, you are right that git ls-files only fixes path selection. Contents still came from the working tree. Confirmed by editing one tracked BUILD.bazel: the count moved 639 to 640 while the stamp still read HEAD. The tool now refuses to run on a dirty tree, with --allow-dirty available and the stamp changing to say so.

That fix exposed something worse that I should have found myself. The tool only scanned paths under src/, so it never read the root BUILD.bazel. That is exactly where #447 introduces the repository's first package_group, meaning the plan's headline "zero package_group definitions" evidence was measured somewhere structurally incapable of observing it change. Now repository-wide, which also moves the public-visibility count from 639 to 644.

New fixture coverage: a modified tracked file must be rejected, and --allow-dirty must both stamp differently and actually observe the uncommitted occurrence, so the override cannot silently return a stale measurement.

5, and the BYOO correction

Your BYOO correction is right and I verified it: otelcol/BUILD.bazel already declares a genrule, with local = True. Phase 6c said the generator "must become a declared Bazel action," which described a problem that does not exist. It now reads: make the action hermetic, retain its dedicated local lane, or record an exception.

On the exception contract, I adopted your five evidence fields as a named section, and reconciled it with the summary: moving a component to the allowlist lowers the count of converging modules and keeps that component's lockfile and module-root CI lane, so the summary is updated when it happens.

The root-local-label criterion is now scoped to migrated components without being waived for vendored first-party code. I measured 80 NVCA BUILD files referring to vendored NVCF Go libraries rather than your 69, which is pattern-dependent, but the point stands unchanged: that is the false-green condition the proposal exists to remove, so a component can be excepted from moving but not from the correctness goal while claiming to be done.

1 and 2

All four commits carry Signed-off-by. Worth flagging one thing: my first attempt appended the sign-off after a blank line, which broke the trailer block and silently dropped Co-authored-by from git's trailer parsing. Both trailers now parse on all four commits.

PR body rewritten against .github/PULL_REQUEST_TEMPLATE.md, 410 words, with the issue reference and checklist. It links to the proposal rather than repeating the inventory and CI analysis.

@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)
docs/dev/bazel-consolidation.md (1)

159-175: 🎯 Functional Correctness | 🟠 Major

Do not make the root-label criterion impossible for exceptions.

This repeats the previous unresolved issue: the text scopes the criterion to migrated components, then says a component may remain unmigrated but cannot be excepted from the correctness goal. That makes retained modules such as nvsnap unable to satisfy the definition of done. Qualify the criterion for in-scope, non-exception components and define exit evidence for exceptions.

🤖 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 `@docs/dev/bazel-consolidation.md` around lines 159 - 175, Revise the
root-local-label criterion and definition of done so it applies only to in-scope
components that are required to migrate, explicitly excluding approved
exceptions such as nvsnap. Add concrete exit evidence required for exceptions,
such as documented approval, rationale, and tracking of remaining vendored
references, while preserving the existing criteria for migrated components.
🤖 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 `@docs/dev/bazel-consolidation.md`:
- Around line 132-133: Update the prose sentence in the dependency-bump section
to hyphenate “repository-wide,” preserving the existing meaning and surrounding
wording.
- Around line 190-192: Reconcile the retained-module accounting throughout this
document: explicitly define whether the deliberate helper and vendored modules
join nvsnap as retained exceptions, then update every related summary count,
target end state, and migration criterion consistently across this section and
Phase 1.

---

Duplicate comments:
In `@docs/dev/bazel-consolidation.md`:
- Around line 159-175: Revise the root-local-label criterion and definition of
done so it applies only to in-scope components that are required to migrate,
explicitly excluding approved exceptions such as nvsnap. Add concrete exit
evidence required for exceptions, such as documented approval, rationale, and
tracking of remaining vendored references, while preserving the existing
criteria for migrated components.
🪄 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: CHILL

Plan: Enterprise

Run ID: efb9ff90-662e-4ae9-a3df-ec714e660445

📥 Commits

Reviewing files that changed from the base of the PR and between e8148d9 and 39aa7e5.

📒 Files selected for processing (3)
  • docs/dev/bazel-consolidation.md
  • tools/ci/bazel-consolidation-inventory
  • tools/scripts/test/test-bazel-consolidation-inventory
🚧 Files skipped from review as they are similar to previous changes (2)
  • tools/scripts/test/test-bazel-consolidation-inventory
  • tools/ci/bazel-consolidation-inventory

Comment thread docs/dev/bazel-consolidation.md Outdated
Comment thread docs/dev/bazel-consolidation.md Outdated
…rrent evidence

Fourth review pass.

The three visibility measurements appended `|| true` to the whole pipeline, so a
failure to read the files was converted into a silent zero. Confirmed by deleting
a tracked BUILD.bazel: the tool reported 640 instead of failing, and exited 0.

The first attempt at a fix was wrong and the negative control caught it. Relying
on `set -e` to propagate a failed command substitution out of a shell function
running as a pipeline stage does not work here; the tool still exited 0. It now
checks the read status explicitly, and a single preflight verifies every tracked
file the tool will read is present and readable. That covers every measurement
rather than only the three that were reported.

nvsnap was carried as a standing exception on the grounds that its base image is
not publicly pullable. That rationale is stale. The workflow comment still says
it, but the code disagrees: nvsnap pins public gcr.io/distroless/static,
docker.io/library/ubuntu, and docker.io/library/alpine bases by digest, builds
its images on @distroless_static, and its CONTRIBUTING documents a public-input
source build. Private NGC is the publish destination, not the base. The plan no
longer asserts the exception. All 19 first-party service modules are in scope,
and Phase 1 decides nvsnap on current evidence rather than inheriting a comment.

The retained-module accounting conflated three categories. The 22 tracked
MODULE.bazel files are 19 first-party services, the root, one migration
scaffolding module used by five nested modules and retired once they migrate, and
one vendored third-party module that is a guard exclusion rather than an
architectural decision. Only the first category can hold an exception.

Also corrected:

- ESS retains its Go workspace while the root Bazel graph ingests its modules.
  Collapsing it contradicted the scope boundary stated earlier in the document.
- Remote caching does not reduce analysis-graph growth. Caching avoids
  re-executing actions; the analysis phase still processes whatever the
  invocation asks for, so target selection is the mitigation.

Co-authored-by: Balaji Ganesan <bganesan@nvidia.com>
Signed-off-by: Balaji Ganesan <bganesan@nvidia.com>

@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 `@docs/dev/bazel-consolidation.md`:
- Around line 227-235: Update the Phase 1 inventory statement in the opening
paragraph to say “Three of the 22” instead of “Two of the 22,” matching the 19
service modules plus root, migration-scaffolding, and vendored modules described
immediately afterward.
🪄 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: CHILL

Plan: Enterprise

Run ID: 2131c465-c10d-474b-b392-b8c27bcf3149

📥 Commits

Reviewing files that changed from the base of the PR and between 39aa7e5 and 194824f.

📒 Files selected for processing (3)
  • docs/dev/bazel-consolidation.md
  • tools/ci/bazel-consolidation-inventory
  • tools/scripts/test/test-bazel-consolidation-inventory
🚧 Files skipped from review as they are similar to previous changes (2)
  • tools/ci/bazel-consolidation-inventory
  • tools/scripts/test/test-bazel-consolidation-inventory

Comment thread docs/dev/bazel-consolidation.md Outdated
@balajinvda

Copy link
Copy Markdown
Contributor Author

All three blocking findings and all four follow-ups addressed in 194824f5.

1, and a wrong first fix

Confirmed by deleting a tracked BUILD.bazel: the tool reported 640 and exited 0 rather than failing.

My first fix was wrong, and the negative control is what caught it. I replaced the blanket || true with a helper that tolerates only grep's no-match status, re-ran the deletion case, and it still exited 0. Relying on set -e to propagate a failed command substitution out of a shell function running as a pipeline stage does not work here, whatever the manual implies.

The tool now checks the read status explicitly rather than depending on set -e, and a single preflight verifies every tracked file it will read is present and readable. That covers every measurement rather than the three that were reported, since the same masking pattern was reachable elsewhere. Fixture test added: a missing tracked BUILD.bazel must fail even with --allow-dirty, and the failure must name the file.

2, which was the most useful finding

You are right and the evidence is unambiguous. nvsnap pins gcr.io/distroless/static, docker.io/library/ubuntu, and docker.io/library/alpine by digest, builds its images on @distroless_static, and its CONTRIBUTING documents a public-input source build. Private NGC is the publish destination, not the base.

The stale claim originates in a .github/workflows/bazel.yml comment that the code has since outgrown, and I propagated it into the plan without checking it. The plan no longer asserts the exception: all 19 first-party service modules are in scope, and Phase 1 decides nvsnap on current evidence instead of inheriting a comment. Its absence from the public matrix is now described as a CI row decision, not an architectural one.

This is the second time in this review cycle that a number or claim I carried forward turned out to be inherited rather than verified, which is the argument for the inventory tool existing at all.

3

Adopted. The 22 tracked modules are now presented as four categories with distinct end states: 19 first-party services that converge, the root, rules/oci-destinations as migration scaffolding retired once its five consumers migrate, and vendored cel.dev/expr as a guard exclusion. Only the first category can hold an architectural exception. Phase 1 is rewritten to match, and the summary states the end state consistently.

Follow-ups

  • Consolidate first-party Bazel code onto a single root module #448 refreshed. It carried 639 and the old BYOO wording. It now states which artifact wins if the two drift.
  • ESS keeps its Go workspace; the work is teaching the root graph to build its modules. Collapsing it contradicted the scope boundary stated earlier in the same document.
  • Remote caching no longer appears as an analysis-graph mitigation. Caching avoids re-executing actions; analysis still processes whatever the invocation asks for.
  • PR description is 330 words, so it stays under the limit once the generated section is appended.

…anisms

Fifth review pass.

Two more false-green paths, both reproduced by injecting the failure and both
previously printing a complete 23-line report and exiting 0:

- `git status` failing was indistinguishable from a clean tree, because its
  output was consumed inside a test rather than captured and checked.
- `git ls-files` failing in the preflight was discarded by process substitution,
  leaving an empty file list that every count read as a plausible zero.

Rather than patch each call site, the tracked-file list is now enumerated once
into a temporary file with its status checked, and every helper reads from that.
This removes the whole class: no helper re-invokes git inside a pipeline where a
failure becomes an empty result.

Test coverage was also incomplete. The missing-file case only exercised the
preflight, never count_occurrences, which is where the original masking lived.
A tracked symlink pointing at a directory passes the readability check and then
fails on read, which reaches count_occurrences directly. Both injected git
failures are covered too, asserting not only a non-zero exit but that no
measurements are printed.

Document corrections:

- Migration completion and exception completion contradicted each other. There
  are now two explicit closure paths: a migrated component satisfies the root
  module criteria, a retained service exception satisfies the exception contract,
  falls outside the migrated count, and records residual correctness risk.
- The nested-module guard needs three mechanisms rather than one allowlist:
  exact vendored-path exclusions, a migration ledger that must shrink
  monotonically, and permanent service exceptions. Requiring a permanent
  exception contract for every entry from Phase 2 was incompatible with
  incremental migration, because it cannot distinguish "not yet" from "never".
- Added a source-of-truth prerequisite phase. A subtree that is still
  upstream-authoritative in imports.yaml can have a later import restore its
  nested module and old labels, silently undoing the migration. stargate is in
  that position today.
- Only the root row on a pull request narrows through rdeps; every per-service
  row builds its whole workspace. Corrected, and noted as an argument for
  consolidation rather than against it.
- The nvsnap statement is qualified to its current Bazel targets. The legacy
  Dockerfile path still defaults to a private base, which is the likely origin of
  the original claim.
- Three of the 22 modules are not first-party services, not two. Also
  repository-wide, a language on the code fence, and Rust toolchain versions
  rather than major versions.

Co-authored-by: Balaji Ganesan <bganesan@nvidia.com>
Signed-off-by: Balaji Ganesan <bganesan@nvidia.com>
@balajinvda

Copy link
Copy Markdown
Contributor Author

All four blockers and all four small corrections addressed in 211f8dab. #448 re-synced.

1, and why I stopped patching call sites

Both reproduce exactly as you describe: injecting each failure gave a complete 23-line report and exit 0.

I fixed them at the source rather than individually, because the pattern was going to keep recurring. Every helper was re-invoking git ls-files inside a pipeline, where failure yields an empty list that reads as a plausible zero. The tracked-file list is now enumerated once into a temporary file with its status checked, and every helper reads from that. git status is captured and checked instead of consumed inside a test.

You are also right that the missing-file test only exercised the preflight. To reach count_occurrences, where the original masking lived, the test now commits a tracked symlink pointing at a directory: readable, so the preflight passes, then fails on read. Both injected git failures are covered too, asserting not only non-zero exit but that no measurements are printed, since a partial report exiting non-zero would still be misleading.

Verified after each fix:

break 'status'   -> exit=1  report_lines=0
break 'ls-files' -> exit=1  report_lines=0

2 and 3

Both accepted, and 3 is a correction to my reasoning rather than my wording. I had collapsed "not yet migrated" and "never migrating" into one allowlist, which cannot express incremental migration: nothing would ever force the first category to resolve. The guard now has three mechanisms, with the migration ledger required to shrink monotonically so a phase that adds an entry or omits a target phase fails.

For 2, the document now states two closure paths in a table. A retained exception is a legitimate outcome but not a quiet one: it lowers the migrated count, keeps a lockfile and module-root lane alive, and puts a stated correctness risk on the record.

4

Good catch, and it is the kind of thing that would have silently undone the work. A subtree that stays upstream-authoritative can have a later import restore its nested module and old labels. Added as a prerequisite phase ahead of the Rust move, requiring either a native ownership cutover or upstream Bazel changes landed and synchronized. That renumbered the final phase, so remaining references now say "the final phase" rather than a number.

Small corrections

All four verified before applying. On the last two:

  • bazel.yml states it explicitly: only the root row on a pull request is a narrowing candidate, and every per-service row builds its whole workspace. My description was too broad. I have noted this as an argument for consolidation rather than against it, since only the root row can narrow today because only the root row has a graph spanning the changed files.
  • docker/agent/Dockerfile.app.criuv2 defaults to a private nvsnap-agent-base, and the init image to a private builder. The nvsnap claim is now scoped to its current Bazel targets, and the Dockerfile paths are called out as the likely origin of the original assertion.

Also fixed: three of the 22 rather than two, repository-wide, a language on the code fence, and Rust toolchain versions.

…sure models

Sixth review pass.

The tool could dirty the checkout it was about to inspect. mktemp honours TMPDIR,
and the scratch file was created before the cleanliness check, so running with a
TMPDIR inside the repository made a clean tree report as dirty and refuse to run.
Reproduced with TMPDIR set to the repository root. The cleanliness check now runs
first, and a fixture covers it: a clean tree with TMPDIR inside the repository
must produce measurements, must not stamp itself as a working-tree measurement,
and must leave no scratch file behind. Reverting the ordering fails that fixture.

Two statements still contradicted the closure and guard models added last pass:

- The exit criteria said a phase is not complete until every component it touched
  satisfies the migration criteria, which excluded the retained-exception path
  that the same document had just introduced. It now reads as the criteria for a
  migrated component, with phase closure requiring either those criteria or the
  exception contract.
- Phase 1 still required an exception contract for every allowlist entry, which
  contradicts the three-mechanism guard and would make the guard unusable during
  the migration itself. Only permanent service exceptions carry a contract.

Also corrected:

- The exception contract applies to retained first-party service modules. The
  vendored module is a guard exclusion and carries no contract.
- The private nvsnap default is in docker/agent/Dockerfile.app, not the criuv2
  variant, which documents public inputs. I named the wrong file.
- imports.yaml is not part of the public tree, so the source-of-truth
  prerequisite now refers to subtrees synchronized from an upstream repository.
  The stargate cutover requirement is unchanged.
- Other CI rows build their full declared scope, which is a whole workspace only
  for nested modules; a root-scoped Java row builds its subtree of the root
  graph.
- Rust toolchains span releases from 1.91.1 to 1.97.0, not major releases.
- One cross-reference pointed below to a contract that is above it.

Co-authored-by: Balaji Ganesan <bganesan@nvidia.com>
Signed-off-by: Balaji Ganesan <bganesan@nvidia.com>
@balajinvda

Copy link
Copy Markdown
Contributor Author

All addressed in 52331dc6. The CodeRabbit thread is answered and resolved; unresolved threads are now zero.

1

Reproduced with TMPDIR set to the repository root: the tool dirtied the checkout it was about to inspect, and a clean tree was rejected. mktemp honours TMPDIR, and the scratch file was created before the cleanliness check.

The check now runs first, and the fixture asserts three things rather than just an exit code: measurements are produced, the stamp does not claim a working-tree measurement, and no scratch file is left in the repository. Reverting the ordering fails it:

FAIL: clean tree rejected when TMPDIR is inside the repository

Worth noting my own first attempt at verifying this was invalid. I tested with a freshly created .tmptest directory, which is itself untracked and so dirties the tree regardless of ordering. The fixture uses the repository root, which is the case a real user hits.

2

Both contradictions were mine, introduced by the fixes from the previous pass rather than surviving from before, which is the more annoying kind.

The exit criteria still said a phase is not complete until every component it touched satisfies the migration criteria, immediately after the same document introduced a second closure path. It now reads as the criteria for a migrated component, with phase closure requiring either those criteria or the exception contract.

Phase 1 still demanded a contract for every allowlist entry, which contradicts the three-mechanism guard and would make the guard unusable during the migration. Only permanent service exceptions carry a contract. And the contract now applies to retained first-party service modules, since the vendored module is a guard exclusion with nothing to justify.

Factual corrections

All verified before applying.

  • The private default is in docker/agent/Dockerfile.app, which pins nvsnap-agent-base plus private uvloop, libuv, and libzmq builder images. I named Dockerfile.app.criuv2, which pins an unqualified local base and documents public inputs. Wrong file; corrected, and the criuv2 variant is now explicitly named as not the offender.
  • imports.yaml is not in the public tree. The prerequisite now refers to subtrees synchronized from an upstream repository, keeping the stargate requirement.
  • "Full declared scope" rather than "whole workspace", since a root-scoped Java row builds its subtree of the root graph.
  • Rust toolchains span 1.91.1 to 1.97.0, confirmed against the declared toolchains. Releases, not major releases.
  • The cross-reference pointing below to a contract that is above it is fixed. The one in the closure table does point below and is unchanged.
  • PR body says "several defects" so it stops going stale.

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.

1 participant