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.
The core insight
Section titled “The core insight”Most versioning systems answer “what version should this be?” at the moment of release. That creates two problems:
- Context is lost. The person releasing may not be the person who made the change. The commit message is gone. The intent is unclear.
- 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.
The .contractual/ directory
Section titled “The .contractual/ directory”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| Path | Purpose |
|---|---|
versions.json | The 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. |
What a changeset file looks like
Section titled “What a changeset file looks like”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 consumersare 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.
Why changesets beat the alternatives
Section titled “Why changesets beat the alternatives”vs Conventional Commits
Section titled “vs Conventional Commits”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:
| Problem | Conventional Commits | Changesets |
|---|---|---|
| Multiple contracts in one commit | No way to say “feat for orders-api, fix for order-schema” | Frontmatter supports multiple contracts |
| Batching releases | Every commit is a release decision | Changesets accumulate; release when ready |
| Human-readable changelog | Tools generate from commit messages | Body is already human-readable markdown |
vs Manual versioning
Section titled “vs Manual versioning”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
vs Tag-based versioning
Section titled “vs Tag-based versioning”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)
The override mechanism
Section titled “The override mechanism”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.
Multiple changesets per contract
Section titled “Multiple changesets per contract”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: patchResult: orders-api gets a major bump. All three changeset bodies are concatenated into a single changelog entry.
Next steps
Section titled “Next steps”- Usage: CLI commands for creating and consuming changesets
- Changeset Format: Full file format reference