Skip to content

fix(manifest): resolve Maven dependencies through Maven's own resolver - #1519

Merged
Jeppe Fredsgaard Blaabjerg (jfblaa) merged 3 commits into
v1.xfrom
fix/maven-resolve-via-maven-resolver
Aug 28, 2026
Merged

fix(manifest): resolve Maven dependencies through Maven's own resolver#1519
Jeppe Fredsgaard Blaabjerg (jfblaa) merged 3 commits into
v1.xfrom
fix/maven-resolve-via-maven-resolver

Conversation

@jfblaa

@jfblaa Jeppe Fredsgaard Blaabjerg (jfblaa) commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Summary

The Maven facts extension collected the dependency graph with maven-dependency-tree, then re-resolved each artifact itself, passing the root module's repository list for every node in the tree. Maven resolves each node against the repositories that node's own descriptor lineage contributes, so a dependency served only by a repository declared in one module of a reactor could not be materialized for any other module that reached it.

Aether's local repository also tracks which repository each cached file came from, so not even an already-downloaded copy counted as available: the resolver went to the network, missed, and --with-files aborted the scan on a dependency mvn compile resolves without trouble.

Resolution now goes through ProjectDependenciesResolver — the component LifecycleDependencyResolver holds, i.e. the one Maven's own lifecycle uses to build every project's classpath.

What that changes

  • Per-node repositories, dependency management, scope derivation and reactor substitution are Maven's, not a re-implementation. The previous graph wasn't Maven's either: DefaultDependencyCollectorBuilder overrode the session's setDependencySelector / setDependencyGraphTransformer with the tree tool's own.
  • Failures come from Maven's own per-dependency errors (getCollectionErrors, getUnresolvedDependencies + getResolutionErrors, and the partial result on DependencyResolutionException), each carrying Maven's message and the dependency's own scope. Fail-closed is kept: an unresolvable dependency still aborts, because a silently missing jar leaves reachability blind to what that artifact contains and under-reports.
  • Which artifacts get fetched is a DependencyFilter, carrying over the old guard. A plain --facts run rejects everything, so it collects without downloading; a reactor sibling's jar is never requested at the validate phase the CLI runs, where nothing has been packaged and nothing need be installed. A filtered-out node produces no ArtifactResult, so it lands in neither resolvedDependencies nor unresolvedDependencies and can never be mistaken for a failure.
  • Records are unchanged: coordinate ids keep Maven's type rather than Aether's file extension, and versions use the base version so a resolved remote snapshot can't leak a timestamped coordinate no manifest names. Conflict-losing nodes, which a verbose collect (mvn -X) leaves in the graph, are skipped.
  • maven-dependency-tree is dropped; the extension jar goes 67K → 26K.

Tests

Two new compat fixtures, both verified to fail on the pre-change jar and pass after:

  • repo-inheritanceliba declares the only repository serving demo.scoped:widget; libb reaches it transitively. Pre-change it fails with Could not find artifact … in central. A second phase mirrors central to a widget-less repo and asserts the failure record is emitted, so fail-closed itself is guarded.
  • duplicate-failure — several modules failing on the same dependency. Their identical failures collapse in the value-equality accumulator shared across the reactor, which a naive "did this module report anything?" size check reads as silence. Runs offline, so it needs no stub repo and never touches the network.

The Maven matrix now spans 3.2.5 → 4.0.0-rc-6, the range the extension claims to support: 30 assertions (5 versions × 6) all pass. Records verified byte-identical to the previous jar on two real multi-module projects; a plain --facts run against a cold local repository fetches poms only, zero jars.

Second commit

docs(changelog) repairs a pre-existing problem the merge surfaced.

1.1.161 never reached npm (1.1.160 is still latest), so nobody can install what that section described. Its tag and GitHub release are immutable and stay as they are — GitHub generates those notes from the PR list, independently of this file — while CHANGELOG.md documents the published package. The next bump derives 1.1.162 from the reachable tag regardless, so this touches no release tooling.

How the section got there: #1516 wrote its heading as ## [Unreleased] - 2026-08-27, and unreleasedRange() in scripts/release/changelog.mts locates the block by comparing the trimmed, lowercased heading for equality with ## [unreleased]. The trailing date made it miss, so the release found nothing accrued, fell back to commit-derived notes, and inserted its heading above the block it couldn't see — stranding [Unreleased] below a released version.

The commit drops the burned 1.1.161 section, returns the Coana 15.10.25 note to ## [Unreleased] for the next release that actually ships, restores ## [Unreleased] at the top without a date, and leaves out "stop the coana bump from hand-writing versions" — a release-workflow change with nothing for a package user to act on, which only appeared because the commit-derived fallback ran.

The locator's intolerance of a trailing date is left for its own change.

Third commit

docs(bump-coana) pins the recreated [Unreleased] heading to exactly that spelling, with the reason attached.

