Buf CLI

Breaking change detection – Tutorial

As you evolve your Protobuf schemas, you might introduce breaking changes—either by breaking your generated code, or by breaking your ability to read existing data. Protobuf has many ways to evolve schemas without breaking existing code, but sometimes it's a better choice to make a breaking change rather than go to the extra effort of backwards compatibility. If you have few clients and can easily update and deploy them, it may be perfectly okay to break your schemas. Buf's breaking change detection reliably and mechanically identifies breaking changes so you and your team can focus on the important human decision of whether to allow them or not.

The tutorial takes you through running breaking change detection locally using common use cases. Read the overview to learn about editor integration, policy checks, and the review flow.

Prerequisites

We recommend completing the Buf CLI tour to get an overview of the Buf CLI first.

  • Install the Buf CLI
  • Clone the buf-tour repo:
    $ git clone git@github.com:bufbuild/buf-tour.git
    

1. Inspect the workspace

Modules represent a collection of files that are configured, built, and versioned as a logical unit when performing Buf CLI operations. Workspaces are collections of modules and are configured by the buf.yaml configuration file, which should generally be put above the directories that contain the modules within it. The example code provides a workspace and module to work with, so start there. From the directory you cloned into, go to the tutorial code:

$ cd buf-tour/start/tutorial-breaking

Your workspace has the directory structure shown below, and is defined by the buf.yaml at its root. The module described in it is your input for the buf breaking commands in the rest of the tutorial.

tutorial-breaking
├── buf.yaml
└── proto
    └── pet
        └── v1
            └── pet.proto

The example buf.yaml file contains all of its required fields. The breaking field controls your breaking change detection settings. It's set to our recommended default of FILE, which provides the highest level of protection against breaking changes.

~/.../buf-tour/start/tutorial-breaking/buf.yaml
version: v2
modules:
  - path: proto
    name: buf.build/tutorials/breaking
lint:
  use:
    - DEFAULT
breaking:
  use:
    - FILE

For more information about specific fields, see the buf.yaml reference.

2. Compare against a local Git repository

The example code is a Git repository, so you can check whether your uncommitted changes break the schemas. First, make a non-breaking change to your schema and add a new type of pet to the enum:

~/.../buf-tour/start/tutorial-breaking/proto/pet/v1/pet.proto
// PetType represents the different types of pets in the pet store.
enum PetType {
  PET_TYPE_UNSPECIFIED = 0;
  PET_TYPE_CAT = 1;
  PET_TYPE_DOG = 2;
  PET_TYPE_SNAKE = 3;
  PET_TYPE_HAMSTER = 4;
+ PET_TYPE_BIRD = 5;
}

Then run buf breaking to compare the workspace to the one in the original repo you downloaded. You should see no errors.

~/.../buf-tour/start/tutorial-breaking
$ buf breaking --against '../../.git#subdir=start/tutorial-breaking/proto'

Note that in the --against target, you need to point to the root of your Git repository, then traverse back down to the directory you're comparing against using the subdir option.

3. Compare against the Buf Schema Registry (BSR)

For organizations that use the BSR, comparing against the version of the module stored there is the most common use case. The example module you're working with exists in the BSR already at https://buf.build/tutorials/breaking, and if you look in the buf.yaml file for your module, the name field points there.

~/.../buf-tour/start/tutorial-breaking/proto/buf.yaml
version: v2
modules:
  - path: proto
    name: buf.build/tutorials/breaking
lint:
  use:
    - DEFAULT
breaking:
  use:
    - FILE

This time, make a breaking change to the schema by changing the fourth item in the enum.

~/.../buf-tour/start/tutorial-breaking/proto/pet/v1/pet.proto
// PetType represents the different types of pets in the pet store.
enum PetType {
  PET_TYPE_UNSPECIFIED = 0;
  PET_TYPE_CAT = 1;
  PET_TYPE_DOG = 2;
  PET_TYPE_SNAKE = 3;
- PET_TYPE_HAMSTER = 4;
+ PET_TYPE_RODENT = 4;
  PET_TYPE_BIRD = 5;
}

Run buf breaking again, this time comparing against the latest version of the tutorial module in the BSR. You should receive an error.

~/.../buf-tour/start/tutorial-breaking
$ buf breaking --against buf.build/tutorials/breaking
Output
proto/pet/v1/pet.proto:11:21:Enum value "4" on enum "PetType" changed name from "PET_TYPE_HAMSTER" to "PET_TYPE_RODENT".

Revert the change.

4. Compare against a remote Git repository

If your .proto files aren't in the BSR yet, generally you'd compare against your remote Git repository instead, since that represents the latest version of your code. That's a straightforward change to the --against target, so we'll also explore what happens when you change the configuration to a different rule set—from FILE to PACKAGE. PACKAGE allows elements to move within a package, unlike FILE, which is stricter.

First, move the PetType enum to a new pet_type.proto file.

~/.../buf-tour/start/tutorial-breaking
$ touch proto/pet/v1/pet_type.proto

Delete the enum from pet.proto, and add an import statement to reference the new .proto file:

~/.../buf-tour/start/tutorial-breaking/proto/pet/v1/pet.proto
+ import "pet/v1/pet_type.proto";
- // PetType represents the different types of pets in the pet store.
- enum PetType {
-   PET_TYPE_UNSPECIFIED = 0;
-   PET_TYPE_CAT = 1;
-   PET_TYPE_DOG = 2;
-   PET_TYPE_SNAKE = 3;
-   PET_TYPE_RODENT = 4;
-   PET_TYPE_BIRD = 5;
}

Then copy/paste the following into pet_type.proto (note that the package is still pet.v1):

~/.../buf-tour/start/tutorial-breaking/proro/pet/v1/pet_type.proto
syntax = "proto3";

package pet.v1;

// PetType represents the different types of pets in the pet store.
enum PetType {
  PET_TYPE_UNSPECIFIED = 0;
  PET_TYPE_CAT = 1;
  PET_TYPE_DOG = 2;
  PET_TYPE_SNAKE = 3;
  PET_TYPE_HAMSTER = 4;
}

Run buf breaking again, this time comparing against the latest version of the remote Git repository. You should get an error showing that the enum was deleted.

~/.../buf-tour/start/tutorial-breaking
$ buf breaking --against 'https://github.com/bufbuild/buf-tour.git#branch=main,subdir=start/tutorial-breaking/proto'
Output
proto/pet/v1/pet.proto:1:1:Previously present enum "PetType" was deleted from file.

Let's assume that moving an enum within the same package isn't considered a breaking change in your organization. For buf breaking to align to this policy, it needs to be set to the PACKAGE rule set instead of FILE. Make that change in your buf.yaml file:

~/.../buf-tour/start/tutorial-breaking/proto/buf.yaml
version: v2
modules:
  - path: proto
    name: buf.build/tutorials/breaking
breaking:
  use:
+ - PACKAGE
- - FILE
lint:
  use:
    - DEFAULT

Run the same buf breaking command, and you should no longer get the error.

These scenarios represent the most common use cases for locally using buf breaking. If your organization also has server-wide breaking change detection, you may see different results when running locally versus when you push a module to the BSR. See the breaking change policy check documentation for the details.