Versioning
How Spec0 thinks about API evolution. The simple rule we ask every team to follow.
The rule
Breaking changes never bump an existing API. They create a new one.
Every successful publish to an existing API in Spec0 is, by construction, backwards-compatible with the version before it. The day you need to remove a field, change a type, or tighten a constraint, you stop publishing to my-api and start a new one — typically my-api-v2. Consumers stay on v1 until they're ready; v2 lives alongside it for as long as both have users.
Why we do it this way
Consumers can't be surprised. If your API is in the registry and you're still publishing to it, integrators know — without reading a changelog — that pulling the latest version won't break their code.
Migration is opt-in, not forced. A v2 alongside v1 means every consumer migrates on their own timeline. No coordinated cutover, no scrambling SDK regenerations, no surprise outages.
Versioning becomes boring. With breaking changes off the table, you don't agonise over major-vs-minor every PR. Every merge that lands is a non-breaking improvement. The registry version moves forward.
How versions move forward
On every successful merge to main: minor bump
The publish workflow reads the highest semver tag currently in your API's registry, bumps the minor segment, and publishes the new spec under that version.
latest = 1.4.0 → next published = 1.5.0Server-side resolution avoids races between concurrent publishes. From CI:
spec0 publish --spec-file ./openapi.yaml --name my-api --bump minorOn a breaking-change PR: CI fails
Every PR that touches the spec runs spec0 diff --breaking-only against main. If Spec0's diff classifies any change as breaking — removing a field, narrowing a type, tightening a constraint, removing an enum value, adding a required field — the PR is blocked. The fix is never to bypass the gate; it's to start a new API.
For a major redesign: new API, new slug
Pick a new slug (payments-api-v2), publish at 1.0.0, and let it accumulate minor bumps from there. The old API stays published — and discoverable — for as long as it has consumers. Mark it deprecated when you're ready to encourage migration.
What counts as breaking?
| Safe — minor bump | Breaking — must be a new API |
|---|---|
| Adding a new endpoint or operation | Removing a field, parameter, endpoint, or operation |
| Adding a new optional field | Changing a field's type |
| Adding a new value to an extensible enum | Adding a required field or required parameter |
Loosening a constraint (raising maxLength) | Tightening a constraint (lowering maxLength, narrowing a regex) |
| Adding a new optional query parameter | Removing or renaming an enum value |
| Updating descriptions, summaries, examples, server URLs | Changing authentication or required headers |
Enforcement in CI
The Spec0 CLI ships with the diff and publish primitives that drive this policy. A typical workflow looks like this:
# .github/workflows/public-api-ci.yml
# On every PR that touches the spec
- run: spec0 diff base.yaml head.yaml --breaking-only
# → exits non-zero if any breaking change is found
# On every merge to main
- run: |
spec0 publish ./spec.yaml \
--name my-api \
--bump minor \
--visibility published
# → publishes under e.g. 1.5.0, idempotent if content matchesReference workflow: .github/workflows/public-api-ci.yml in the spec0-platform repo. Spec0 dogfoods this exact policy on its own public API.
See also
- APIs — the registry, governance gates, and how publishing works end-to-end.
spec0 publish—--bump minor|patchfor server-resolved version bumps.spec0 diff—--breaking-onlyfor the BWC gate.