Skip to content

Catch and fix nested modules that require unpublishable versions - #4

Merged
juicycleff merged 3 commits into
mainfrom
fix/nested-module-requires
Aug 27, 2026
Merged

Catch and fix nested modules that require unpublishable versions#4
juicycleff merged 3 commits into
mainfrom
fix/nested-module-requires

Conversation

@juicycleff

Copy link
Copy Markdown
Contributor

The bug

A nested go.mod usually carries a replace so the working tree builds against
itself. That replace is ignored the moment the module becomes somebody else's
dependency, so the require line is what a consumer actually resolves, and
several repos here require the root at v0.0.0, which is not a version anyone
can fetch:

go get github.com/xraph/grove/drivers/pgdriver@v1.6.2
reading github.com/xraph/grove/go.mod at revision v0.0.0: unknown revision v0.0.0

It works today only for people who already require the root at a real version,
because MVS then picks that one instead, which means the failure is invisible to
everyone inside the org and waiting for the first person outside it who reaches
for a driver before reaching for grove. No local build notices either. The
replace covers it.

Forge has the same bug wearing a different version string. It uses
v0.0.0-00010101000000-000000000000, the zero pseudo-version Go writes when a
module is reachable solely through a replace, and because that string is valid
semver it gets past a format check while still resolving for nobody.

Counted across the Go repos: 155 requires. nexus 90, grove 23, forge 19,
herald 11, trove 6, farp 5, dtl 1.

What this adds

go-ci gains a nested-modules job. It reads every intra-repo require and
fails on v0.0.0, on the zero pseudo-version, and on anything that is not
semver. A version that simply is not published yet gets reported and allowed,
because release rewrites those before it tags. The job is a no-op in a
single-module repo, so every caller picks it up without changing anything.

go-release gains the other half. It discovers nested modules, builds and tests
each one, pins their intra-repo requires to the version being cut, tags each as
<dir>/<version>, and then stands up a scratch module and runs go get on
every one of them through the public proxy, which is the real gate here because
it fails the way a consumer would rather than the way CI does.

The pin uses go mod edit. A regex missed the single-line require x v1.2.3
form entirely, and grove has one of those sitting in kv/go.mod, so the first
version of this quietly left it broken while reporting success. go mod edit
knows both forms, never touches a replace, and rejects a version whose major
does not match the path suffix.

The pin commit is never pushed to a branch. It is reachable only through the
nested tags, so your branch keeps pointing nested modules at the last release
and keeps building against the working tree.

Two things worth knowing

Underscore-prefixed directories are skipped. The go tool ignores them, and nexus
keeps three example modules under _examples that require providers nobody ever
tagged.

The submodules input description said nested go.mod repos were out of scope,
which is why every affected repo hand-rolled its own release workflow, and why
each of those hand-rolled workflows reinvented the same mistake independently.
Corrected here.

Rollout

All 29 callers pin @v1, so nothing changes until that tag moves. The repos
listed above are already fixed and pushed, so they pass on day one.

Verified against the real repos: grove and forge fail before their fixes and
pass after, cortex passes, single-module repos are unaffected. actionlint is
clean.

…sions

A nested go.mod usually carries a replace so the working tree builds
against itself. That replace is ignored the moment the module becomes
somebody else's dependency, so the require line is what a consumer
actually resolves. Several repos require the root at v0.0.0, which is not
a version anyone can fetch:

  go get github.com/xraph/grove/drivers/pgdriver@v1.6.2
  reading github.com/xraph/grove/go.mod at revision v0.0.0:
  unknown revision v0.0.0

It works today only for people who already require the root at a real
version, so MVS picks that instead. Anyone reaching for the nested module
first hits a wall, and no local build ever notices, because the replace
hides it. Across the Go repos here that is 113 requires: nexus 85, grove
16, farp 5, trove 5, dtl 1, herald 1.

go-ci gains a nested-modules job that reads every intra-repo require and
fails on v0.0.0 or a version that is not semver. A version that is simply
not published yet is reported and allowed, since release rewrites these
before it tags. The job is a no-op in a single-module repo, so every
caller picks it up without changing anything.

go-release gains the other half: it discovers nested modules, builds and
tests each, pins their intra-repo requires to the version being cut, tags
each as <dir>/<version>, and then resolves every one of them through the
public proxy. That last step is the real gate, because it fails the way a
consumer would rather than the way CI does.

The pin uses go mod edit, not a regex. It handles the single-line require
form as well as the block, never touches a replace, and rejects a version
whose major does not match the path suffix. A regex accepts all three.

The pin commit is never pushed to a branch. It is reachable only through
the nested tags, so the branch keeps pointing nested modules at the last
release and keeps building against the working tree.

Also corrects the submodules input description, which told callers nested
go.mod repos were out of scope.
v0.0.0-00010101000000-000000000000 is what Go writes when a module is only
ever reached through a replace. It is valid semver, so a format check waves
it through, and it resolves for nobody. forge carries 19 of them and the
check called it clean.

Same failure as a bare v0.0.0, just wearing a shape that looks deliberate.
The go tool ignores a directory whose name starts with an underscore, so a
module under _examples is not surface anyone consumes. nexus keeps three
there that require providers which were never tagged, and flagging those
asks for a fix that would mean publishing example-only modules.

Discovery in release skips them for the same reason: they should not be
tagged.
@juicycleff
juicycleff merged commit 6695b98 into main Aug 27, 2026
42 checks passed
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