Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Configuration

Repository configuration lives in monochange.toml.

JSON Schema

A JSON Schema for editor support is published with the book at https://monochange.github.io/monochange/schemas/monochange.schema.json. That URL is the moving “current” alias for the latest docs. Stable generated copies use public schema-version suffixes, starting with https://monochange.github.io/monochange/schemas/monochange.v0.1.schema.json.

Schema-aware TOML editors such as Taplo can opt in with a comment directive at the top of monochange.toml:

#:schema https://monochange.github.io/monochange/schemas/monochange.schema.json

The same file is also available from GitHub raw content at https://raw.githubusercontent.com/monochange/monochange/main/docs/src/schemas/monochange.schema.json. Regenerate committed schema assets with schema:update and verify them with schema:check; lint:all runs the check in CI.

Shared documentation

This book is maintained with mdt so shared content blocks stay synchronized across pages.

  • Shared blocks live in .templates/*.t.md
  • Consumer files include them with <!-- {=templateName} --> directives
  • Run mdt update (or docs:update in this repository) after changing any template or consumer block
  • Run mdt check (or docs:check) before opening a PR to verify synchronization

When you edit a template such as .templates/cli-steps.t.md, the changes propagate to every documentation file that references it. This keeps the book, readmes, and inline help consistent without manual copying.

Defaults

[defaults]
# Severity added to a dependent package when this package changes.
# `bump_propagation` on a package or group overrides this floor.
parent_bump = "patch"
# Parsed and validated, but discovery reports private packages either way.
include_private = false
# Warn when group members carry different current versions.
warn_on_group_mismatch = true
# Conflicting explicit `version` entries across changesets: warn and pick the
# highest (false, the default) or fail planning outright (true).
strict_version_conflicts = false
# Ecosystem for [package.*] tables that omit `type`.
package_type = "cargo"

[defaults.changelog]
# `{{ path }}` is replaced with each package path.
path = "{{ path }}/changelog.md"
# `keep_a_changelog` or `monochange`.
format = "keep_a_changelog"

Packages

Declare every release-managed package explicitly.

[defaults]
package_type = "cargo"

[defaults.changelog]
path = "{{ path }}/changelog.md"
format = "keep_a_changelog"

[package.sdk-core]
path = "crates/sdk_core"
versioned_files = [
	# A bare string infers the package ecosystem (`cargo` here).
	"Cargo.toml",
	# An explicit entry names the ecosystem, and can update another package's
	# manifest when `name` identifies it.
	{ path = "crates/sdk_core/extra.toml", type = "cargo" },
]
# Skip the Git tag for this package.
tag = false
# Skip the provider release for this package.
release = false
# Default tag shape, rendering `sdk-core/v1.2.3`.
version_format = "namespaced"

[package.sdk-core.changelog]
# Override the default changelog path and format for this package only.
path = "crates/sdk_core/CHANGELOG.md"
format = "monochange"

Bump propagation to dependents

Packages, groups, and the defaults section declare what a target’s own changes mean for the packages that depend on them via bump_propagation:

[defaults]
# Workspace-wide fallback for dependents with no package or group declaration.
bump_propagation = "inherit"
bump_propagation_max = "major"

[package.core]
# Dependents match this package's release severity, never exceeding `minor`.
bump_propagation = "inherit"
bump_propagation_max = "minor"

[package.tooling]
# Dependents always receive at least a minor bump from this package's changes.
bump_propagation = "minor"

[package.leaf]
# Dependents never release because of this package.
bump_propagation = "none"

[group.sdk]
# A group declaration applies to members that declare nothing.
packages = ["core"]
bump_propagation = "major"
  • inherit matches the target’s own release severity: a breaking change in the package means breaking changes for its dependents. bump_propagation_max clamps the inherited severity (only valid with inherit).
  • A fixed severity (none, patch, minor, major) is a floor: dependents release at least that severity whenever the package releases. none disables dependency propagation entirely.
  • Declarations resolve most-specific-first: a package declaration overrides its group’s declaration, which overrides [defaults].bump_propagation. Targets matching no declaration fall back to the legacy [defaults].parent_bump floor. Semantic compatibility evidence can still escalate beyond the declared floor.

Required fields:

  • path
  • type, unless [defaults].package_type is set

Supported type values:

  • cargo
  • npm
  • deno
  • dart
  • flutter
  • python

Optional package fields:

  • type, when [defaults].package_type is set
  • bump_ceiling
  • changelog
  • classification_enforced
  • empty_update_message
  • publish
  • versioned_files
  • tag
  • release
  • version_format

version_format controls the Git tag identity used for package and group releases. It defaults to namespaced when no configuration is set, which produces collision-safe tags like my-package/v1.2.3. Set it to primary for the single top-level release identity that should use tags like v1.2.3. You can also provide a custom tag template with {{ name }}, {{ version }}, and {{ ecosystem }}:

[package.cli]
path = "crates/cli"
type = "cargo"
version_format = "{{ ecosystem }}/{{ name }}/v{{ version }}"

Custom formats must include {{ version }}, render to valid Git tag names without whitespace or other invalid ref characters, and must not collide with another release owner for the same sample version. If several packages share a custom format, include {{ name }} so the generated tags remain unique.

version_source controls where release planning reads the package’s current release version from. The default manifest reads the version field from the package manifest. Set version_source = "tag" to resolve the baseline from the latest reachable release tag matching the owner’s version_format. This is useful when the manifest carries no version, such as GitHub Actions repositories, or when the tag is the release identity:

[package.web]
path = "."
type = "npm"
version_source = "tag"
initial_version = "0.1.0"

initial_version is the baseline used when no matching release tag exists yet; without it, a tag-versioned package with no tag produces a warning and no release target.

floating_tags declares moving tag aliases that tag-release force-moves to every non-prerelease release tag, such as v1.2, v1, or latest:

[package.cli]
path = "crates/cli"
type = "cargo"
version_format = "primary"
floating_tags = ["v{{ major }}.{{ minor }}", "v{{ major }}"]

Alias templates support {{ major }}, {{ minor }}, {{ patch }}, and the version_format variables ({{ version }}, {{ name }}, {{ ecosystem }}). Floating tags are skipped for prereleases, never receive provider releases, and are excluded from baseline and previous-tag resolution.

Classification policy

bump_ceiling and classification_enforced decouple change classification from what a package is allowed to release.

  • bump_ceiling caps the severity classification may propose for the package. It clamps the proposed changeset bump, the enforceable minimum, and the release floor to the ceiling, and never raises a smaller bump.
  • classification_enforced = false makes classification advisory for the package. The proposal still appears in reports and change-classification comments, but monochange changeset validate --api never fails on its behalf.

Both fields resolve most-specific-first: a package declaration overrides its group’s declaration, and a group declaration applies to members that do not declare their own. Set the policy on a group when a whole family of packages shares it.

[group.main]
packages = ["cli", "docs-site"]

[package.docs-site]
path = "packages/docs-site"
type = "npm"
# Prose-only package: keep the advisory proposal at patch and never block a
# release on classification.
bump_ceiling = "patch"
classification_enforced = false

changelog accepts three forms on packages:

  • true → use {{ path }}/CHANGELOG.md
  • false → disable the package changelog
  • "some/path.md" → use that exact path

[defaults].changelog also accepts three forms:

  • true → default every package to {{ path }}/CHANGELOG.md
  • false → default every package to no changelog
  • "{{ path }}/changelog.md" or another pattern → replace {path} with each package path

A package-level changelog value overrides the default for that package.

The table form also accepts initial_header. monochange renders this Markdown only when a changelog file is created from empty content. Existing changelog preambles are preserved and are not rewritten on later releases. If initial_header is omitted or blank, monochange uses the selected format’s built-in header: keep_a_changelog gets the Keep a Changelog/SemVer preamble, and monochange gets the monochange-managed preamble. Package and group changelog tables can override the default header.

[defaults.changelog]
path = "{{ path }}/changelog.md"
format = "keep_a_changelog"
initial_header = """
# Changelog

All notable changes to this project will be documented in this file.

This changelog is managed by [monochange](https://github.com/monochange/monochange).
"""

initial_header templates can use release context such as {{ monochange_version }}, {{ config_path }}, {{ monochange_config_path }}, {{ workspace_root }}, {{ changelog_path }}, {{ changelog_format }}, {{ package }}, {{ package_name }}, {{ package_id }}, {{ package_path }}, {{ group }}, {{ group_name }}, {{ group_id }}, {{ member_count }}, {{ members }}, {{ release_owner }}, {{ release_owner_kind }}, {{ version }}, {{ new_version }}, and {{ current_version }}.

empty_update_message lets changelog targets render a readable fallback entry when a version update is required but no direct release notes were recorded for that target. This is especially useful for grouped packages that keep their own changelog entries even when only another member of the group changed.

empty_update_message can be set on:

  • [defaults]
  • [package.<id>]
  • [group.<id>]

Per-package changelog overrides ([package.<id>.changelog]) can also customize sections:

  • [defaults]
  • [package.<id>]
  • [group.<id>]

Defaults are inherited by packages and groups; package/group definitions append target-specific sections on top of the workspace defaults.

Template placeholders may include:

  • {{ package }} / {{ package_name }}
  • {{ package_id }}
  • {{ group }} / {{ group_name }}
  • {{ group_id }}
  • {{ version }} / {{ new_version }}
  • {{ current_version }} / {{ previous_version }}
  • {{ bump }}
  • {{ trigger }}
  • {{ ecosystem }}
  • {{ release_owner }} / {{ release_owner_kind }}
  • {{ members }} / {{ member_count }} for group changelogs
  • {{ reasons }}

Fallback order:

  • package changelog entries: package → group → defaults → built-in message
  • group changelog entries: group → defaults → built-in message

The built-in grouped-package fallback reads:

No package-specific changes were recorded; {{ package }} was updated to {{ version }} as part of group {{ group }}.

Prerelease mode

Enable [prerelease] when you want repeatable alpha/rc/binary prerelease builds before the final stable release. Prerelease mode writes SemVer prerelease versions into manifests by default, keeps changesets for the later stable release, skips changelog file updates by default, and does not publish packages unless explicitly enabled.

[prerelease]
enabled = true
channel = "alpha"
numbering = "increment" # increment | date | datetime
base = "planned" # planned | current-stable | fixed
base_version = "0.0.0" # required when base = "fixed"
branches = [
	"next",
	"prerelease/*",
] # optional; overrides [source.releases].branches for tag/publish checks while enabled

write_manifests = true
keep_changesets = true
changelog = false
release_notes = true
publish_packages = false

Base strategies:

  • planned: compute the next stable version from changesets and dependency propagation, then append the prerelease suffix.
  • current-stable: use the current/original stable manifest version as the prerelease base.
  • fixed: use base_version, which is useful for binary/nightly workflows such as 0.0.0-alpha.0.

When no changesets exist, prerelease mode still synthesizes release decisions from discovered packages and version groups. Repeated prerelease runs persist state in .monochange/prerelease-state.json so a series advances from alpha.0 to alpha.1 without repeatedly reapplying the same stable bump. Set branches when prerelease tag/publish workflow steps should be allowed from a different branch set than stable releases. Disable prerelease mode for the final stable release; successful stable preparation removes the state file. If prerelease mode is disabled and .monochange/prerelease-state.json is still present, validation/check fails so stale prerelease state is not ignored.

Prerelease release notes

release_notes = true (the default) renders hosted release notes for each prerelease even though changelog = false skips changelog file updates. Notes come from the same configured [changelog.outputs] artifacts that stable releases use, so [source.releases].changelog_output selects the prerelease body too.

Because keep_changesets = true leaves earlier changeset files in place, each prerelease reports only the changesets that were added since the previous prerelease. A changeset already covered by an earlier prerelease in the same series is omitted from later notes, and editing the body of an already reported changeset does not make it reappear. The already reported changesets are tracked in .monochange/prerelease-state.json under release_note_changesets.

Two situations restart the series and present every pending change again:

  • Changing channel, so the first beta prerelease does not look like an empty delta after a series of alpha prereleases.
  • Removing .monochange/prerelease-state.json, which makes the next prerelease the first of a new series.

keep_changesets = false consumes the changesets instead, so the delta is naturally empty after the first prerelease and release_notes has nothing left to report.

Prerelease versions and floating tags

A prerelease version never moves a floating_tags alias. Aliases such as v1 or latest keep pointing at the newest stable release commit while a prerelease series is active, and only a stable release repoints them.

Switching channel also restarts the increment sequence. With numbering = "increment", a series at 1.1.0-alpha.4 becomes 1.1.0-beta.0 after switching to beta rather than continuing at beta.5.

Rust semantic compatibility

Rust packages can opt into cargo-semver-checks when change classify runs at the semantic detection level:

[ecosystems.cargo.semver_checks]
enabled = true
timeout_seconds = 300

[[ecosystems.cargo.semver_checks.matrix]]
name = "default"
feature_mode = "default"

[[ecosystems.cargo.semver_checks.matrix]]
name = "all-features"
feature_mode = "all"

[[ecosystems.cargo.semver_checks.matrix]]
name = "linux-no-defaults"
feature_mode = "none"
features = ["server"]
target = "x86_64-unknown-linux-gnu"

Each matrix cell defines one supported feature and compilation-target contract. Feature mode accepts default, all, none, or heuristic. features applies to both endpoints; baseline_features and current_features support intentional feature renames and migrations. Cell names must be unique. A matrix has 1–16 cells, and timeout_seconds is 1–1800 seconds per cell.

Install cargo-semver-checks and every configured Rust target before classification. The analyzer reports failures as incomplete evidence and retains monochange’s conservative syntax result. It never silently treats a missing tool or failed target build as compatible.

cargo install cargo-semver-checks --locked
rustup target add x86_64-unknown-linux-gnu
monochange change classify --detection-level semantic --format json

cargo-semver-checks executes Cargo builds, including build scripts and procedural macros, from both comparison endpoints. Use least-privilege CI credentials and do not run this analysis for untrusted pull-request code through pull_request_target.

Package publishing

Built-in package publishing is configured through publish on packages and ecosystems.

[ecosystems.npm.publish]
enabled = true
mode = "builtin"
registry = "npm"
trusted_publishing = true

[ecosystems.npm.publish_order]
dependency_fields = ["dependencies", "devDependencies", "peerDependencies", "catalogDependencies"]

[ecosystems.npm.publish.trusted_publishing]
workflow = "publish.yml"
environment = "publisher"

[ecosystems.npm.publish.attestations]
require_registry_provenance = true

[package.web.publish]
mode = "builtin"

[package.web.publish.placeholder]
readme_file = "docs/web-placeholder.md"

[package.legacy.publish]
trusted_publishing = false

Supported fields:

  • enabled - include this package in managed publishing
  • mode - builtin or external. When builtin (the default), monochange’s built-in publisher handles release publishing. When external, monochange skips the package during release publishing (PublishPackages): your own CI or scripts handle release publishing instead. The mode setting does not affect placeholder publishing (PlaceholderPublish), which processes all packages with publish.enabled = true.
  • registry - public registry override for the package ecosystem
  • trusted_publishing - true/false or a table with enabled, repository, workflow, and environment
  • attestations.require_registry_provenance - require registry-native package provenance when the selected registry/provider capability supports it
  • rate_limits.enforce - block built-in publish runs when the selected package set exceeds a known single registry window
  • fail_on_duplicate - fail the publish step when a version is already published on the registry instead of skipping it (default: false); the built-in publish-packages step exposes the same policy as the --fail-on-duplicate CLI input for a single run
  • timeout.timeout_seconds - maximum seconds a single package publish command may run before it is killed and retried; set to 0 to disable the timeout (default: 300)
  • timeout.retries - number of times to retry a publish command that times out before reporting the package as failed (default: 2)
  • placeholder.readme - inline placeholder README content
  • publish_order.dependency_fields - ecosystem-level dependency fields used to topologically order package publishes
  • placeholder.readme_file - workspace-relative file to use as placeholder README content

Inheritance flows from [ecosystems.<name>.publish] to matching packages, and package-level values override the inherited ecosystem defaults. Configure shared trusted-publishing, attestation, and context policy on the ecosystem, then use package-level publish settings for opt-outs or package-specific workflows.

Built-in publishing targets only the canonical public registry for each supported ecosystem:

  • Cargo → crates.io
  • npm packages → npm
  • Deno packages → jsr
  • Dart / Flutter packages → pub.dev
  • Python packages → pypi
  • Go modules → go_proxy via VCS tags

Private registries and custom publication flows are still external. For those packages, set mode = "external" and handle release publication outside monochange. Placeholder publishing (monochange step placeholder-publish) still works for external-mode packages because it is a bootstrap utility, not a release publishing step.

Placeholder publishing

monochange step placeholder-publish exists for the bootstrap case where a package must already exist in the registry before you can finish automation setup such as trusted publishing.

Placeholder publishing works for all packages with publish.enabled = true, including those set to publish.mode = "external". The mode field controls who handles release publishing (monochange’s built-in publisher vs your own CI/scripts); it does not affect placeholder publishing because that is a one-time bootstrap utility, not a release step. To opt out of placeholder publishing entirely, set publish.enabled = false.

For each publishable package, monochange:

  • checks whether the package already exists in its configured public registry
  • skips packages that already exist
  • publishes a placeholder package only for packages that are missing
  • uses version 0.0.0
  • renders a default placeholder README unless placeholder.readme or placeholder.readme_file overrides it

placeholder.readme and placeholder.readme_file are mutually exclusive. If both are set, config validation fails.

Publish order dependency fields

publish_order.dependency_fields controls which manifest dependency fields create publish-order edges for an ecosystem. npm defaults to dependencies and devDependencies, so peer packages do not block publishing unless opted in. Cargo defaults stay dependencies, dev-dependencies, and build-dependencies. Deno defaults to dependencies and imports, Dart/Flutter default to dependencies and dev_dependencies, Python defaults to dependencies, and Go defaults to require. Optional Python extras (optional-dependencies) and Poetry groups (group.dependencies) only affect publish order when you opt in.

[ecosystems.npm.publish_order]
# Add peer and custom package.json fields.
dependency_fields = ["dependencies", "devDependencies", "peerDependencies", "catalogDependencies"]
[ecosystems.npm.publish_order]
# Or remove devDependencies from publish ordering.
dependency_fields = ["dependencies"]
[ecosystems.python.publish_order]
# Include optional dependency groups in Python publish ordering.
dependency_fields = ["dependencies", "optional-dependencies", "group.dependencies"]
[ecosystems.go.publish_order]
# An empty list disables Go require-based publish ordering.
dependency_fields = []

The same resolved policy is used by monochange step plan-publish-rate-limits and monochange step publish-packages.

Trusted publishing

trusted_publishing lets you tell monochange that package publication is expected to come from a verified GitHub Actions context.

[ecosystems.npm.publish]
trusted_publishing = true

[ecosystems.npm.publish.trusted_publishing]
repository = "owner/repo"
workflow = "publish.yml"
environment = "publisher"

[package.cli.publish.trusted_publishing]
workflow = "publish-cli.yml"

[package.legacy.publish]
trusted_publishing = false

When trusted_publishing is enabled:

  • npm package publishing must run from a verifiable CI/OIDC identity and must not use long-lived npm token environment variables
  • npm trusted-publisher enrollment is manual or external: monochange can render the expected npm trust github ... repair command and verify the GitHub workflow context, but monochange step publish-packages does not run npm trust automatically
  • trusted npm publishing uses the npm CLI directly; pnpm workspaces still use pnpm for non-trusted npm publishing paths
  • Cargo, jsr, pub.dev, and PyPI also require manual trusted-publishing setup; monochange reports the setup URL and blocks built-in release publishing until trust is configured

Attestation policy

publish.attestations.require_registry_provenance is separate from publish.trusted_publishing. Trusted publishing must be enabled first, then the attestation policy tells monochange to require registry-native package provenance where the selected registry and CI provider support it.

[ecosystems.npm.publish]
trusted_publishing = true

[ecosystems.npm.publish.attestations]
require_registry_provenance = true

[package.legacy.publish.attestations]
require_registry_provenance = false

monochange treats npm provenance and JSR package provenance as enforceable built-in registry provenance. PyPI PEP 740 attestations are modeled in the capability matrix, but require_registry_provenance is rejected for PyPI until the built-in Python publisher exposes a publish command that can require uploading those attestations. crates.io, pub.dev, Go proxy publishing, and custom registries are also rejected when this requirement is enabled because monochange cannot verify equivalent registry-native package attestations for those flows.

GitHub release asset attestations are a separate release policy under [source.releases.attestations] and are valid only for the GitHub source provider:

[source.releases.attestations]
require_github_artifact_attestations = true

For a GitHub-focused setup guide with exact registry fields, commands, and workflow requirements, see Trusted publishing and OIDC. For monorepo workflow and tag-shape recommendations, see Multi-package publishing patterns.

monochange resolves the GitHub trust context from:

  • explicit repository, workflow, and environment values in config
  • otherwise [source] plus GitHub Actions environment such as GITHUB_WORKFLOW_REF and GITHUB_JOB
  • and, when possible, the workflow job environment declared in .github/workflows/<file>.yml

If monochange cannot determine the GitHub repository or workflow for an npm package, it cannot render a precise npm trust github ... repair command or verify the expected GitHub context.

Implementation limits

The built-in package publishing flow is intentionally narrow:

  • no private or custom registry support in mode = "builtin"
  • rate-limit planning can batch work and enforce single-window safety, but monochange still does not sleep across windows or requeue later batches automatically
  • registry-side trusted-publisher enrollment is still manual for every registry; npm is special only because monochange can render and verify the GitHub setup context

If your workflow needs any of these, keep the package on mode = "external" and let your own CI or scripts own publication.

For end-to-end GitHub and GitLab examples - including npm trusted publishing on GitHub and token/external-mode patterns on GitLab - see Advanced: CI, package publishing, and release PR flows.

Groups

Groups own outward release identity for their member packages.

[group.sdk]
packages = ["sdk-core", "web-sdk", "mobile-sdk"]
changelog = "changelog.md"
versioned_files = [{ path = "group.toml", type = "cargo" }]
tag = true
release = true
version_format = "primary"

Rules:

  • group members must already be declared under [package.<id>]
  • package and group ids share one namespace
  • a package may belong to only one group
  • only one package or group may use version_format = "primary"
  • custom version_format templates must include {{ version }} and render unique, valid Git tag names; include {{ name }} when sharing a template across release owners
  • group tag, release, and version_format override member package release identity
  • package changelogs and package versioned_files still apply when grouped
  • grouped packages can customize fallback changelog entries with empty_update_message when no direct package notes are present
  • [group.<id>.changelog].include can filter which member-targeted changesets appear in the group changelog without changing release planning or package changelogs

For grouped changelog filtering, use the changelog table form:

[group.sdk.changelog]
path = "docs/sdk-changelog.md"
include = ["sdk-cli"]

include accepts:

  • "all" - include direct group-targeted changesets and all member-targeted changesets (default)
  • "group-only" - include only direct group-targeted changesets
  • [] or ["package-id", ...] - include direct group-targeted changesets plus member-targeted changesets only when every target in that group is listed

Versioned files

versioned_files are additional managed files beyond native manifests.

Examples:

# package-scoped shorthand infers the package ecosystem
versioned_files = ["Cargo.toml"]
versioned_files = ["**/crates/*/Cargo.toml"]

# explicit typed entries remain available
versioned_files = [{ path = "group.toml", type = "cargo", name = "sdk-core" }]
versioned_files = [{ path = "docs/version.txt", type = "cargo" }]
versioned_files = [
	{ path = "Cargo.toml", type = "cargo", fields = ["workspace.metadata.bin.monochange.version"], prefix = "" }, # bare version, e.g. 1.2.3
]
versioned_files = [
	{ path = "package.json", type = "npm", fields = ["metadata.bin.monochange.version"] },
]

# generic format entries update explicit fields in non-ecosystem files
versioned_files = [
	{ path = "metadata.json", format = "json", fields = ["release.version"] },
	{ path = "tools.toml", format = "toml", fields = ["tool.sdk.version"] },
	{ path = "pubspec-overrides.yaml", format = "yaml", fields = ["metadata.sdkVersion"] },
	{ path = ".env", format = "env", fields = ["VERSION"] },
]

# ecosystem-level defaults inherited by matching packages
[ecosystems.npm]
versioned_files = ["**/packages/*/package.json"]

Typed manifest entries can update dependency sections and arbitrary string fields inside TOML or JSON manifests. Dependency targets in versioned_files must reference declared package ids. Groups must use explicit typed entries because monochange cannot infer a group ecosystem from a bare string.

Value templates

A versioned file can render a whole value instead of the plain version. This is how a store build number reaches a pubspec.yaml or an Expo app.json:

[[package.app.versioned_files]]
path = "pubspec.yaml"
type = "dart"
value_template = "{{ identity }}+{{ build }}"

The template sees the full variable namespace, including values declared with [package.<id>.values.<id>]. See Release values and version schemes.

A package’s own ecosystem manifest must stay a plain SemVer, so value_template on that path rejects calendar, ordinal, and counter variables with a configuration error. Move store values into a separate versioned file, or set version_source = "tag" when the manifest cannot carry a SemVer version at all.

Dependency prefixes

Typed entries write internal dependency references with a range prefix. Set prefix on an entry to control it exactly. Accepted values are "^", "~", ">=", "=", "v", or "" for a bare version:

versioned_files = [
	# write internal npm dependencies as tilde ranges, e.g. "~1.2.3"
	{ path = "package.json", type = "npm", fields = ["dependencies"], prefix = "~" },
]

Resolution order for the prefix:

  1. the entry’s prefix
  2. [ecosystems.<type>] dependency_version_prefix (see the [ecosystems.*] reference in Ecosystems)
  3. the ecosystem default: ^ for npm, deno, and dart; >= for python; v for go; empty for cargo

The prefix applies to internal dependency references only. The package’s own version field is written without it. format entries ignore prefix and always write the bare version, and regex entries cannot set prefix. monochange versions sync --strategy uses its own fixed per-ecosystem prefixes and ignores dependency_version_prefix; see Internal dependency versions for that table.

Format versioned files

Use format when a version lives in a structured or key/value file that should not receive ecosystem-specific dependency handling. Supported values are json, toml, yaml, yml, and env.

[package.core]
path = "crates/core"
versioned_files = [
	{ path = "metadata.json", format = "json", fields = ["release.version"] },
	{ path = ".env", format = "env", fields = ["VERSION"] },
]

Key rules:

  • format entries cannot set type or regex
  • fields is required and must name every value to update; monochange does not infer ecosystem defaults in format mode
  • JSON, TOML, YAML, and YML fields use dot-separated object/table paths such as release.version
  • env fields use exact keys such as VERSION and update existing KEY=value or export KEY=value lines
  • field names can include {{ name }} and {{ version }} placeholders for simple context-aware paths or keys

Regex versioned files

Regex entries let you version-stamp any plain-text file, such as README badges, download links, or install scripts, without needing an ecosystem-specific parser. The regex must contain a named version capture group; monochange replaces the captured substring with the new version while preserving the surrounding text.

[package.core]
path = "crates/core"
versioned_files = [
	# update a download link in the README
	{ path = "README.md", regex = 'https://example\.com/download/v(?<version>\d+\.\d+\.\d+)\.tgz' },
	# update a version badge
	{ path = "README.md", regex = 'img\.shields\.io/badge/version-(?<version>\d+\.\d+\.\d+)-blue' },
]

[group.sdk]
packages = ["core", "cli"]
versioned_files = [
	# update the install script across all packages (glob pattern)
	{ path = "**/install.sh", regex = 'SDK_VERSION="(?<version>\d+\.\d+\.\d+)"' },
]