Tracing #1516: the bump wrote ## [1.1.161](…) - 2026-08-27 — a version heading, which the skill already forbids — and the follow-up correction changed the version to Unreleased but kept the date. The skill said what not to write, not what the heading must look like once recreated, so the partial fix landed on a form the release cannot see.

Note the limit: the bump PR was opened by app/socket-pr-bot, which lives outside this repo, so this commit only governs the in-repo procedure and whoever corrects a malformed heading by hand. The bot still writes a ## [<version>] heading and a package.json version — both of which #1515 forbade — and that needs fixing where the bot lives.

The Maven facts extension collected the dependency graph with
maven-dependency-tree, then re-resolved each artifact itself, passing the
root module's repository list for every node in the tree. Maven resolves
each node against the repositories that node's own descriptor lineage
contributes, so a dependency served only by a repository declared in one
module of a reactor could not be materialized for any other module that
reached it. Aether's local repository also records which repository each
cached file came from, so not even an already-downloaded copy counted as
available, and --with-files aborted the scan on a dependency the build
itself resolves without trouble.

Resolution now goes through ProjectDependenciesResolver, the component
Maven's own lifecycle uses to build a project's classpath. Per-node
repositories, dependency management, scope derivation and reactor
substitution are Maven's rather than a re-implementation of them, and
failures are reported from Maven's own per-dependency errors. Which
artifacts get fetched is expressed as a DependencyFilter, so a plain
--facts run collects without downloading anything and a reactor sibling's
jar is never requested at the validate phase the CLI runs, where nothing
has been packaged. A filtered-out node yields no ArtifactResult, so it can
never be mistaken for a resolution failure.

Coordinate ids keep Maven's `type` rather than Aether's file extension, and
versions use the base version so a resolved remote snapshot cannot leak a
timestamped coordinate no manifest names. Conflict-losing nodes, which a
verbose collect leaves in the graph, are skipped. Records are byte-identical
to the previous output on the projects exercised here.

Drops the bundled maven-dependency-tree; the extension jar goes 67K -> 26K.

Adds two compat fixtures. repo-inheritance covers a dependency reachable
only through a repository a sibling module declares, plus the fail-closed
half: an unresolvable dependency must still be reported, since a silently
missing jar leaves reachability blind to what it contains.
duplicate-failure covers several modules failing on the same dependency,
whose identical failures collapse in the value-equality accumulator shared
across the reactor. The Maven matrix now spans 3.2.5 through 4.0.0-rc-6,
the range the extension claims to support.
…tion

1.1.161 never reached npm — 1.1.160 is still latest there — so no user can
install what that section describes. Its tag and GitHub release are
immutable and stay; the changelog documents the published package, and
GitHub generates its own release notes from the PR list, so the two need
not agree. The next bump derives 1.1.162 from the reachable tag either way,
so nothing here affects the release tooling.

How the section got there: #1516 wrote its heading as
`## [Unreleased] - 2026-08-27`, and unreleasedRange() in
scripts/release/changelog.mts locates the block by comparing the trimmed,
lowercased heading for equality with `## [unreleased]`. The trailing date
made it miss, so the release found nothing accrued, fell back to the
commit-derived section, and inserted its own heading above the block it
could not see — stranding `[Unreleased]` below a released version.

- Drops the 1.1.161 section and returns the Coana 15.10.25 note to
  `## [Unreleased]`, to be promoted by the next release that ships.
- Puts `## [Unreleased]` back at the top, without a date.
- Leaves out "stop the coana bump from hand-writing versions": a
  release-workflow change with nothing for a user of the package to act
  on, which only appeared because the commit-derived fallback ran.
- Files the Maven resolver fix under `[Unreleased]`.

The locator's intolerance of a trailing date is left alone here; it wants
its own change.
@jfblaa
Jeppe Fredsgaard Blaabjerg (jfblaa) force-pushed the fix/maven-resolve-via-maven-resolver branch from 43769b8 to 0b44351 Compare August 28, 2026 10:08
The rule against writing a `## [<version>]` heading did not say what the
heading may look like when it is recreated, and #1516 shows why that
matters. The bump wrote `## [1.1.161](...) - 2026-08-27`; the follow-up
correction changed the version to `Unreleased` but kept the date, leaving
`## [Unreleased] - 2026-08-27`.

unreleasedRange() in scripts/release/changelog.mts matches that heading for
equality — case-insensitively, but otherwise exactly — so the dated form is
invisible to it. The release promoted nothing, fell back to the section
derived from the commits in range, and inserted its own heading above the
block it could not see. The note sat below a released version where no
release would pick it up, and the version it named never reached npm.

Pins the recreated heading to exactly `## [Unreleased]` and says why a date
breaks promotion, so the next correction of a malformed heading lands on
the form the release can actually find.
@jfblaa
Jeppe Fredsgaard Blaabjerg (jfblaa) merged commit 7b9ca74 into v1.x Aug 28, 2026
10 checks passed
@jfblaa
Jeppe Fredsgaard Blaabjerg (jfblaa) deleted the fix/maven-resolve-via-maven-resolver branch August 28, 2026 10:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants