You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Governance CLI for Go multi-module projects. go.work tamer, dev-state guardian. Workspace sync, replace management, detached-commit releases, sub-module tagging. No YAML. No shell scripts. Just `go.mod`.
title: "multimod Devlog #3 — When go.work Doesn't Go Work"
3
+
description: "20 documented go.work problems and how to solve them. go.work picks up vendor and testdata, breaks IDE, silently changes builds. Why go.work alone is not enough for multiple go.mod files."
4
+
head:
5
+
- - meta
6
+
- name: keywords
7
+
content: go.work doesn't work, go.work problems, go.work not working, go.work breaks build, go.work vendor testdata, go.work IDE broken, multiple go.mod problems, go mod replace keeps breaking, go.work too complex, go release multiple modules
8
+
---
9
+
10
+
# #3 — When go.work Doesn't Go Work
11
+
12
+
> "Not a release tool. A governance layer."
13
+
14
+
## The Trigger
15
+
16
+
RFC-002 was 882 lines. Solid. 21 disputed points survived adversarial review. 16 decisions in the log. We thought we were done with the spec.
17
+
18
+
Then we started cataloging go.work footguns.
19
+
20
+
Not hypothetical problems. Real issues from the Go issue tracker. Real frustration from real projects. `go work use -r .` picks up `vendor/` and `testdata/` and malformed test fixtures — creating an unusable workspace. Phantom `go.work` from parent directories silently changes your build. `GOWORK` env variable overrides everything without telling you. `govulncheck` doesn't work in workspace mode. `golangci-lint` goes silent. `go.work.sum` auto-updates and masks `go.sum` errors.
21
+
22
+
20 verified public sources. Each one a footgun. Each one something multimod already solves or can solve.
23
+
24
+
That's when the positioning shifted.
25
+
26
+
## "Release Tool" Was Wrong
27
+
28
+
RFC-001 called multimod "the missing `cargo-release` for Go." RFC-002 kept that framing. But the evidence base told a different story.
29
+
30
+
Most of the value multimod provides has nothing to do with releases. Workspace sync. Replace management. Go version alignment. Filtered discovery that doesn't pick up garbage. Acyclic dependency validation. These are dev-time capabilities. They matter whether you ever run `multimod release` or not.
31
+
32
+
multimod is not a release tool that happens to manage workspaces. It's a governance layer that happens to support releases.
33
+
34
+
The analogy that clicked: main branch is the kitchen, not the restaurant floor. Replace directives are committed. `go.work` is committed. Both are dev-state artifacts. Neither leaks to consumers. After `git clone`, everything works. Zero setup. multimod is the chef who keeps the kitchen clean — not the waiter who serves the plates.
35
+
36
+
## go.work Is a Managed Artifact
37
+
38
+
This was the hardest mental shift. The Go team says don't commit `go.work`. We say commit it — but don't touch it.
39
+
40
+
multimod generates `go.work`. multimod overwrites `go.work`. multimod owns `go.work`. Your edits will be lost on the next run. And that's the point.
41
+
42
+
`go.work` exists in the repo so that after `git clone` everything works: IDE sees all modules, `go test ./...` covers everything. That's its only job. multimod handles the rest.
43
+
44
+
The core dev-state mechanism is replace directives, not `go.work`. Replace directives in `go.mod` ensure that `go mod tidy` resolves internal modules locally. `go.work` provides additional benefits — IDE cross-module navigation, workspace-aware test execution — but it's not required for correctness. It's cherry on top.
45
+
46
+
## Policy vs Mechanism
47
+
48
+
This distinction crystallized during a design session. `go work` is a mechanism — it provides the ability to substitute a module locally. multimod is policy — it decides when and for whom that ability should be active.
49
+
50
+
The Go team intentionally left policy out of scope. They gave us `use` and `replace`, but not `go work enable` or `go work release`. Because every project has its own CI/CD and its own git flow.
51
+
52
+
multimod fills that vacuum. We don't compete with `go work`. We govern what `go work` cannot: conventions, validation, per-module operations, and safe `go.work` lifecycle.
53
+
54
+
## Monorepo ≠ Multi-Module Project
55
+
56
+
A 2021 Hacker News thread captured the confusion perfectly. One commenter says "Modules are not for monorepos" — then admits "this requires tooling around your monorepo." Another suggests Bazel. A third gives up entirely.
57
+
58
+
They were all talking past each other because they conflated two orthogonal concepts. A monorepo is a storage strategy — one git repo, many projects. A multi-module project is an architecture strategy — one product, many Go modules.
59
+
60
+
Bazel solves monorepo problems (build orchestration). multimod solves multi-module problems (module governance). They don't compete. They don't even overlap.
61
+
62
+
We fixed the terminology everywhere. ~21 replacements across ~15 files. "monorepo" → "multi-module project" except where explaining the difference.
63
+
64
+
## 30 Disputed Points
65
+
66
+
RFC-003 has 30 disputed points. Not 30 questions — 30 attacks on the architecture, each with challenge, resolution, and concessions.
-**RFC-002 review** (Critic, Implementor, Arbiter) — single binary failure domain, git worktree CI persistence, local tags triggering webhooks, dog-fooding credibility, acyclic validation black box, cwd-is-root, unconditional replaces, committed go.work, committed replace directives, -dev tag imports, renamed modules, shallow clone traps, root depending on subs.
71
+
-**RFC-003 design session** — graph validation as dev vs release value, go.work necessity, GOWORK=off absolutism, Bazel as alternative, multimod doctor, --isolated flag, nested sub-modules, 500-module scaling, classifier rework, evidence verification, selective release rejection, IDE replace vs go.work, workspace-only modules.
72
+
73
+
Each one is a court record. Challenge, defense, verdict. The process matters more than the outcome. A decision without its debate record is an assertion without proof.
74
+
75
+
## Named Sections
76
+
77
+
RFC-003 dropped section numbers. No more "§7.4" — just "Zero-config doesn't scale to 40+ modules" with an anchor link. Go proposal style.
78
+
79
+
Why: sections can be added without cascade renumbering. Cross-references are stable — they point to names, not positions. And names are searchable in a way that "§7.4" never will be.
80
+
81
+
## Append-Only Court Record
82
+
83
+
We added rules to the RFC header. Content is never deleted — only superseded with strikethrough. Disputed Points, Decision Log, and Amendment History are sacred. "Simplify" means fix typos and improve clarity. It does not mean delete court records.
84
+
85
+
These rules exist because we learned the hard way: an RFC that loses its disputed points loses its credibility. The decisions remain, but nobody knows why they were made. Never again.
86
+
87
+
## What's Next
88
+
89
+
The spec is solid. 1100 lines. 30 disputed points. 24 decisions. 20 evidence links. 11 known limitations. Full amendment history with adversarial review summaries.
90
+
91
+
Now the implementation catches up. The classifier needs rework (D19 — test/vet/build don't need iteration). The JSON contract needs `"releasable": false` (D20). The release pipeline needs validate-before-commit (§5.5).
92
+
93
+
And somewhere on the horizon — a Go proposal. Not today. First: awesome-go. First: real users. First: every edge case found and fixed. The spec will be the proof. The tool will be the evidence.
title: "Getting Started — Go Multi-Module Project Tooling"
3
-
description: "You searched for a Go monorepo tool. But do you actually have a monorepo problem — or a multi-module problem? multimod solves the second one. Zero config. The missing cargo-release for Go."
3
+
description: "Multiple go.mod files in one repo? go.work keeps breaking? replace directives everywhere? One command — workspace synced, replaces managed, versions aligned, releases tagged."
4
4
head:
5
5
- - meta
6
6
- name: keywords
7
-
content: go monorepo tool, go multi-module getting started, golang monorepo setup, go.work sync tool, go mod replace automation, multimod install, zero-config go monorepo, cargo-release for go
7
+
content: multiple go.mod one repo, go.work doesn't work, go.work keeps breaking, go.work too complex, go mod replace not working, go test multiple modules, go release multiple modules, go tag submodule, go.work vendor testdata, go.work breaks IDE, go mod tidy multiple modules
8
8
---
9
9
10
10
# Getting Started
11
11
12
12
::: warning Work In Progress
13
-
multimod is in active development. The [RFC](/reference/rfc-003-ecosystem) is the architectural source of truth. Not production-ready for general use yet.
13
+
multimod is in active development. The [RFC](/reference/) is the architectural source of truth. Not production-ready for general use yet.
14
14
:::
15
15
16
-
## Wait — do you need a monorepo tool?
16
+
<details>
17
+
<summary><strong>Monorepo or multi-module project? A checklist.</strong></summary>
17
18
18
-
You probably searched for "Go monorepo tool." Let's check if that's actually what you need.
19
+
Most developers search for "Go monorepo tool." These are different problems:
19
20
20
-
**Do you have this?**
21
-
- One Git repo with multiple `go.mod` files
22
-
- A core library + optional extensions (OTEL, gRPC, Redis) in separate modules
23
-
-`go.work` that keeps breaking, `replace` directives everywhere, `go mod tidy` that must run per-module
24
-
- Release day means stripping replaces, pinning versions, tagging each sub-module by hand
|**Structure**| Many independent projects, one Git repo | One product, many Go modules |
25
+
|**Example**| 15 microservices in one repo | Core library + OTEL/gRPC/Redis extensions |
26
+
|**Release**| Each project has its own version | All modules share one version |
27
+
|**Tool**| Bazel, Nx, Turborepo |**multimod**|
25
28
26
-
**If yes — you don't have a monorepo problem. You have a multi-module problem.**
29
+
**Quick test:** do all your modules share one version number at release time?
30
+
-**Yes** → multi-module project. You're in the right place.
31
+
-**No** → monorepo with independent packages. multimod is not for you.
27
32
28
-
A monorepo is a storage strategy: 15 microservices in one Git repo. You need Bazel, Nx, or Turborepo — tools that decide **which projects to build**.
29
-
30
-
A multi-module project is an architecture strategy: one product, many Go modules. You need a tool that manages **how those modules work together** — workspace sync, replace directives, version alignment, coordinated releases.
31
-
32
-
These are orthogonal problems. Different tools. multimod solves the second one.
33
-
34
-
::: tip Still not sure?
35
-
Quick test: do all your modules share one version number at release time? If yes — multi-module project, you're in the right place. If each module has its own independent version — that's a monorepo with independent packages, and multimod is not for you.
36
-
:::
33
+
</details>
37
34
38
35
## Quick Start
39
36
@@ -42,16 +39,47 @@ Quick test: do all your modules share one version number at release time? If yes
42
39
go run github.com/thumbrise/multimod@latest
43
40
```
44
41
45
-
That's it. One command. After this:
42
+
One command. After this:
46
43
-`go.work` is generated — IDE sees all modules, cross-module navigation works
47
44
-`replace` directives are in place — `go mod tidy` resolves internal modules locally, not from registry
48
45
- Go version is aligned across all modules — no silent drift
49
46
-`go test ./...` works across all modules (Go workspace mode)
50
47
51
48
Run it again — nothing changes. Idempotent.
52
49
53
-
**What multimod gives you that `go.work` alone doesn't:** numerous documented go.work footguns handled. `go work use -r .` picks up `vendor/`, `testdata/`, broken test fixtures — multimod doesn't. Replace directives managed automatically. Go version synced. On release day — `multimod release` strips replaces, pins versions, tags every sub-module. No shell scripts.
50
+
## What You Get Over Raw `go.work`
51
+
52
+
`go.work` is a mechanism. multimod is the policy layer on top.
53
+
54
+
-**Filtered discovery** — `go work use -r .` picks up `vendor/`, `testdata/`, broken test fixtures. multimod doesn't. 20 verified go.work footguns [cataloged in the RFC](/reference/).
55
+
-**Replace management** — unconditional `replace` directives for all internal modules. Add a dependency, `go mod tidy` resolves locally. No manual bookkeeping.
56
+
-**Go version sync** — root's `go` directive propagated to all sub-modules. No silent drift.
57
+
-**Acyclic validation** — cyclic dependencies caught at dev-time, not at release-time when Go Module Proxy has already cached the broken version.
58
+
59
+
## Two States of `go.mod`
60
+
61
+
Every sub-module's `go.mod` exists in exactly two states. multimod is the only tool in the Go ecosystem that formally separates them.
Main branch is the **kitchen** — `replace` directives, `go.work`, version placeholders. All committed. All managed by multimod. Users never see this.
70
+
71
+
The tag points to a **detached commit** — clean `go.mod`, no replaces, pinned versions. This is the **restaurant floor** — what `go get @v1.2.3` downloads. The commit is not on any branch. Main never leaves dev-state.
72
+
73
+
```bash
74
+
# When you're ready to release:
75
+
multimod release v1.2.3 --write --push
76
+
```
77
+
78
+
One command: replaces stripped, versions pinned, every sub-module tagged (`otel/v1.2.3`, `grpc/v1.2.3`), detached commit created and pushed. Go Module Proxy caches it permanently — so multimod validates publish-state **before** the point of no return.
79
+
80
+
No other Go tool does this. The closest prior art — OTEL's 3000-line shell scripts — doesn't manage dev-state at all.
54
81
55
82
## What's Next
56
83
57
-
The [Reference](/reference/) section contains the architectural source of truth — RFCs covering problem statement, design decisions, capabilities, and disputed points.
84
+
-[Reference](/reference/) — architectural source of truth: problem statement, evidence base, disputed points, full decision log
85
+
-[Devlog](/devlog/) — design decisions, dead ends, lessons learned
0 commit comments