[ecosystems.cargo]
versioned_files = [
	# update a workspace-wide version constant
	{ path = "crates/constants/src/lib.rs", regex = 'pub const VERSION: &str = "(?<version>\d+\.\d+\.\d+)"' },
]

Key rules:

  • regex entries cannot set type, prefix, fields, or name: they operate on raw text
  • the regex must include a (?<version>...) named capture group
  • the path field supports glob patterns (e.g. **/README.md)
  • regex entries work on packages, groups, and ecosystem-level versioned_files

Lockfile commands

By default monochange rewrites supported lockfiles directly from the release plan. That keeps normal monochange run release runs close to --dry-run speed instead of launching package managers just to rewrite workspace version strings.

Built-in direct lockfile updates cover:

  • Cargo: Cargo.lock
  • npm-family: package-lock.json, pnpm-lock.yaml, bun.lock, and bun.lockb
  • Deno: deno.lock
  • Dart / Flutter: pubspec.lock

For Python projects, monochange infers package-manager lockfile commands instead of mutating lockfiles directly: uv.lock uses uv lock, and poetry.lock uses poetry lock --no-update. Unknown Python lockfile names are skipped rather than guessed.

If you configure lockfile_commands for an ecosystem, monochange stops using the built-in direct updater for that ecosystem and those commands fully own lockfile refresh. Use that escape hatch only when your workspace needs package-manager-side regeneration beyond version rewrites.

For Cargo specifically, monochange no longer falls back to cargo generate-lockfile automatically when a lockfile looks incomplete. That keeps monochange run release on the fast path and leaves the final dependency-resolution refresh under your control: either configure [ecosystems.cargo].lockfile_commands explicitly or run cargo generate-lockfile / cargo check yourself afterwards.

If you want to measure that tradeoff before opting into a refresh command, run the prepare_release_apply_cargo_lockfile_refresh Criterion benchmark. It compares the default direct_rewrite path against an explicit full_refresh_command run on the same synthetic Cargo workspace.

[ecosystems.npm]
lockfile_commands = [
	{ command = "pnpm install --lockfile-only", cwd = "packages/web" },
	{ command = "npm install --package-lock-only", cwd = "packages/legacy", shell = true },
]

cwd is resolved relative to the workspace root. shell = false runs the command directly, shell = true uses sh -c, and shell = "bash" uses a custom shell binary.

CLI commands

CLI workflow commands are user-defined commands that run as monochange run <command>. Each [cli.<command>] table in monochange.toml defines one workflow with its own help text, inputs, and ordered step list.

monochange init writes a minimal starter config and does not seed default [cli.*] workflow aliases. Add [cli.<command>] tables only for repository-specific workflows that need to chain multiple steps, expose custom names, or run shell Command steps.

Built-in steps are also available directly as immutable monochange step <name> commands. The binary generates those commands from the step schemas, so monochange step discover, monochange step prepare-release, monochange step affected-packages, and the other step commands do not require config entries. Use monochange step <name> in CI when you want a stable built-in operation without depending on a repository-defined wrapper.

Some top-level names are reserved for binary commands, including init, mcp, help, version, analyze, check, and step. The step command namespace is reserved for immutable built-in step commands, and run is reserved for executing configured workflows. Do not define [cli.step] or [cli.run] tables.

Explicit step input inheritance

Config-defined workflow commands have two input layers:

  1. [[cli.<command>.inputs]] declares the flags and arguments accepted by monochange run <command>.
  2. inputs on each step decides which of those parsed command inputs are visible while that step runs.

Command inputs are not inherited automatically. A step receives a command input only when the step explicitly lists it. This makes wrappers predictable when a command-level flag and a step-specific input share the same name.

Use the array shorthand when a step should inherit command inputs unchanged:

[cli.discover]
inputs = [
	{ name = "format", type = "choice", choices = ["text", "json", "json-min"], default = "text" },
]
steps = [
	{ type = "Discover", inputs = ["format"] },
]

Use the map form when a step needs fixed values, renamed values, templates, or a mixture of inherited and overridden values:

[cli.release-pr]
inputs = [
	{ name = "format", type = "choice", choices = ["text", "json", "json-min", "markdown"], default = "text" },
	{ name = "open_as_draft", type = "boolean", default = false },
]
steps = [
	{ type = "PrepareRelease", inputs = ["format"] },
	{ type = "OpenReleaseRequest", inputs = { format = "markdown", draft = "{{ inputs.open_as_draft }}" } },
]

Step-local when expressions and command templates evaluate against the same explicit step input context. If a when condition references inputs.publish, the step must include publish in its inputs array or map. Use inputs = ["publish"] for unchanged inheritance, or inputs = { publish = "{{ inputs.publish }}" } when you need the map form for other overrides.

Override values in the map form accept native TOML literals: strings, booleans, integers, and floats. Booleans stay booleans in the parsed model and are stringified to "true"/"false" when the step runs; numbers are coerced to their string form at parse time, so writing { jobs = 4, ratio = 2.5 } is exactly the same as writing { jobs = "4", ratio = "2.5" }:

[[cli.release.steps]]
name = "publish"
type = "Command"
command = "npm publish --jobs {{ inputs.jobs }}"
inputs = { jobs = 4, dry_run = true }

Interactive command steps

Add an explicit interactive boolean input to the command, pass it to a Command step, and run the workflow with --interactive when you want the command to own the terminal. The step inherits stdio for that run, so prompts and terminal UIs work, and the progress spinner is suppressed while the command runs.

[cli.publish]
help_text = "Publish packages"

[[cli.publish.inputs]]
name = "interactive"
type = "boolean"
default = false

[[cli.publish.steps]]
name = "publish"
type = "Command"
command = "npm publish"
inputs = ["interactive"]
monochange run publish --interactive

Leave interactive unset (the default) for CI and scripted runs so those commands stay non-interactive. Interactive steps do not capture output: steps.<id>.stdout and steps.<id>.stderr are empty for them, so downstream steps cannot read what an interactive command printed.

Built-in monochange step <name> commands are different: they are generated directly from the step schema, so their CLI flags map to that single step without a [cli.*] wrapper.

[changelog]
templates = [
	"#### {{ summary }}\n\n{{ details }}\n\n{{ context }}",
	"#### {{ summary }}\n\n{{ context }}",
	"#### {{ summary }}\n\n{{ details }}",
	"- {{ summary }}",
]

[package.core]
path = "crates/core"
[package.sdk-core.changelog.types]
security = { bump = "patch", section = "Security" }

[cli.discover]
help_text = "Discover packages across supported ecosystems"

[[cli.discover.inputs]]
name = "format"
type = "choice"
choices = ["text", "json", "json-min"]
default = "text"

[[cli.discover.steps]]
name = "discover packages"
type = "Discover"
inputs = ["format"]

[cli.release]
help_text = "Prepare a release from discovered change files"

[[cli.release.inputs]]
name = "format"
type = "choice"
choices = ["text", "json", "json-min"]
default = "text"

[[cli.release.steps]]
name = "prepare release"
type = "PrepareRelease"
inputs = ["format"]

[cli.publish-release]
help_text = "Prepare a release and publish provider releases"

[[cli.publish-release.inputs]]
name = "format"
type = "choice"
choices = ["text", "json", "json-min"]
default = "text"

[[cli.publish-release.steps]]
name = "prepare release"
type = "PrepareRelease"
inputs = ["format"]

[[cli.publish-release.steps]]
name = "publish release"
type = "PublishRelease"
inputs = ["format"]

[[cli.publish-release.steps]]
name = "comment released issues"
type = "CommentReleasedIssues"

[cli.release-pr]
help_text = "Prepare a release and open or update a provider release request"

[[cli.release-pr.inputs]]
name = "format"
type = "choice"
choices = ["text", "json", "json-min"]
default = "text"

[[cli.release-pr.steps]]
name = "prepare release"
type = "PrepareRelease"
inputs = ["format"]

[[cli.release-pr.steps]]
name = "open release request"
type = "OpenReleaseRequest"
inputs = ["format"]

[cli.affected]
help_text = "Evaluate pull-request changeset policy"

[[cli.affected.inputs]]
name = "format"
type = "choice"
choices = ["text", "json", "json-min"]
default = "text"

[[cli.affected.inputs]]
name = "changed_paths"
type = "string_list"
required = true

[[cli.affected.inputs]]
name = "label"
type = "string_list"

[[cli.affected.steps]]
name = "evaluate affected packages"
type = "AffectedPackages"
inputs = ["format", "changed_paths", "label"]

CLI command interpolation variables:

  • built-in command variables are available directly as {{ version }}, {{ group_version }}, {{ released_packages }}, {{ changed_files }}, and {{ changesets }}
  • command templates can read CLI inputs through {{ inputs.name }}
  • every step can override the inputs it receives with inputs = { ... }; direct references like "{{ inputs.labels }}" preserve list and boolean values when rebinding to built-in steps
  • built-in commands already attach descriptive step name labels such as prepare release and publish release; keep or replace those labels when you want progress output to stay readable
  • custom command variables become available when variables is present: map your own names to variables such as version, group_version, released_packages, changed_files, and changesets
  • always_run = true on any step causes it to run even when a previous step has failed, which is useful for cleanup, notification, or dry-run preview steps
  • update_release_json = true on a CommitRelease step allows the step to create or overwrite the release record file when it is missing or differs from the expected content; the default (false) treats a missing or mismatched record as an error
  • dry_run_command on a Command step replaces command only when the CLI command is run with --dry-run
  • dry_run = true on a [cli.<command>] table forces the entire command to run in dry-run mode even when the user does not pass --dry-run
  • shell = true runs the command through the current shell; the default mode runs the executable directly after shell-style splitting

Performance tip: keep the default monochange run release path focused on built-in steps such as PrepareRelease. Arbitrary Command steps shell out to external tools, so expensive follow-up work like formatting, validation, publishing, or pushes should usually be gated behind an explicit input such as when = "{{ inputs.commit }}" if you want local release preparation to stay sub-second.

RetargetRelease is intentionally different from PrepareRelease-driven steps. It operates from git history plus source/provider information, discovers the durable ReleaseRecord, and then exposes structured retarget.* outputs for later command steps.

See Repairable releases for when to use monochange step retarget-release versus publishing a new patch release.

Release titles

Every release renders two titles from minijinja templates: the release title becomes the provider release name (the GitHub, GitLab, Gitea, or Forgejo release heading), and the changelog version title becomes the ## heading at the top of that release’s entry in each changelog file. They answer different questions — the provider release is attached to its tag and covers one release, while a changelog file spans every version — so they are configured separately.

Each title resolves most-specific-first: release_title (or changelog_version_title) on the package or group, then [defaults].release_title (or [defaults].changelog_version_title), then a built-in default chosen by the owner’s version_format:

Owner version formatBuilt-in release titleBuilt-in changelog version title
primaryv{{ version }} ({{ date }})[{{ version }}]({{ tag_url }}) ({{ date }}) when a source is configured, otherwise {{ version }} ({{ date }})
namespaced{{ id }} v{{ version }} ({{ date }}){{ id }} [{{ version }}]({{ tag_url }}) ({{ date }}) when a source is configured, otherwise {{ id }} {{ version }} ({{ date }})

Both templates render with these variables:

  • {{ id }} — the release owner: the package or group id
  • {{ version }} — the planned version, without a v prefix
  • {{ previous_version }} — the version of the previous release tag, empty for a first release
  • {{ date }}, {{ time }}, {{ datetime }} — the release date and time
  • {{ changes_count }} — the number of changesets in the release
  • {{ tag_url }} — the URL of the release tag on the provider
  • {{ compare_url }} — the provider comparison URL between the previous tag and this one
[defaults]
# Name every release after its owner with the tag-style version.
release_title = "{{ id }} v{{ version }} ({{ date }})"

[group.sdk]
release_title = "SDK {{ version }} ({{ date }})" # override for one group

Titles are rendered once, when the release is prepared. The rendered release title is persisted in the release record, so publishing the provider release from git history (monochange step publish-release --from-ref HEAD) replays the exact prepare-time title and date; records written before schema v0.9 carry no persisted title and fall back to the built-in default for the target’s version format, dated from the record.

GitHub release settings

Use [source] plus [source.releases] when you want command steps such as PublishRelease to derive repository release payloads from the prepared release. GitHub remains the default provider when provider is omitted. Add [source.releases] to restrict tag and publish operations to commits reachable from allowed release branches; branches accepts multiple names and glob patterns such as release/*.

The [source] section configures provider integration for releases, pull requests, and changeset enforcement. GitHub is the default provider when provider is omitted.

For self-hosted instances, set api_url or host to your server’s URL. These fields must use https://. Insecure http:// schemes are rejected because API tokens would be transmitted in cleartext.

[source]
provider = "github"
owner = "ifiokjr"
repo = "monochange"
# Optional: GitHub Enterprise or a self-hosted instance.
# api_url = "https://github.company.com/api/v3"

[source.releases]
enabled = true
# Create the provider release as a draft.
draft = false
# Mark the provider release as a prerelease.
prerelease = false
# Render the release body from monochange notes.
source = "monochange"
# Restrict tag and publish operations to commits reachable from these branches.
branches = ["main", "release/*"]
# Refuse to tag from a non-matching branch.
enforce_for_tags = true
# Refuse to publish from a non-matching branch.
enforce_for_publish = true
# Allow release commits from any branch.
enforce_for_commit = false
changeset_context_timeout_seconds = 120

[source.pull_requests]
enabled = true
# Head branch for the release PR.
branch_prefix = "monochange/release"
base = "main"
title = "chore(release): prepare release"
# Optional: override the release commit subject while keeping `title` for the
# release PR title. When omitted, the commit subject falls back to `title`.
# commit_subject = "chore(release): prepare release"
labels = ["release", "automated"]
auto_merge = false
# How much of the release notes the release PR body carries.
# "full" inlines every target's notes; "summary" renders only the header,
# the target list, and the changelog paths.
body_style = "full"
# Optional: cap the rendered release PR body in characters. Defaults to the
# provider's own limit (65536 for GitHub). Notes that do not fit are dropped
# from the end and replaced with a pointer to the changelog files.
# max_body_chars = 65536

[changesets.affected]
enabled = true
# Fail the check when coverage is missing.
required = true
skip_labels = ["no-changeset-required"]
# Explain the failure on the pull request.
comment_on_failure = true
changed_paths = ["crates/**", "packages/**", "npm/**", "skills/**"]
ignored_paths = [
	"docs/**",
	"specs/**",
	"readme.md",
	"CONTRIBUTING.md",
	"license",
]

[changesets.classification]
# Labels that skip `monochange change classify` for the pull request.
skip_labels = ["release"]

Ecosystem settings

These settings are parsed from config and document intended control points for discovery:

Each key below is parsed and validated. enabled, roots, and exclude do not currently filter discovery, so treat them as declared intent rather than an active filter.

[ecosystems.cargo]
enabled = true
roots = ["crates/*"]
exclude = ["crates/experimental/*"]
# Range operator written into internal Cargo dependency references.
dependency_version_prefix = "^"
# Extra files to version-stamp in matching packages.
versioned_files = ["Cargo.toml"]
# Configuring this replaces the built-in direct lockfile rewrite for Cargo.
lockfile_commands = [{ command = "cargo generate-lockfile" }]

[ecosystems.npm]
enabled = true
roots = ["packages/*"]
exclude = ["packages/legacy/*"]
dependency_version_prefix = "^"
versioned_files = ["**/packages/*/package.json"]
lockfile_commands = [
	# `cwd` is relative to the workspace root.
	{ command = "pnpm install --lockfile-only", cwd = "packages/web" },
]

[ecosystems.deno]
enabled = true
# Deno has no inferred lockfile command.

[ecosystems.dart]
enabled = true
lockfile_commands = [{ command = "flutter pub get", cwd = "packages/mobile" }]

[ecosystems.python]
enabled = true
# Without an explicit command, `uv.lock` uses `uv lock` and `poetry.lock` uses
# `poetry lock --no-update`. Other lockfile names are skipped.
lockfile_commands = [{ command = "uv lock" }]

[ecosystems.go]
enabled = true
# monochange infers `go mod tidy` for go.mod and go.sum refreshes.
lockfile_commands = [{ command = "go mod tidy" }]

Changelog configuration

When [defaults].package_type is set, package entries may omit an explicit type.

Existing package and group changelogs support two appendable Markdown formats:

  • monochange keeps the current heading-and-bullets layout
  • keep_a_changelog renders section headings such as ### Features, ### Fixes, and ### Breaking changes

Defaults can set a repository-wide changelog path pattern and format, while package and group changelog tables can override either field.

Release-note streams and outputs

Streams separate the wording intended for different audiences without changing the changeset syntax. The built-in default stream always exists and preserves the current developer-oriented changelog behavior. A custom type opts into another stream with stream; types that omit it continue to use default.

Each changeset file resolves to exactly one stream. If one implementation needs both developer-facing detail and user-facing wording, author two small changesets and choose a type from each stream. This keeps each entry understandable on its own and prevents internal details from leaking into product notes.

[changelog.streams.user]
description = "Product release notes for app users"

[changelog.sections.native]
heading = "Native releases"
# Lower priority renders first and wins when one changeset targets several
# packages with different types.
priority = 5

[changelog.sections.app_features]
heading = "App features"
priority = 10

[changelog.types.native]
bump = "major"
section = "native"

[changelog.types.app_feature]
bump = "minor"
section = "app_features"
# Routes every changeset using this type into the `user` stream.
stream = "user"

[changelog.outputs.user]
stream = "user"
format = "json"
mode = "release"
path = "{{ path }}/release-notes/{{ version }}.json"
targets = ["app"]

[source.releases]
source = "monochange"
# Publish the `user` output as the hosted release body.
changelog_output = "user"

This example makes native a major bump in the default stream and app_feature a minor bump in the user stream. A mobile app can use that distinction to require an app-store release for native changes while letting an app_feature release ship through a patch system such as Shorebird.

Configured sections and types extend the built-in set

[changelog.sections] and [changelog.types] add to the built-in vocabulary. A declared key overrides the built-in entry of the same name; every other built-in key stays available.

The built-in types include the semantic aliases and the stream types:

TypeBumpSection
majormajorbreaking
breakingmajorbreaking
minorminorfeat
featminorfeat
changeminorchange
patchpatchfix
fixpatchfix
refactorpatchrefactor
nonenonenone
docsnonedocs
securitynonesecurity
testnonetest

Adding one custom type therefore does not remove the aliases:

[changelog.types.app_feature]
bump = "minor"
section = "app_features"

With that table, app_feature is added while minor, patch, fix, and every other built-in type still resolve. To narrow the vocabulary for one target, use excluded_changelog_types on that package or group:

[package.core]
excluded_changelog_types = ["docs"]

Named [changelog.outputs.<id>] tables support:

FieldMeaning
streamStream to render; defaults to default
targetsOne or more configured package or group ids
pathDestination template supporting {{ path }}, {{ id }}, and {{ version }}
formatmonochange, keep_a_changelog, json, or text
modeappend for a cumulative Markdown changelog or release for a standalone current-release artifact
initial_headerOptional header for append mode; invalid with release mode

JSON and text outputs must use mode = "release". The existing package/group changelog configuration is the implicit output named default; [source.releases].changelog_output selects which output becomes the hosted release body.

JSON release notes expose structured entry fields such as summary, details_markdown, packages, change_type, bump, stream, style, and provenance. They do not embed a pre-rendered Markdown entry. Text output is rendered from the same data without Markdown emphasis or link syntax, which keeps it readable in logs and shell pipelines.

Preview or export one configured artifact without preparing the release:

# stdout
monochange notes --output user --target app

# explicit file (relative paths resolve from the workspace root)
monochange notes --output user --target app --file artifacts/app-release-notes.json

--output selects the configured stream and format. It is required so automation cannot accidentally publish the wrong audience. Use --target when an output has more than one target. The command is read-only: it does not update versions, consume changesets, or write the output’s configured path. Omitting --file (or passing --file -) writes to stdout, so ordinary shell redirection also works.

Use [changelog.style] to tune rendered release-note shape. metadata_style accepts inline (the default), blockquote, plain, or omit. The inline style renders owner, review request, and issue metadata as one ·-separated paragraph; when a PR/MR link is available, commit links are omitted because the review link already identifies the change.

Routine entries use a compact bullet. Breaking entries and entries with migration guidance, code fences, or multiline details use an expanded heading and body. A package’s own release notes omit the redundant package label; group and workspace notes keep package labels so readers can see what each entry affects.

One changeset can target several packages with different change types. monochange renders that changeset once, in the section with the lowest priority, and lists every package it targeted. Each package label carries a colored symbol for the bump that package received: 🔴 major, 🟠 minor, 🟢 patch, and ⚪ none. Set package_bump_symbols = false to omit the symbols.

Affected packages are metadata about a change, so they render as a _Packages:_ line directly above the _Owner:_ line rather than inside the heading. A package’s own release notes omit the label entirely, because the document already identifies the package.

An expanded entry — a breaking change, or any change whose body contains a code block, a blank line, or migration guidance — puts the package line directly beneath its heading and above the explanation, so a reader learns what the change affects before reading it. A compact entry keeps the line beside the bullet text.

Built-in section headings are plain text, such as Features and Fixes. Configure custom [changelog.sections.<id>].heading values when a project deliberately wants emoji or other decoration.

[changelog.style]
# `inline` (default), `blockquote`, `plain`, or `omit`.
metadata_style = "inline"
# Prefix each package label with a colored bump symbol.
package_bump_symbols = true
# `inline` (default), `badge`, or `omit`.
package_label_style = "inline"
# `blank_line` (default), `thematic_break`, or `none`.
section_separator = "blank_line"

You can also customize release-note rendering with a workspace-wide [changelog] table plus per-package or per-group changelog overrides.

Supported template variables include:

VariableMeaningNotes
{{ summary }}rendered release-note summary headingalways available
{{ details }}optional long-form details bodyomitted when the changeset has no details
{{ package }}owning package id for the rendered entryuseful in shared templates
{{ version }}release version for the current targetpackage or group version
{{ target_id }}release target idpackage id or group id
{{ bump }}resolved bump severitynone, patch, minor, or major
{{ type }}changeset note typee.g. feature, fix, security; omitted when absent
{{ context }}compact default metadata blockpreferred rendered block for human-readable notes
{{ changeset_path }}source .changeset/*.md pathtracked in manifests and still available for custom templates, but not shown by default in {{ context }}
{{ change_owner }}plain-text hosted actor labelusually something like @ifiokjr
{{ change_owner_link }}markdown link to the hosted actorfalls back to plain text when no URL is available
{{ review_request }}plain-text PR/MR labele.g. PR #31 or MR !42
{{ review_request_link }}markdown link to the PR/MRfalls back to plain text when no URL is available
{{ introduced_commit }}short SHA for the commit that first introduced the changesetplain text only
{{ introduced_commit_link }}markdown link to the introducing commitpreferred for changelog output
{{ last_updated_commit }}short SHA for the most recent commit that changed the changesetonly populated when different from {{ introduced_commit }}
{{ last_updated_commit_link }}markdown link to the most recent commit that changed the changesetonly populated when different from {{ introduced_commit }}
{{ closed_issues }}plain-text list of issues closed by the linked review requesttypically #12, #18
{{ closed_issue_links }}markdown links to issues closed by the linked review requestpreferred for changelog output
{{ related_issues }}plain-text list of related issues that were referenced but not closedhost support may vary
{{ related_issue_links }}markdown links to related issues that were referenced but not closedhost support may vary

The *_link variants render markdown links when the hosting provider exposes URLs. By default {{ context }} renders the highest-value metadata for readers: owner, review request, introduced commit, last updated commit when different, and linked issues. It does not expose the transient .changeset/*.md path unless you explicitly reference {{ changeset_path }} in your template.

Release values and version schemes

Release planning tracks one version axis: a SemVer identity. Two optional surfaces add a second number and a human-facing label.

Version schemes

[version_scheme.<id>] renders a display label from calendar parts, release ordinals, and declared values. Reference one from a package with display_version:

[version_scheme.calver]
template = "{{ year }}.{{ month_padded }}.{{ release_of_month }}"

[package.app]
display_version = "calver"

Template variables:

GroupVariables
Identitymajor, minor, patch, version, identity, prerelease
Calendaryear, year_short, month, month_padded, quarter, day, date, time
Ordinalsrelease_of_month, release_of_quarter, release_of_year
Declaredevery [package.<id>.values.<id>] id, plus label

Ordinals chain from the previous release record and restart at 1 in a new month, quarter, or year. Values and labels are frozen into the release record, so re-rendering a historical release always produces the same string.

Declared values

[package.<id>.values.<id>] declares a value that becomes a template variable. Every declaration names exactly one source.

# A stamped counter in a file you create and commit.
[package.app.values.build]
file = "build.json" # { "build": 0 }
field = "build" # dot-separated paths are supported
on_release = "increment" # or { add = { amount = 10 } } or "none"
reset = "version" # "version" for iOS trains, "never" for Play/macOS

[package.app.values.artifact]
hash = "artifacts/app.aab" # sha256 over a file
encoding = "base36" # hex | base32 | base36 | digits
length = 8

[package.app.values.run]
env = "GITHUB_RUN_NUMBER"

[package.app.values.rev]
git = "commit_count" # or short_hash

[package.app.values.when]
timestamp = "commit" # or now
SourceStampedOrdering guarantee
file + field with on_releaseyesmonotonic within reset
hashnonone — an identifier
envnonone
gitnodepends on the git value
timestampnonone

Only stamped counters and ordinals are monotonic. A hash-derived value is valid in a display label, but must not be relied on for ordering; monochange treats a scheme that uses one as non-monotonic rather than pretending otherwise.

Counter files

Counter files are yours to create and commit. monochange reads the declared field and rewrites only that value, preserving the surrounding formatting and comments:

{ "build": 0 }

The first stamped release writes 1. A missing file, a missing field, or a non-integer value is a blocking error naming the path, field, and expected shape:

counter file `build.json` does not exist; create it with its starting value, for example {"build": 0}

Adopting monochange in a repository whose app already has a production build number means creating the file once with the current value.

reset = "version" restarts the counter when the identity version changes, which is Apple’s release-train rule for iOS build numbers. reset = "never" is the Google Play and macOS rule: the value only ever increases.

Re-running prepare-release for a version that already has a release record reuses the frozen values instead of stamping counters again, so a repeated run never double-increments.

Package references

Package references in changesets and CLI commands should use configured ids.

Prefer package ids when a leaf package changed. That keeps the authored change as specific as possible, and monochange will still propagate bumps to dependents and synchronize any configured groups automatically.

Use a group id only when the change is intentionally owned by the whole group and should read that way in release output.

Current status

Implementation notes:

  • [defaults].include_private is parsed and validated, but discovery reports private packages either way. Rely on include_private only for release planning, not for filtering what step discover prints.
  • [ecosystems.*].enabled, .roots, and .exclude are parsed and validated, but discovery still scans every supported ecosystem. A package found by discovery appears regardless of those settings.
  • [defaults].strict_version_conflicts controls conflicting explicit version entries across changesets. The default warns and picks the highest; setting it to true fails planning instead.
  • Source automation reads [source], provider release settings under [source.releases], pull request settings under [source.pull_requests], and affected-package policy under [changesets.affected]. GitHub is the default provider.
  • Live GitHub release and release-request publishing uses octocrab with GITHUB_TOKEN or GH_TOKEN, falling back to the authenticated GitHub CLI credential from gh auth token when neither variable is set. GitLab and Gitea use direct HTTP APIs.
  • Release-request publishing uses local git for branch, commit, and push operations before provider API updates when not in dry-run mode.
  • Changeset policy commands apply only to the GitHub provider and expect [changesets.affected], a changed_paths command input, and diagnostics formatted for GitHub Actions.
  • Supported [[cli.<command>.steps]] types are Config, Validate, Discover, DisplayVersions, CreateChangeFile, PrepareRelease, CommitRelease, VerifyReleaseBranch, PublishRelease, PlaceholderPublish, PublishPackages, PlanPublishRateLimits, OpenReleaseRequest, CommentReleasedIssues, AffectedPackages, DiagnoseChangesets, RetargetRelease, ReleaseRecord, PublishReadiness, TagRelease, and Command.
  • See the CLI step reference for per-step guidance, prerequisites, and composition examples.

Validation

Run:

monochange step validate

monochange step validate validates:

  • package and group declarations
  • manifest presence for each package type
  • group membership rules
  • versioned_files structural rules (type/format/regex conflicts, required format fields, capture groups)
  • versioned_files content checks: file existence, version field readability, regex pattern matching
  • .changeset/*.md targets and overlap rules
  • Cargo workspace version-group constraints
  • [source] url scheme security (https:// required)