Linting

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
Breaking Changes config/routes.oas.json 0:0 error Removed "/todos/{id}" (was "/todos/{id}") breaking-change 15:25 error Modified "required" from "false" to "true" breaking-change 37:21 error Modified "type" from "string" to "integer" breaking-change ✖ 3 breaking changes remaining

Turn on detection

Breaking change detection is off by default. Turn it on in zuplo.lint.jsonc in the root of your project:

JSONzuplo.lint.jsonc
{ "version": 2, "breakingChanges": { "enabled": true, "baseline": "origin/main", }, }

Then run the linter as usual:

TerminalCode
git fetch origin main npx zuplo lint

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

FieldDescription
enabledTurns detection on. Defaults to false.
baselineThe 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:

ChangeExample message
Removing a path or an operationRemoved "/todos/{id}" (was "/todos/{id}")
Adding a required parameterAdded "parameters"
Making an optional parameter requiredModified "required" from "false" to "true"
Making a request body property requiredAdded property "required"
Changing the type of a parameter, property, or schemaModified "type" from "string" to "integer"
Removing a value from an enumRemoved property "enum" (was "done")
Removing a response status codeRemoved "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:

severityfailOn: "error" (default)failOn: "warn"
"error" (default)FailsFails
"warn"Passes, and reportsFails

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:

YAML.github/workflows/lint.yaml
steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: actions/setup-node@v4 with: node-version: 24 - name: Install dependencies run: npm ci - name: Lint run: npx zuplo lint

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
Breaking Changes 0:0 error Breaking change detection failed: baseline ref "main" not found in this repository; fetch it first (for example, git fetch origin main) or set breakingChanges.baseline to a ref that exists, such as origin/main breaking-change

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.

MessageFix
baseline ref "main" not foundFetch the branch (git fetch origin main), or set baseline to origin/main.
cannot read baseline ... not a git repositoryRun the linter inside a Git repository, or turn off detection for that environment.
invalid baseline refUse a branch, tag, or commit SHA rather than an expression such as HEAD~1.

Next steps

Last modified on