Skip to content

Fix vendored Maven pin overriding BOM/parent management (#488) - #1359

Open
Mikola Lysenko (mikolalysenko) wants to merge 4 commits into
mainfrom
agent/v5-maven-external-mgmt
Open

Mikola Lysenko (mikolalysenko) wants to merge 4 commits into
mainfrom
agent/v5-maven-external-mgmt

Conversation

@mikolalysenko

@mikolalysenko Mikola Lysenko (mikolalysenko) commented Oct 9, 2026 •

Copy link
Copy Markdown
Collaborator

LLM Description written by Claude Code:claude-opus-5-5

Fixes #488

Summary

vendor on a Maven reactor no longer silently downgrades a build whose version of the patched artifact comes from an imported BOM or from a parent outside the checkout. It also no longer freezes the pin after the BOM is bumped. Before pinning a local root, the planner now finds out what that management says. When the managed version is not the patch's base version, or cannot be determined, the root is left unpinned and a specific warning says why.

Root cause

vendor/jvm/maven_reactor.rs pinned every wired local root unless a declaration inside the checkout conflicted (rewrite_declarations → conflicting_literal_version). A local <dependencyManagement> pin beats both of these:

  • imported BOMs (<scope>import</scope>);
  • parents resolved from a repository (<relativePath/>, a corporate or Spring Boot parent).

The planner never read either one.

Fix

  • maven_reactor::plan_with_external and external_poms_needed. For each wired root not already unpinned, Reactor::external_management checks every pom that resolves through that root and declares no version for g:a in the checkout. It looks for:

    • an explicit management entry or inherited <dependencies> literal in the external parent chain. Properties follow Maven's order: the reactor's own values override the parent's, which covers the ${ct.version} sibling from the issue comment;
    • otherwise, the first imported BOM that manages g:a: the reactor chain's imports, then the external parents'. Nested imports and BOM parents are followed in the BOM's own context.

    The outcome for the root:

    • A different version, or an inherited literal (no pin can override one), leaves the root unpinned with vendor_jvm_degraded reason: conflicting_managed_version.
    • A POM in no cache that can't be fetched, or an undefined property, leaves it unpinned with reason: management_unresolved. The remedy names mvn -q dependency:resolve.
    • An existing pin on a root that is now unpinned is removed. That fixes the "upgrade blocked" re-run, which used to report already_vendored.
  • Fetching. vendor_maven_jvm (maven_repo.rs) loops over external_poms_needed and gets each POM with acquire_upstream_metadata: local caches first, then the registry when online, verified the same way as the Gradle parent/BOM metadata.

  • vendor --check (apply::check_entry) reads the same POMs from the local repository only, with no network. When some are missing it accepts either decision vendor could have made. A BOM bump it can read shows up as drift.

  • Probe and redownload paths (maven_repo.rs probe, redownload.rs) only use the plan's tree writes, so they keep plan_with_config (no external weighing).

  • Docs: docs/design/maven-vendoring.md and CLI_CONTRACT.md (vendor_jvm_degraded reasons).

Per-issue checklist

  • Vendored Maven reactor pins over an imported BOM or external parent, so a build that uses 1.11.0 is silently downgraded to 1.10.0-socket.* and a later BOM bump never takes effect #488. The BOM import at 1.11.0 is no longer a downgrade: imported_bom_managing_another_version_leaves_the_root_unpinned. The test also asserts that the pre-fix plan (plan, no external weighing) pins. That assertion is the red evidence.
  • External parent at 1.11.0: an_external_parent_managing_another_version_leaves_the_root_unpinned.
  • ${ct.version} overridden locally: a_local_property_overriding_an_external_parents_version_is_honored.
  • Upgrade blocked after a BOM bump: a_bom_bump_after_vendoring_removes_the_pin. vendor --check covers it too: check_weighs_an_imported_bom_from_the_local_repository.
  • Controls:
    • BOM at the base version: imported_bom_at_the_base_version_is_pinned.
    • BOM that doesn't manage the GA: a_bom_that_does_not_manage_the_artifact_keeps_the_pin.
    • Nested import: a_nested_bom_import_is_followed.
    • Unavailable metadata: unavailable_external_management_leaves_the_root_unpinned.

Commands run

  • cargo test -p socket-patch-core --no-fail-fast: green.
  • cargo test -p socket-patch-cli --no-fail-fast: green except two e2e_vendor_cargo_build old-toolchain cases. They depend on locally installed old cargo toolchains, are unrelated, and fail the same way on the other branches.
  • cargo test -p socket-patch-cli --test e2e_vendor_maven_build -- --ignored (real Maven 3.9.16): green.
  • cargo test -p socket-patch-cli --test e2e_vendor_jvm_build -- --ignored maven (maven_reactor capstone): green.
  • cargo clippy --workspace --all-features -- -D warnings: clean.
  • cargo fmt --check: clean for the changed files. Local rustfmt flags only the untouched upstream/mod.rs:917.

🤖 Generated with Claude Code


Note

Medium Risk
Changes Maven vendoring and pin/check behavior for reactors with external BOMs and parents; incorrect management analysis could leave builds unpinned or accept stale pins, but scope is limited to Maven reactor/mixed shapes with extensive new tests.

Overview
Fixes #488 by changing Maven reactor vendoring so a local root is not pinned when an imported BOM or external parent (outside the checkout) manages the patched artifact at a version other than the patch base—or when that management cannot be resolved.

The planner gains plan_with_external / external_poms_needed, which walk external parent chains and BOM imports (including nested imports), interpolate properties in Maven order, and emit vendor_jvm_degraded with conflicting_managed_version or management_unresolved while leaving the affected root unpinned; re-runs after a BOM bump drop an obsolete pin instead of reporting in sync. vendor fetches those external POMs from local caches or the registry via external_maven_poms; vendor --check replans using only the local Maven repository and tolerates missing metadata when either pin/no-pin decision would match what vendor wrote. Docs and CLI_CONTRACT.md document the new degraded reasons.

Reviewed by Cursor Bugbot for commit 9858181. Configure here.


Generated by Claude Code

The vendored Maven reactor pinned every wired local root, and a local
dependencyManagement pin beats both imported BOMs and parents
resolved outside the checkout. A project whose corporate BOM or
Spring Boot parent managed commons-text at 1.11.0 was silently
moved to 1.10.0-socket.*, vex attested it, and a later BOM bump never
took effect because re-runs reported the pin in sync.

Before pinning a root the planner now reads the external parent
chain and every import-scope BOM (local caches or the registry, like
other upstream metadata), interpolating properties in Maven's order.
Management or an inherited literal at another version leaves the
root unpinned with conflicting_managed_version; metadata that cannot
be read leaves it unpinned with management_unresolved. A re-run
after a BOM bump drops the pin. vendor --check reads the metadata
from the local repository only and accepts either decision when some
of it is missing.

Fixes #488.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@mikolalysenko
Mikola Lysenko (mikolalysenko) marked this pull request as ready for review October 9, 2026 18:23
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

BugBot review

@cursor cursor 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.

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Early match skips later module versions
    • Changed external_management to continue checking all modules via labeled loop instead of returning Ok(None) on first base version match, preventing incorrect downgrades when later modules have different effective versions.

Create PR

Or push these changes by commenting:

@cursor push ac82cd3ea8
Preview (ac82cd3ea8)
diff --git a/crates/socket-patch-core/src/patch/redirect/upstream/mod.rs b/crates/socket-patch-core/src/patch/redirect/upstream/mod.rs
--- a/crates/socket-patch-core/src/patch/redirect/upstream/mod.rs
+++ b/crates/socket-patch-core/src/patch/redirect/upstream/mod.rs
@@ -917,7 +917,10 @@
     fn bun_lock_remedies_name_the_forced_reinstall() {
         for file in ["bun.lockb", "bun.lock", "packages/app/bun.lockb"] {
             let remedy = checkout_remedy(&[file.to_string()]);
-            assert!(remedy.contains(&format!("`git checkout -- {file}`")), "{remedy}");
+            assert!(
+                remedy.contains(&format!("`git checkout -- {file}`")),
+                "{remedy}"
+            );
             assert!(remedy.ends_with(
                 ", then run `bun install --force` (a plain `bun install` keeps the patched copy)"
             ), "{remedy}");

diff --git a/crates/socket-patch-core/src/vendor/jvm/maven_reactor.rs b/crates/socket-patch-core/src/vendor/jvm/maven_reactor.rs
--- a/crates/socket-patch-core/src/vendor/jvm/maven_reactor.rs
+++ b/crates/socket-patch-core/src/vendor/jvm/maven_reactor.rs
@@ -782,7 +782,7 @@
     ) -> Result<Option<ManagedConflict>, Missing> {
         let (g, a) = (patch.group_id, patch.artifact_id);
         let ext_chain = self.external_chain(root, lookup)?;
-        for rel in self.scope.iter().filter(|rel| self.local_root(rel) == root) {
+        'module_loop: for rel in self.scope.iter().filter(|rel| self.local_root(rel) == root) {
             let locally_versioned = self.chain(rel).any(|p| {
                 let doc = &self.poms[p].doc;
                 doc.keyed_declarations(g, a)
@@ -822,7 +822,7 @@
                     // overridden by management, so a pin cannot reach it
                     // even at the base version.
                     if managed && is_base_like(&v, patch.version) {
-                        return Ok(None);
+                        continue 'module_loop;
                     }
                     let how = if managed {
                         "managed by"
@@ -866,7 +866,7 @@
                 let mut seen = BTreeSet::new();
                 if let Some(v) = bom_manages(bom, g, a, lookup, &mut seen, 0)? {
                     if is_base_like(&v, patch.version) {
-                        return Ok(None);
+                        continue 'module_loop;
                     }
                     let (bg, ba, bv) = bom;
                     return Ok(Some(ManagedConflict {

You can send follow-ups to the cloud agent here.

Comment thread crates/socket-patch-core/src/vendor/jvm/maven_reactor.rs
Comment thread crates/socket-patch-core/src/vendor/jvm/maven_reactor.rs Outdated
Comment thread crates/socket-patch-core/src/vendor/jvm/maven_reactor.rs Outdated
external_management returned Ok(None) as soon as one module's inherited
parent or imported BOM resolved the base version, so a later module that
imports its own BOM (or sets its own property) at another version was
never checked. The root pin then overrode that module's management and
reintroduced the #488 downgrade. Continue to the next module instead and
only clear the root once every module agrees.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KoEk4Y9wedBsjtt9kTUPVY
imports_of read only the project's top-level dependencyManagement, so a
<scope>import</scope> BOM inside a profile (activeByDefault included) was
never weighed and the root pin could override the version Maven actually
uses. Read every profile's management too, after the project's, as Maven
appends it. Activation is not evaluated, so a profile BOM that might apply
keeps the root unpinned rather than risking a silent downgrade.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KoEk4Y9wedBsjtt9kTUPVY
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

bugbot run


Generated by Claude Code

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Burn-down agent: disabled auto-merge before pushing fixes for the two open Bugbot findings (9858181: every module is now weighed before skipping the root pin, and profile BOM imports count). The final reviewer should re-review this head and re-arm. Awaiting CI and Bugbot.


Generated by Claude Code

@cursor cursor 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.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 9858181. Configure here.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants