Skip to content

Why this is its own module

Project-root detection is about a hundred lines of code. It began life inside go-tool-base, the CLI framework, and was cut out into a module of its own. That looks like overhead — another repository, another version number, another entry in your go.mod — so it is worth stating what it buys, because the reasoning also explains why some obvious features will never be added here.

The cost of asking a framework where your project root is

In Go, a dependency is transitive and a module is the unit of it. Importing one package from a framework brings the framework's module requirements with it, and go.sum, govulncheck, your SBOM and your organisation's supply-chain review all see the whole graph — not the one function you called.

For a CLI framework that graph is large by design: a command library, a config loader, a terminal-rendering stack, telemetry. None of it is wrong. All of it is irrelevant to answering "which directory holds go.mod". Split out, the answer to that question costs afero, cockroachdb/errors and the standard library, and nothing else.

That is the trade: one more line in go.mod, in exchange for a dependency graph you can read in one screen.

Why the boundary is enforced by a test rather than by intent

A stated intention to stay small survives until the first convenient import. So the constraint is executable. depfootprint_test.go shells out to go list -deps ./... and fails the build if the graph ever contains go-tool-base, go-git, Viper, pflag, Cobra, the Charm stack, OpenTelemetry, or the AWS, Google Cloud and Azure SDKs.

The list is not a security policy — nothing there is dangerous. It is the shape of the module written down where CI can check it. If a change needs one of those, the change does not belong in this module; the test says so before review does.

What that boundary rules out

Reading the constraint forwards tells you what to expect from this package, and what to stop waiting for:

  • No logging. A logger would mean a logging dependency or an interface for callers to satisfy. The walk returns a result or ErrNotFound; anything worth saying about it, the caller says.
  • No configuration. No config file, no environment variable, no marker list read from disk. That would need a config loader, and a config loader is a framework's job. Every input is an argument to the call.
  • No git integration. .git is a marker because Stat can see it, not because anything here understands repositories. Branch names, remotes and submodule boundaries are out of reach by construction.
  • No CLI. The module ships a library and the pipeline builds no binary — which is also why there is no release signing here, and why go.sum plus the Go checksum database is the whole integrity story.

The full list of what the package does not do, including the parts that are about the algorithm rather than the dependency graph, is in what workspace does not do.

When you would not use it

If your tool already depends on a framework that answers this question, use that answer. The value of a leaf module is entirely in the graph you avoid, and there is none to avoid if you have already paid for it. workspace is for the tool that wants project-root detection without buying anything else — a linter, a small utility, a library that must stay cheap to depend on.