Spec0docs
Platform

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.0

Server-side resolution avoids races between concurrent publishes. From CI:

spec0 publish --spec-file ./openapi.yaml --name my-api --bump minor

On 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 bumpBreaking — must be a new API
Adding a new endpoint or operationRemoving a field, parameter, endpoint, or operation
Adding a new optional fieldChanging a field's type
Adding a new value to an extensible enumAdding a required field or required parameter
Loosening a constraint (raising maxLength)Tightening a constraint (lowering maxLength, narrowing a regex)
Adding a new optional query parameterRemoving or renaming an enum value
Updating descriptions, summaries, examples, server URLsChanging 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 matches

Reference 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|patch for server-resolved version bumps.
  • spec0 diff--breaking-only for the BWC gate.

On this page