protobuf-compat-checking
Wraps `buf breaking` to compare a Protobuf schema against a past version, classifies breaking changes by category (FILE / PACKAGE / WIRE_JSON / WIRE), configures buf.yaml rule selection plus per-path exemptions, and gates CI on the exit code. Use when reviewing `.proto` changes in a PR for gRPC services, BSR modules, or schema-versioned message bus payloads.
protobuf-compat-checking
Overview
buf breaking is the canonical Protobuf compatibility checker from the Buf project (buf-breaking). It compares the current schema against a past version (BSR module, Git ref, tarball, or buf image) and reports changes that would break clients, servers, or generated code.
This is the Protobuf-specific counterpart to openapi-contract-diff (REST) and graphql-schema-regression (GraphQL).
When to use
Breaking categories
buf breaking rules ship in four nested categories - stricter subsumes looser (buf-breaking):
| Category | Detects |
|---|---|
FILE | Source-code breakage per-file. Default; strictest. |
PACKAGE | Source-code breakage per-package. |
WIRE_JSON | Wire-format or JSON-encoding breakage. |
WIRE | Wire-format breakage only. Loosest. |
Choose FILE for new internal services where source compatibility matters; choose WIRE_JSON for stable APIs where source rearrangement (e.g. moving a message between files) is acceptable but JSON-mapping breakage is not; choose WIRE only when JSON encoding is out-of-scope.
Configuration
buf.yaml v2 form (buf-yaml):
version: v2
modules:
- path: proto
name: buf.build/acme/foo
breaking:
use:
- FILE
except:
- FILE_NO_DELETE # allow file deletions despite using FILE
ignore:
- foo/legacy/ # exclude entire path from all breaking rules
ignore_only:
FILE_NO_DELETE:
- foo/bar.proto # exclude specific path from a specific rule
ignore_unstable_packages: true # ignore v1alpha1 / v1beta1 / *test* packagesversion: v2 is required for the structure above; legacy v1 and v1beta1 files use a different shape (buf-yaml). When migrating, run buf migrate v1tov2 to convert.
ignore_unstable_packages: true is the canonical pattern - Protobuf convention is that v1alpha1, v1beta1, and *test* packages are unstable and explicitly NOT covered by compatibility guarantees.
Running
Against a Git reference
buf breaking --against '.git#branch=main'
buf breaking --against '.git#tag=v1.2.0'
buf breaking --against '.git#ref=HEAD~1'(Per buf-breaking.)
The .git#... syntax tells buf to checkout the referenced ref into a temp directory and use it as the baseline. This is the most common CI pattern: compare PR against main.
Against a BSR module
buf breaking --against buf.build/acme/foo
buf breaking --against buf.build/acme/foo:<commit-sha>Use the BSR baseline when the team treats BSR as the source of truth and a release labeled v1 is what production is consuming.
Against a tarball or buf image
buf breaking --against archive.tar.gz
buf breaking --against image.bin # buf-image-format binaryUseful for hermetic builds where the baseline is published as an artifact rather than a git ref.
Reading the report
buf breaking exits non-zero on detected breaking changes. The default output prints one line per finding with the offending file, line, column, rule ID, and message:
proto/orders/v1/order.proto:42:3:Field "1" with name "amount" on message
"Order" has the same number as deleted field with name "total".
[ENUM_VALUE_NO_DELETE_RESERVED]For machine consumption, supply --error-format json:
buf breaking --against '.git#branch=main' --error-format json{
"path": "proto/orders/v1/order.proto",
"start_line": 42,
"start_column": 3,
"type": "ENUM_VALUE_NO_DELETE_RESERVED",
"message": "Field 1 ..."
}Pipe to jq for triage:
buf breaking --against '.git#branch=main' --error-format json | \
jq -r '"\(.type) \(.path):\(.start_line) — \(.message)"'CI integration
GitHub Actions
The Buf project ships an official Action (bufbuild/buf-action) that runs lint + breaking + push against BSR in one job. For self-hosted breaking-only:
# .github/workflows/buf-breaking.yml
name: buf-breaking
on:
pull_request:
paths:
- '**/*.proto'
- 'buf.yaml'
- 'buf.lock'
jobs:
breaking:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0 # need history for `.git#branch=main` baseline
- uses: bufbuild/buf-setup-action@v1
- name: buf breaking
run: buf breaking --against '.git#branch=main,subdir=.'fetch-depth: 0 is required so the .git#branch=main ref resolves - the default shallow clone doesn't have it.
For stricter pipelines, also gate on buf lint:
- name: buf lint
run: buf lint
- name: buf breaking
run: buf breaking --against '.git#branch=main,subdir=.'Tag-based releases
For projects using git tags as release boundaries:
# Compare PR vs the latest released tag
LATEST_TAG=$(git describe --tags --abbrev=0)
buf breaking --against ".git#tag=${LATEST_TAG}"This catches breaking changes vs production, not just vs the trunk HEAD - useful when main itself contains in-progress un-released work.