Skip to content

Versioning

A changeset is a small markdown file that records which contracts are changing and at what semver severity. Contractual generates them automatically. Review and optionally edit them. The release step consumes them.


Most versioning systems answer “what version should this be?” at the moment of release. That creates two problems:

  1. Context is lost. The person releasing may not be the person who made the change. The commit message is gone. The intent is unclear.
  2. Batching is harder. Each release must be decided independently. Multiple changes to multiple contracts require multiple manual decisions.

Changesets flip the model: the person who makes the change declares its severity at the time they make it. The release step simply adds up the declarations.


Running contractual init creates this directory at the repo root. Every file inside should be committed. It is the source of truth for contract versions and history.

.contractual/
├── versions.json # Current semver version for every contract
├── changesets/ # One markdown file per unreleased change
│ ├── fuzzy-lion-dances.md
│ └── silver-hawk-runs.md
└── snapshots/ # Point-in-time copies of each spec
├── orders-api/
│ ├── 1.0.0.yaml
│ └── 1.1.0.yaml
└── order-schema/
├── 1.0.0.json
└── 1.1.0.json
PathPurpose
versions.jsonThe canonical version for every contract. Updated by contractual version.
changesets/Markdown files describing unreleased changes. Consumed and deleted by contractual version.
snapshots/Immutable copies of a spec at a released version. Used as the baseline for breaking change detection.

Changeset files live in .contractual/changesets/. Each file has a name like fuzzy-tiger-runs.md (adjective-noun-verb). The format is YAML frontmatter plus a markdown body:

---
"orders-api": minor
"order-schema": patch
---
## orders-api
Added optional `shipping_address` field to the Order object. Existing consumers
are unaffected. The field is not required and has a default of `null`.
## order-schema
Updated description of the `amount` field to clarify it represents cents, not dollars.

The frontmatter declares the semver level for each affected contract. The body is freeform markdown that becomes the changelog entry.


Conventional Commits encode the bump intent in the commit message (feat:, fix:, BREAKING CHANGE:). This works well for single packages. It breaks down for contracts:

ProblemConventional CommitsChangesets
Multiple contracts in one commitNo way to say “feat for orders-api, fix for order-schema”Frontmatter supports multiple contracts
Batching releasesEvery commit is a release decisionChangesets accumulate; release when ready
Human-readable changelogTools generate from commit messagesBody is already human-readable markdown

Manually editing versions.json before each release:

  • Forgotten when developers are moving fast
  • No enforced changelog entry
  • No structural check that the declared severity matches actual changes

Tagging commits (v1.2.0) with no intermediary step:

  • No way to accumulate changes across multiple PRs
  • No changelog without additional tooling
  • No connection between the tag and specific structural changes

How Contractual extends the changeset model

Section titled “How Contractual extends the changeset model”

The original Changesets library (for npm packages) is deliberately blind to what changed. It trusts the human to declare the right bump. That works for code packages where “breaking” is subjective.

Schemas are different. Contractual can structurally diff two schema versions and classify changes automatically.

Contractual auto-generates the changeset frontmatter from detected changes. This provides:

  • Machine accuracy for unambiguous changes (field removal is always breaking)
  • Human judgment for context (a major change in a pre-1.0 contract might be minor in practice)

Every auto-generated changeset can be edited before the PR merges. If Contractual classifies a change as major but it is safe, change the frontmatter:

---
"orders-api": minor # was: major; field removed but no consumers use it
---

The body should document why the override is appropriate. This becomes part of the changelog and the audit trail.


When multiple PRs modify the same contract before a release, each generates its own changeset. At release time, contractual version picks the highest bump declared across all changesets:

.contractual/changesets/
fuzzy-tiger-runs.md → orders-api: minor
bright-stone-falls.md → orders-api: major
calm-river-flows.md → orders-api: patch

Result: orders-api gets a major bump. All three changeset bodies are concatenated into a single changelog entry.