ADR-0013: Generated code is committed
- Status: accepted
- Date: 2026-08-10
Context
Protobuf bindings and database access code are generated, from
proto/meshp/v1/*.proto and from migrations/ plus queries/. Either they are
committed or they are produced by a build step.
Leaving them out looked tidier and broke immediately in practice. Generated
protobuf code imports google.golang.org/protobuf, so with proto/gen/
ignored, the dependency was absent from go.mod — everything built on a fresh
clone precisely because nothing referenced the generated package, and the moment
a developer ran make proto, go build ./... and govulncheck both failed on
code the repository could not see.
The deeper problem is that Go's tooling assumes buildable source in the tree.
go install github.com/meshpnet/meshp/cmd/meshp@latest runs no Makefile. Neither
does a downstream module importing our protocol package — and meshp-sdk, the
mobile clients and eventually third parties are all expected to.
Decision
Generated code is committed:
proto/gen/— protobuf bindingsinternal/store/gen/— sqlc output
The generators remain the source of truth. CI regenerates and fails if the result
differs from what is committed, so the tree cannot drift from the .proto and
.sql files it came from.
Every generator must be pinned, including the plugins a generator invokes. The
check is only meaningful if regeneration is deterministic, and the first version of
this setup was not: buf itself was pinned in the Makefile while buf.gen.yaml
named remote: buf.build/protocolbuffers/go with no version. Bindings were
committed by protoc-gen-go v1.36.11 and CI regenerated them with v1.36.12 hours
later, so a pull request that touched no .proto at all failed on a one-line
version comment. A gate that fails for reasons unrelated to the change is a gate
people learn to ignore.
protoc-gen-go is therefore invoked locally through go tool, so its version is
whatever go.mod pins — one number, visible to Dependabot, and necessarily the
same version as the protobuf runtime the generated code is compiled against.
Consequences
go build, go test, go install and govulncheck all work on a fresh clone
with no generate step. Downstream modules can import our protocol package. New
contributors do not hit a build failure before they have read anything.
The costs are real but small. Regeneration produces large diffs that reviewers
must learn to skim rather than read. A pull request touching a .proto and
forgetting to regenerate is rejected by CI rather than by a human, which is the
right outcome but an extra round trip for the author. And committed generated
code can be edited by hand, which nothing prevents — the regeneration check
catches it, so the failure is loud, but it is worth saying that the check is the
only thing standing between us and hand-patched bindings.
Alternatives considered
Generate at build time, keep it out of the tree. Tidier history, and the
approach we started with. Rejected because it breaks go install and every
downstream importer, and because it silently hid a missing dependency.
Commit, and skip the regeneration check. Cheaper CI. Rejected: without the check, committed output drifts from its source and the first symptom is a field that exists in the schema and not in the code.
A separate module for generated code. Isolates the diffs and lets the protocol version independently. Worth revisiting if the protocol is ever consumed widely enough to need its own release cadence; today it would mean a cross-repo version bump for every protocol change during the period we change it most.
Rendered from docs/adr/0013-commit-generated-code.md, which is where it is edited.