DEV Community

Jeff
Jeff

Posted on Originally published at powerduck.com

Breaking Change Detection in CI: Catching a Removed Field Before Release

Quick answer: Breaking API changes are rarely the result of someone intending to break clients; they are the result of a change that looks harmless in a pull request but removes or alters a field that a generated client depends on. Relying on a human review of a large OpenAPI diff is how these slip through, because the breakage is a structural comparison against the previous contract, not a judgment about the code quality. The reliable approach is to run an automated breaking-change check in CI on every spec pull request, classify the delta against the previously released version, and block merges that remove or change published operations without an explicit, reviewed exception. If the first time a breaking change is detected is a client 400 in production, the review process failed; the diff should be rejected automatically long before it ships.

Documentation-focused tools are good at rendering and linting a spec, and linting catches style and structural problems inside the current document, but it cannot answer the separate question of whether this version is compatible with the last released one. That comparison is temporal: it requires two versions of the contract and a rule set for what constitutes a breaking delta, such as deleting a field, tightening a constraint, or making an optional parameter required. A clean, lint-passing spec can still be a breaking change relative to what clients already consume.

What actually counts as breaking

Some changes are obviously breaking and others are quietly so, and the second category is what the CI check is for.

Change Breaking? Why clients break
Remove an endpoint or field Yes Generated calls fail
Make optional field required Yes Old requests rejected
Add an optional field No Ignored by old clients
Narrow an enum or range Yes Valid values now fail
Change a type or format Yes Parsing assumptions break
Add a new endpoint No Purely additive

The enum narrowing row is a classic blind spot: removing a value that existing clients still send passes linting and looks like cleanup, but it turns previously valid requests into 400s the moment the change deploys.

Run the comparison in CI against the released contract

The check should fetch the last released spec, compare it to the pull request version, and fail the build on any breaking rule rather than merely printing a report that a reviewer can ignore.

Wiring the check to the required status means a structural break cannot be merged by accident, which is the whole point. The reviewer then sees an explicit list of breaking rules rather than having to reconstruct the delta from a raw diff.

Allow deliberate breaks through an explicit path

Some breaking changes are intentional, such as a versioned API removal that was announced in advance. Those should not be silently force-merged; they should pass through a documented exception that records the approval and the migration path, so the audit trail shows the break was deliberate and clients were warned.

An expiration on the suppression prevents it from becoming a permanent hole, and tying it to a specific path keeps the rest of the spec protected. The default remains to block; the exception is narrow and time-bound.

Gate client generation and release on the same contract

Because clients and mocks are generated from the same OpenAPI contract, a passing compatibility check means the generated SDKs stay safe to publish, and the diff output can feed release notes automatically. The contract is the single gate: lint for internal correctness, breaking-change detection for backward compatibility, then generation. Keeping these steps together in one pipeline prevents the situation where docs render correctly but the underlying change silently invalidates every consumer.

The practical recommendation is to treat backward compatibility as an automated build gate, not a review expectation. Every spec pull request diffs against the last released contract, breaking rules fail the build, and intentional removals pass through a scoped, expiring suppression with a recorded migration. If detection depends on a reviewer noticing a deleted enum value in a thousand-line diff, clients will find the break first.

Frequently asked questions

Is linting the spec enough to prevent breaks?

No. Linting validates the current document's structure and style, while breaking-change detection compares two versions over time. A lint-clean spec can still remove a field that existing clients use.

What counts as a breaking change?

Removing operations or fields, making an optional parameter required, narrowing enums or ranges, and changing types or formats. Additions of optional fields or new endpoints are generally safe.

How do we ship an intentional breaking change?

Use a scoped, expiring suppression tied to the specific path with a recorded approval and migration guide, rather than disabling the check or force-merging.

Where should the check run?

In CI on every spec pull request, comparing against the last released contract and blocking the merge when a breaking rule fires.

How does this protect generated clients?

Since SDKs and mocks are generated from the same contract, a passing compatibility gate guarantees the published clients remain safe, and the diff can feed release notes automatically.

This is part of the Powerduck vs Redocly comparison — start there for the full picture of contract governance.

Top comments (0)