Breaking Changes
Catch a change that breaks callers before it ships.
kizuna diff compares two versions of your API and reports what each change costs the people already calling it. It reads the declarations, so route keys, MCP tool names and job keys are all in view, and every Zod schema is compared field by field.
Run it against a ref
kizuna diff --against mainBREAKING users.patchUser renamed to users.updateUser
client method and generated client names change, HTTP surface unaffected
BREAKING POST /users body.organisationId is now required
BREAKING the job reconcileInvoices is gone
whatever calls POST /jobs/run with that key stops working
CHANGED GET /users/:id can now answer 410
ADDED POST /users/:id/archive addedGate it in CI
One checkout, one install, no build. fetch-depth: 0 matters, since the default shallow clone has no base commit to compare against.
name: Breaking Changes
on:
pull_request:
branches: [main]
jobs:
breaking-changes:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: pnpm/action-setup@v6
- uses: actions/setup-node@v6
with:
node-version: 24
cache: pnpm
- run: pnpm install --frozen-lockfile
- name: Compare against main
id: diff
run: pnpm kizuna diff --against ${{ github.event.pull_request.base.sha }}
- name: Require a label for breaking changes
if: steps.diff.outcome == 'failure'
run: |
if [[ "${{ contains(github.event.pull_request.labels.*.name, 'breaking changes') }}" == "true" ]]; then
echo "Breaking changes acknowledged via label."
else
echo "::error::Breaking changes detected. Add the 'breaking changes' label to acknowledge."
exit 1
fiA breaking change blocks the pull request until someone adds the breaking changes label.
The route surface
| Change | Level |
|---|---|
| A route removed | breaking |
| A route renamed | breaking |
| A method or path changed | breaking |
| A declared status no longer answered | breaking |
| A job key removed | breaking |
| An MCP tool removed or renamed | breaking |
| A new status | changed |
| A new deprecation or sunset | changed |
| A route added | added |
A rename is spotted by matching a departed key against an arrived one on the same method and path, so it reads as one breaking change rather than a removal and an addition. Renaming users.patchUser to users.updateUser leaves the method, the path and every schema alone, and still changes the client method, the MCP tool name, and the method names in the generated Swift and Kotlin clients.
Schemas, where direction decides everything
A caller sends requests and reads responses, so tightening an input and dropping an output are the two that hurt.
| Change | Request | Response |
|---|---|---|
| A required field added | breaking | changed |
| An optional field added | quiet | changed |
| A field removed | changed | breaking |
| An optional field required | breaking | changed |
| A type changed | breaking | breaking |
| An enum value removed | breaking | breaking |
| An enum value added | changed | breaking |
That last row catches people out. Adding a value to a response enum breaks any client that generated a closed enum for it, which is what unknownEnumCase exists for in the Swift and Kotlin clients.
Bodies, query parameters, headers and every response body are compared, reported by path and followed into arrays:
BREAKING POST /users body.address.postcode is now required
BREAKING GET /users 200.items[].id is number instead of stringWhat it cannot see is a rule inside .refine() or .superRefine(), because those are functions rather than declarations. A tightened refinement passes quietly.
Keeping generated clients current
A generated client that has fallen behind is a compile error waiting to happen in whatever imports it. kizuna generate --check catches it, and checkClients does the same inside a script of your own:
import { checkClients, formatStale } from '@kizunajs/cli';
import kizuna from '../kizuna.config';
const stale = checkClients(kizuna.api, kizuna.clients);
if (stale.length > 0) {
console.error(formatStale(stale));
process.exit(1);
}A generated client is out of date:
src/lib/api-client.generated.ts is behind the config
Run `kizuna generate` and commit the result.Reference
- Deprecations, for retiring a route rather than removing it
- Configuration