Documentation for v2.7.0. Read the latest, v2.12.0.

Developer

API lifecycle

The unit of evolution is the version line: the ordered MAJOR.MINOR.PATCH versions one Producer has published for one API type. This chapter is what happens on that line, with the exact messages a refusal carries.

What changed in 2.6.0

  • Every accepted provide, every refusal and every newly deprecated element can be delivered as a webhook (version.published, provide.rejected with the rule that fired, version.deprecated). Receivers are in Webhooks.
  • A GA provide with a previous GA on the line reports the classification against it in the version.published event: compatibility.impact is patch, minor or major. The diff is computed only when a hook will receive the event.

In short#

  1. Iterate on snapshots: overwritable, last writer wins, never compatibility-checked, expire when unused.
  2. Release the same number as GA: it is promoted in place and becomes immutable.
  3. Bump when a GA provide is refused with 409: the body carries the proposed version; set it in the spec and republish.
  4. Deprecate an element in the spec before removing it; a removal of a deprecated element is not breaking.
  5. Keep old GA versions: they serve pinned Consumers until those move their pin. The graph shows who still depends on what.

The rules of a version line#

The version-line rules A version line holds 1.0.0 GA, 1.1.0 GA and 1.2.0 snapshot. Six incoming provides and their verdicts: 1.1.0 GA with the same bytes is a no-op; 1.1.0 GA with different content is refused (immutable_ga) with the next free number proposed by what changed; 1.1.0 as a snapshot is refused (snapshot_on_ga); 1.2.0 GA promotes the snapshot in place; 1.3.0 GA that is breaking relative to 1.1.0 is refused (breaking_without_major) with 2.0.0 proposed; 1.2.0 snapshot again overwrites the snapshot. The line of orders (openapi) today 1.0.0 GA · immutable 1.1.0 GA · latest GA 1.2.0 snapshot next free on a 409 An incoming provide What Sanshain answers 1.1.0 ga same bytes no-op: nothing stored, changes all zero 1.1.0 ga different content 409 immutable_ga proposes the next free number by impact: 1.1.1 patch · 1.2.1 minor · 2.0.0 major (1.2.0 is taken, so the minor skips to 1.2.1) 1.1.0 snapshot 409 snapshot_on_ga a released number never carries a snapshot again 1.2.0 ga promotion: 1.2.0 becomes GA in place 1.3.0 ga breaking vs 1.1.0 409 breaking_without_major publish as 2.0.0 instead 1.2.0 snapshot again overwrite: last writer wins, audited
What a provide meets on the line. Every 409 names the number to publish as instead.

1. Iterating on snapshots#

While an API is in flux, provide it as a snapshot, which is the default: every build publishes a snapshot unless the ga switch is set (how stability is decided).

2. Going GA#

When the version is ready, provide the same number with stability: ga:

HTTP/1.1 403 Forbidden
{ "error": "publishing GA for 'orders' requires the 'releaser' role — publish as a snapshot, or ask an administrator to grant the role" }

3. Choosing the next version: the server proposes#

You never guess the bump. A provide is refused with 409 Conflict and a proposed_version when:

The three messages, verbatim:

{ "error": "version 1.1.0 of openapi 'orders' is GA and immutable, but the submitted content differs — did you forget to increment info's version? Publish as 1.2.1 instead",
  "proposed_version": "1.2.1" }

{ "error": "version 1.1.0 of openapi 'orders' is GA — a released number can never carry a snapshot again; bump your version to 1.2.1",
  "proposed_version": "1.2.1" }

{ "error": "version 1.3.0 of openapi 'orders' is breaking relative to GA 1.1.0 but does not bump the major — publish as 2.0.0 instead: <what broke>",
  "proposed_version": "2.0.0" }

When the bytes differ but no endpoint the splitter sees changed, the first two messages add (no endpoint content changed — the difference may be whitespace or comments; documents are compared byte-for-byte after line endings and a leading BOM are normalized). Line endings and a BOM are not content; whitespace, indentation and comments are.

The breaking-change classifier covers all three API types:

Elements marked deprecated in the previous GA are exempt: removing them is not breaking. Pinned Consumers are never broken by a new version; the checks keep the numbers honest so that a Consumer reading 2.4.0 → 2.5.0 can trust the upgrade.

4. Deprecation and migration#

Once a new major is GA, move Consumers off the old elements:

5. Removal and retirement#

Removing an element is always two GA releases within the line:

  1. Publish a version with the deprecation marker: an ordinary compatible update, a minor bump.
  2. Publish a later version without the element. Because it was deprecated in the previous GA, the removal is not breaking, so a minor bump suffices.

Old GA versions stay available by default; a pin never rots out from under a Consumer. The escape hatch is the audited delete-version action on the Producer page (admins and the Producer's maintainers): it frees the number, Consumers still pinned to it fail 404 on their next require, and a sanshain-branch that references it shows a dangling edge until the number is re-provided. The confirmation lists both before asking. Dropping a whole protocol family is a different act, retiring.

6. Multi-producer topics: AsyncAPI message contracts#

REST paths belong to one service; Kafka topic names live in a global namespace, and an audit log or a dead-letter queue legitimately has many producers. Sanshain therefore tracks AsyncAPI compatibility per message, not per channel. On a GA provide, every named publish message registers a message contract keyed by channel and message name:

Contracts are enforced on GA provides only; snapshots are never contract-checked. Requiring the owner's snippet and $ref-ing it from your own spec keeps a consumer's copy aligned. Sanshain reads 2.x publish/subscribe from the application's perspective (the convention); only publish/send registers a contract.

Subscribe operations become consumer edges#

The mirror of publishing is subscribing. When a provided AsyncAPI document has subscribe (2.x) or receive (3.x) operations, Sanshain harvests them on every provide as version-less consumer edges, so a channel your own spec says you read needs no hand-written require. Each harvested subscription:

Dropping a subscribe from a later provide retracts its edge. A subscription is never a pin and never registers a contract.

Summary#

Situation Stability Result Action
Any change, new or same snapshot number snapshot accepted iterate; overwrites are audited
Same GA number, identical content GA accepted, no-op none
Same GA number, different content GA 409 immutable_ga republish as proposed_version
Breaking change without a major bump GA 409 breaking_without_major republish as the proposed major
Snapshot for a GA number snapshot 409 snapshot_on_ga use the proposed next number
GA without the releaser role GA 403 ga_requires_releaser publish a snapshot, or get the role
Removing a deprecated element GA accepted, minor deprecate first, remove one GA later
Owned message breaks (AsyncAPI) GA 409 message_contract_conflict widen your own message only
Co-publish with a different schema (AsyncAPI) GA 409 message_contract_conflict match the owner's schema or rename

The code in the third column is the rule of the provide.rejected webhook event.

Checked against v2.6.0.