skip to content

In a repo whose root is one module and whose ./cli directory is another, how do you tag the cli module?

level: middleimportance: nice to knowfreq 30%

answer

  1. one repo, two go.mod files
  2. the tag namespace has to be split
  3. directory prefix, relative to repo root
  4. cli/v1.2.0, not v1.2.0
  5. root module tags stay unprefixed

basics

~10 s

Prefix the Git tag with the module's directory path relative to the repository root: cli/v1.2.0 publishes the module whose go.mod sits in ./cli. A plain v1.2.0 tag publishes only the root module.

solid answer

~40 s

One repository can hold several modules, each with its own `go.mod`, and the tag namespace is split by directory prefix. The module in `./cli` — whose path is `github.com/acme/tools/cli` — is released by pushing a tag named `cli/v1.2.0`; the root module is released by `v0.7.0` with no prefix. The prefix is the directory path relative to the repository root, exactly as it appears in the module path after the repository part. That is what lets the two modules version independently: `cli` can reach v1.2.0 while the root is still on v0.7.0, and consumers of one never see the other's releases. Get the prefix wrong and the tag simply does not name any module, so `go get github.com/acme/tools/[email protected]` reports an unknown revision even though the tag is plainly there in the repository.

code

text · 7 lines
text
repo github.com/acme/tools
  go.mod       module github.com/acme/tools
  cli/go.mod   module github.com/acme/tools/cli

git tag v0.7.0        # the root module
git tag cli/v1.2.0    # the module in ./cli
git push origin v0.7.0 cli/v1.2.0

go deeper

for a junior

Know that a repository can contain more than one module, each with its own go.mod, and that they are released separately. The exact tag spelling is fine to look up.

for a middle

State the rule precisely: the tag is the module's directory path relative to the repository root, then a slash, then the version — cli/v1.2.0 — while the root module keeps unprefixed tags. Explain why the prefix is needed at all.

for a senior

Diagnose the common mess: a bare tag pushed for a nested module produces an unknown-revision error on fetch plus an unintended root release. Be ready to describe how you would automate release tagging so the prefix cannot be forgotten.

for a principal

Judge whether the split is worth it. Separate modules give independent dependency graphs and release cadence, but they turn every internal change into a publish-then-upgrade round trip. Decide on dependency weight and audience, not on directory aesthetics.

## One repository, several modules A repository is not a module. A module is a directory tree rooted at a `go.mod`, and a repository may contain any number of them: a root library plus a `./cli` command with heavier dependencies, a `./examples` module that pulls in things the library should not require, a `./tools` module holding build-time programs. This creates a naming problem. If both the root and `./cli` are released from the same Git repository, a single flat tag namespace cannot say which one `v1.2.0` refers to. Go resolves it by **prefixing the tag with the module's subdirectory**. ### The rule For a module whose `go.mod` is at `<subdir>/go.mod` relative to the repository root, the version tag is `<subdir>/vX.Y.Z`. ``` repo github.com/acme/tools go.mod -> module github.com/acme/tools cli/go.mod -> module github.com/acme/tools/cli git tag v0.7.0 # publishes github.com/acme/tools git tag cli/v1.2.0 # publishes github.com/acme/tools/cli git push origin v0.7.0 cli/v1.2.0 ``` The prefix is the same suffix that distinguishes the two module paths. The root module's tags carry no prefix at all. A nested module two levels deep, `internal-free/tools/gen`, would use `internal-free/tools/gen/v1.0.0`. ### What this buys you **Independent version lines.** The library at the root can stay at v0 while the command is at v1.2.0, or the command can churn through releases without forcing a version bump on library consumers. Nothing in one module's tag stream is visible to the other's consumers. **Independent dependency graphs.** This is usually the real reason to split. A `./cli` module can require an argument parser, a colour library and a config loader without those appearing anywhere in the build of a service that imports only the root library. The root module's `go.mod` never mentions them, because the packages under `cli/` are no longer part of the root module at all — a nested `go.mod` carves that subtree out of its parent. **Independent release cadence** for things that are genuinely different products living in one repository for convenience. ### What it costs **The root module cannot just import the nested one.** Once `./cli` has its own `go.mod`, code in the root module that imports `github.com/acme/tools/cli/...` is importing an external dependency, which must be required at a published version like any other. That is a genuine friction point for a repository that is really one project, and it is the main argument against splitting. **Tagging discipline gets harder.** Two tag streams in one repository means release automation, changelogs and human memory all have to keep the prefix straight. The common failure is pushing `v1.2.0` when you meant `cli/v1.2.0`: the tag exists, the release looks done, and `go get github.com/acme/tools/[email protected]` reports an unknown revision because no tag names that module at that version. Meanwhile you have accidentally published a root-module v1.2.0 you did not intend to promise anything about. **A single commit spans both modules.** Coordinated changes need two tags on possibly the same commit, and reviewers have to notice when a change to the root's API needs a matching release before the nested module can consume it. ### Deciding to split at all Split when the dependency sets genuinely differ and you would be forcing weight onto library consumers otherwise, or when the pieces have different audiences and release rhythms. Do not split merely because directories look tidy: a nested module converts a free internal import into a versioned external dependency with a release step between every change, and that cost is paid on every commit, not just at release time.

  • Does the ./cli directory need its own go.mod for the prefixed tag to mean anything?
    Yes. Without a go.mod there, `cli/` is just a package inside the root module, and a `cli/v1.2.0` tag names no module at all. The go.mod is what carves the subtree out of the parent module and gives it an identity to version.
  • After splitting ./cli into its own module, how does the root module use its code?
    It cannot import it for free any more. Packages under `cli/` belong to a different module, so the root would have to require `github.com/acme/tools/cli` at a published version like any external dependency. In practice the dependency should run the other way — the command imports the library, not the reverse.
  • Someone pushed v1.2.0 intending to release the cli module. What is the visible symptom?
    `go get github.com/acme/tools/[email protected]` fails with an unknown revision, because no tag names that module at that version, while the repository plainly shows a v1.2.0 tag. You have also published an unintended root-module release, which you must live with and version past.

saying these in an interview costs you the question

  • Assumes one repository can only hold one module
  • Tags a nested module with a bare vX.Y.Z
  • Puts the prefix after the version, like v1.2.0-cli
  • Expects the root module to import the nested one freely
  • Thinks nested modules share one version number