3.5 KiB
ADR-GRPC-002: Schema governance runs without protoc and without the Buf CLI
- Status: accepted
- Date: 2026-08-30
- Scope:
:grpc:grpc-proto-contract,:grpc:grpc-codegen
Context
Stable Tasks 8 through 11 require proto style rules, Buf format/lint/breaking governance, a single Java codegen owner, and a descriptor artifact whose consumer-compile result gates a release.
Two of the tools those tasks name are absent from this toolchain. The Buf CLI is not installed. And
protoc is available through the Gradle protobuf plugin, but every leaf in this repository passes
spotless with google-java-format, checkstyle, SpotBugs at HIGH confidence, Error Prone and -Werror
— and generated protobuf sources pass none of them. Turning codegen on means excluding a source set
from five quality gates.
There is precedent for such an exclusion: the jmh source set has spotbugsJmh and checkstyleJmh
disabled and Error Prone off. So the carve-out is available. It is also a decision about the quality
baseline of a leaf, taken for one task, and outside what this work was asked to change.
adapter:inbound:grpc also carries a recorded decision in the opposite direction: its CLAUDE.md
forbids the protobuf plugin and .proto in that leaf, on the grounds that a consuming feature module
should own its schema.
Decision
Commit the .proto sources and implement every rule the tasks require as executable Java, with no
protoc run and no Buf CLI invocation.
GrpcProtoContractValidator reads .proto text and enforces proto3 syntax, the
{organization}.{domain}.v{major} package rule, java_multiple_files, a generated Java package
disjoint from the hand-written one, _UNSPECIFIED enum zero values, reserved declarations checked
against a supplied removal history, a well-known-type allowlist and a map-field allowlist. It runs
against the committed schema in its own test, so the shipped .proto files are live rather than
decorative.
GrpcBufPolicy fixes the breaking gate at Buf's FILE category and names the four lifecycle stages
a compliant pipeline registers. GrpcCodegenManifest fixes one codegen owner and refuses a literal
generator version. GrpcDescriptorArtifact, GrpcConsumerFixture and GrpcSchemaArtifactPublisher
carry the schema hash, the descriptor digest and the per-consumer source-break report, and refuse a
publish that breaks a consumer or republishes a released version with different bytes.
The committed buf.yaml states the same rules, so running the CLI in an environment that has it
reaches the same verdict.
Consequences
The invariants are enforced; the process is not run. Everything Tasks 8 to 11 are about — which schema changes are refused, which consumer breaks block a release, who owns generation — is a build-checkable rule here. What is missing is the protoc invocation and the Buf binary.
Turning codegen on is a bounded change. GrpcCodegenManifest.caSkeleton() already names the
owner, the managed version source, the build-directory output paths and the disjoint package policy
that a real plugin configuration has to satisfy. The work is a source-set carve-out and a plugin
block, not a redesign.
The fixtures use a text codec. GrpcTextCodec gives the testkit a UTF-8 marshaller so the
in-process and Netty lanes can exercise interceptors, status mapping, metadata limits and stream
sequencing without generated stubs. Those contracts are properties of the platform and the transport,
not of any message shape, so the substitution costs nothing — and the lanes run today rather than
after codegen lands.