63 lines
3.5 KiB
Markdown
63 lines
3.5 KiB
Markdown
# 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.
|