Breaking Change Detection
- Applies to
- Gatewayruntime for API, AI, and MCP gateways
- Hosting
- Managed EdgeManaged DedicatedSelf-Hosted
Your OpenAPI files are the contract your API consumers build against. Remove an endpoint, make a query parameter required, or change a field from a string to an integer, and every client that relied on the old shape breaks — usually in production, and usually after the change has shipped.
Breaking change detection catches these changes before you merge them.
zuplo lint compares each OpenAPI file in your project with the same file on a
Git branch, such as main, and reports every change that can break an existing
consumer:
Code
Turn on detection
Breaking change detection is off by default. Turn it on in zuplo.lint.jsonc in
the root of your project:
zuplo.lint.jsonc
Then run the linter as usual:
Code
Detection runs alongside the rest of the lint checks. It uses no extra flags, so the same command works on your machine and in CI.
Settings
| Field | Description |
|---|---|
enabled | Turns detection on. Defaults to false. |
baseline | The branch, tag, or commit SHA to compare against, such as main, origin/main, or v1.2.0. Defaults to "main". |
severity | "error" or "warn". Defaults to "error". Controls whether a breaking change fails the run — see Block or warn. |
Choose a baseline
The baseline is the version of your API that consumers use today. Compare against whatever represents that version:
- The branch you deploy production from, usually
main. Each pull request then reports what it would break once merged. This is the most common setup. - A release tag, such as
v1.2.0. Every change since the last release is checked, which is useful when several merges ship together.
Use origin/main rather than main when you lint in CI. CI systems usually
check out the pull request's commit without creating a local main branch, so
only the remote-tracking branch origin/main exists. origin/main also works
on your machine after git fetch, so one configuration serves both.
The baseline value must be a branch, tag, or commit SHA. Expressions such as
HEAD~1 or main^ aren't accepted.
What counts as a breaking change
A change is breaking when a client written against the baseline can fail against the new version. The linter reports these kinds of change, among others:
| Change | Example message |
|---|---|
| Removing a path or an operation | Removed "/todos/{id}" (was "/todos/{id}") |
| Adding a required parameter | Added "parameters" |
| Making an optional parameter required | Modified "required" from "false" to "true" |
| Making a request body property required | Added property "required" |
| Changing the type of a parameter, property, or schema | Modified "type" from "string" to "integer" |
| Removing a value from an enum | Removed property "enum" (was "done") |
| Removing a response status code | Removed "codes" (was "201") |
These changes are safe, so the linter doesn't report them:
- Adding a new path, operation, or response
- Adding an optional parameter or request body property
- Adding a property to a response
- Adding a value to an enum
- Changing descriptions, summaries, and examples
Detection compares the API contract — paths, operations, parameters, request
bodies, and responses. It doesn't compare the gateway configuration inside
x-zuplo-route, so removing a policy or changing a handler isn't reported as a
breaking change.
Read the results
Breaking changes appear in their own Breaking Changes section, after the
other lint results, and are counted separately in the summary. Every result uses
the ID breaking-change.
Each result points at the change in your current file. A removal has nothing
left to point at, so its location is 0:0; the message names what was removed.
In --format json output, breaking changes are in the breakingChanges array
and counted in summary.breakingChanges. In --format sarif output, they're
included with the other results.
New files have nothing to compare against, so a file that doesn't exist on the
baseline never reports breaking changes. Detection checks the same files as the
rest of the linter: every .oas.json file in config/, or the files listed in
include.
ignorePaths doesn't apply to
breaking changes.
Block or warn
severity decides whether a breaking change fails the run:
severity | failOn: "error" (default) | failOn: "warn" |
|---|---|---|
"error" (default) | Fails | Fails |
"warn" | Passes, and reports | Fails |
Use "error" to block pull requests that break the contract. Use "warn" when
breaking changes are sometimes intentional — for example, while an API is still
in beta — and you want reviewers to see them without blocking the merge.
When you do mean to ship a breaking change, the usual approach is to version the
API instead: keep the old operation, add the new one under a new path such as
/v2/todos, and remove the old one later. See
Versioning on Zuplo.
Run in CI
Detection reads the baseline from your Git history, so the CI checkout has to
include it. With GitHub Actions, set fetch-depth: 0 on actions/checkout:
.github/workflows/lint.yaml
On other CI systems, run git fetch origin main (or the branch you compare
against) before the linter. See Lint in CI/CD for
complete workflows, including GitHub code scanning.
Requirements and errors
Detection uses Git, so it needs:
- Git installed where the linter runs.
- A project inside a Git repository. The project can be the repository root or any subdirectory of it.
- The baseline ref in the local repository.
If any of these is missing, the linter can't compare, and it says so instead of reporting no changes:
Code
This result uses the severity you set, so with the default "error" it fails
the run. A comparison you asked for that can't happen fails the same gate a
breaking change would.
| Message | Fix |
|---|---|
baseline ref "main" not found | Fetch the branch (git fetch origin main), or set baseline to origin/main. |
cannot read baseline ... not a git repository | Run the linter inside a Git repository, or turn off detection for that environment. |
invalid baseline ref | Use a branch, tag, or commit SHA rather than an expression such as HEAD~1. |
Next steps
- Lint in CI/CD — run detection on every pull request.
- Lint configuration — the rest of the
zuplo.lint.jsoncoptions. - Versioning on Zuplo — ship a breaking change without breaking existing consumers.