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(ordocs:updatein this repository) after changing any template or consumer block - Run
mdt check(ordocs: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]
parent_bump = "patch"
include_private = false
warn_on_group_mismatch = true
strict_version_conflicts = false
package_type = "cargo"
[defaults.changelog]
path = "{{ path }}/changelog.md"
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 = [
"Cargo.toml",
{ path = "crates/sdk_core/extra.toml", type = "cargo" },
]
tag = false
release = false
version_format = "namespaced"
[package.sdk-core.changelog]
path = "crates/sdk_core/CHANGELOG.md"
format = "monochange"
Required fields:
pathtype, unless[defaults].package_typeis set
Supported type values:
cargonpmdenodartflutterpython
Optional package fields:
type, when[defaults].package_typeis setchangelogempty_update_messagepublishversioned_filestagreleaseversion_format
changelog accepts three forms on packages:
true→ use{{ path }}/CHANGELOG.mdfalse→ disable the package changelog"some/path.md"→ use that exact path
[defaults].changelog also accepts three forms:
true→ default every package to{{ path }}/CHANGELOG.mdfalse→ 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>]
extra_changelog_sections can also be set on:
[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 }}.
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 publishingmode-builtinorexternalregistry- public registry override for the package ecosystemtrusted_publishing-true/falseor a table withenabled,repository,workflow, andenvironmentattestations.require_registry_provenance- require registry-native package provenance when the selected registry/provider capability supports itrate_limits.enforce- block built-in publish runs when the selected package set exceeds a known single registry windowplaceholder.readme- inline placeholder README contentpublish_order.dependency_fields- ecosystem-level dependency fields used to topologically order package publishesplaceholder.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 currently 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_proxyvia VCS tags - Python packages →
pypi
Private registries and custom publication flows are still external. For those packages, set mode = "external" and handle publication outside monochange.
Placeholder publishing
mc 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.
For each managed package with built-in publishing enabled, 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.readmeorplaceholder.readme_fileoverrides 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 mc plan-release-publish and mc publish.
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 packages can be configured automatically with
npm trust github ... - pnpm workspaces use
pnpm exec npm trust ...andpnpm publish, so workspace protocol and catalog dependency handling stays aligned with the workspace manager - Cargo,
jsr,pub.dev, andPyPIcurrently 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 currently 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, andenvironmentvalues in config - otherwise
[source]plus GitHub Actions environment such asGITHUB_WORKFLOW_REFandGITHUB_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, automatic trust setup cannot proceed.
Current implementation limits
The built-in package publishing flow is intentionally narrow for now:
- 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
- manual trusted-publishing setup is still required for
crates.io,jsr,pub.dev, andPyPI
If your workflow needs any of those today, 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" - group
tag,release, andversion_formatoverride member package release identity - package changelogs and package
versioned_filesstill apply when grouped - grouped packages can customize fallback changelog entries with
empty_update_messagewhen no direct package notes are present [group.<id>.changelog].includecan 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 = "" },
]
versioned_files = [
{ path = "package.json", type = "npm", fields = ["metadata.bin.monochange.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.
Regex versioned files
Regex entries let you version-stamp any plain-text file — README badges, download links, 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:
regexentries cannot settype,prefix,fields, orname— they operate on raw text- the regex must include a
(?<version>...)named capture group - the
pathfield 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 mc 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, andbun.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 mc 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 top-level commands. Each [cli.<command>] table in monochange.toml becomes mc <command>, with its own help text, inputs, and ordered step list.
mc 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 mc step:* commands. The binary generates those commands from the step schemas, so mc step:discover, mc step:prepare-release, mc step:affected-packages, and the other step commands do not require config entries. Use step:* 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, and check. The step: prefix is reserved for immutable built-in step commands. Do not define [cli.step:*] tables.
Explicit step input inheritance
Config-defined workflow commands have two input layers:
[[cli.<command>.inputs]]declares the flags and arguments accepted bymc <command>.inputson 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"], 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", "markdown"], default = "markdown" },
{ 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.
Built-in mc step:* commands are different: they are generated directly from the step schema, so their CLI flags map to that single step without a [cli.*] wrapper.
[release_notes]
change_templates = [
"#### {{ summary }}\n\n{{ details }}\n\n{{ context }}",
"#### {{ summary }}\n\n{{ context }}",
"#### {{ summary }}\n\n{{ details }}",
"- {{ summary }}",
]
[package.core]
path = "crates/core"
extra_changelog_sections = [
{ name = "Security", types = ["security"], default_bump = "patch" },
]
[cli.discover]
help_text = "Discover packages across supported ecosystems"
[[cli.discover.inputs]]
name = "format"
type = "choice"
choices = ["text", "json"]
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"]
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"]
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"]
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"]
name = "format"
type = "choice"
choices = ["text", "json"]
default = "text"
type = "PrepareRelease"
type = "Command"
command = "cargo test --workspace --all-features"
dry_run_command = "cargo test --workspace --all-features"
shell = true
[cli.affected]
help_text = "Evaluate pull-request changeset policy"
[[cli.affected.inputs]]
name = "format"
type = "choice"
choices = ["text", "json"]
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
namelabels such asprepare releaseandpublish release; keep or replace those labels when you want progress output to stay readable - custom command variables become available when
variablesis present: map your own names to variables such asversion,group_version,released_packages,changed_files, andchangesets always_run = trueon any step causes it to run even when a previous step has failed, which is useful for cleanup, notification, or dry-run preview stepsupdate_release_json = trueon aCommitReleasestep 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 errordry_run_commandon aCommandstep replacescommandonly when the CLI command is run with--dry-rundry_run = trueon a[cli.<command>]table forces the entire command to run in dry-run mode even when the user does not pass--dry-runshell = trueruns the command through the current shell; the default mode runs the executable directly after shell-style splitting
Performance tip: keep the default mc 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 mc repair-release versus publishing a new patch release.
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.
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"
# api_url = "https://github.company.com/api/v3" # optional: for GitHub Enterprise
[source.releases]
enabled = true
draft = false
prerelease = false
source = "monochange"
[source.releases]
branches = ["main", "release/*"]
enforce_for_tags = true
enforce_for_publish = true
enforce_for_commit = false
changeset_context_timeout_seconds = 120
[source.pull_requests]
enabled = true
branch_prefix = "monochange/release"
base = "main"
title = "chore(release): prepare release"
labels = ["release", "automated"]
auto_merge = false
[changesets.affected]
enabled = true
required = true
skip_labels = ["no-changeset-required"]
comment_on_failure = true
changed_paths = ["crates/**", "packages/**", "npm/**", "skills/**"]
ignored_paths = [
"docs/**",
"specs/**",
"readme.md",
"CONTRIBUTING.md",
"license",
]
name = "production"
trigger = "release_pr_merge"
release_targets = ["sdk"]
requires = ["main"]
Ecosystem settings
These settings are parsed from config and document intended control points for discovery:
[ecosystems.cargo]
enabled = true
roots = ["crates/*"]
exclude = ["crates/experimental/*"]
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 = [
{ command = "pnpm install --lockfile-only", cwd = "packages/web" },
]
[ecosystems.deno]
enabled = true
# Deno currently has no inferred lockfile command.
[ecosystems.dart]
enabled = true
lockfile_commands = [{ command = "flutter pub get", cwd = "packages/mobile" }]
[ecosystems.python]
enabled = true
lockfile_commands = [{ command = "uv lock" }]
[ecosystems.go]
enabled = true
# monochange infers `go mod tidy` for go.mod / go.sum refreshes.
lockfile_commands = [{ command = "go mod tidy" }]
Changelog configuration
When [defaults].package_type is set, package entries may omit an explicit type.
monochange currently supports two changelog formats:
monochangekeeps the current heading-and-bullets layoutkeep_a_changelogrenders 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.
You can also customize release-note rendering with a workspace-wide [release_notes] table plus per-package or per-group extra_changelog_sections definitions.
Supported template variables include:
| Variable | Meaning | Notes |
|---|---|---|
{{ summary }} | rendered release-note summary heading | always available |
{{ details }} | optional long-form details body | omitted when the changeset has no details |
{{ package }} | owning package id for the rendered entry | useful in shared templates |
{{ version }} | release version for the current target | package or group version |
{{ target_id }} | release target id | package id or group id |
{{ bump }} | resolved bump severity | none, patch, minor, or major |
{{ type }} | changeset note type | e.g. feature, fix, security; omitted when absent |
{{ context }} | compact default metadata block | preferred rendered block for human-readable notes |
{{ changeset_path }} | source .changeset/*.md path | tracked in manifests and still available for custom templates, but not shown by default in {{ context }} |
{{ change_owner }} | plain-text hosted actor label | usually something like @ifiokjr |
{{ change_owner_link }} | markdown link to the hosted actor | falls back to plain text when no URL is available |
{{ review_request }} | plain-text PR/MR label | e.g. PR #31 or MR !42 |
{{ review_request_link }} | markdown link to the PR/MR | falls back to plain text when no URL is available |
{{ introduced_commit }} | short SHA for the commit that first introduced the changeset | plain text only |
{{ introduced_commit_link }} | markdown link to the introducing commit | preferred for changelog output |
{{ last_updated_commit }} | short SHA for the most recent commit that changed the changeset | only populated when different from {{ introduced_commit }} |
{{ last_updated_commit_link }} | markdown link to the most recent commit that changed the changeset | only populated when different from {{ introduced_commit }} |
{{ closed_issues }} | plain-text list of issues closed by the linked review request | typically #12, #18 |
{{ closed_issue_links }} | markdown links to issues closed by the linked review request | preferred for changelog output |
{{ related_issues }} | plain-text list of related issues that were referenced but not closed | host support may vary |
{{ related_issue_links }} | markdown links to related issues that were referenced but not closed | host 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 — without exposing the transient .changeset/*.md path unless you explicitly reference {{ changeset_path }} in your template.
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
Current implementation notes:
defaults.include_privateis parsed, but discovery behavior is still centered on the supported fixture-driven CLI commands documented here[ecosystems.*].enabled/roots/excludeare parsed, but discovery still scans all supported ecosystems regardless of those settings todaydefaults.strict_version_conflictscontrols whether conflicting explicitversionentries across changesets warn-and-pick-highest (default) or fail planning outright- source automation expects
[source]with provider release settings and release branch policy under[source.releases], pull request settings under[source.pull_requests], and affected-package policy settings under[changesets.affected]; GitHub remains the default provider - live GitHub release and release-request publishing uses
octocrabwithGITHUB_TOKEN/GH_TOKEN; GitLab and Gitea use direct HTTP APIs - release-request publishing still uses local
gitfor branch, commit, and push operations before provider API updates when not in dry-run mode - changeset policy commands currently apply only to the GitHub provider and expect
[changesets.affected], achanged_pathscommand input, and reusable diagnostics for GitHub Actions consumption - supported command steps today are
Validate,Discover,CreateChangeFile,PrepareRelease,CommitRelease,PublishRelease,OpenReleaseRequest,CommentReleasedIssues,AffectedPackages,DiagnoseChangesets,RetargetRelease, andCommand - see the CLI step reference for detailed per-step guidance, prerequisites, and composition examples
Validation
Run:
mc step:validate
mc step:validate validates:
- package and group declarations
- manifest presence for each package type
- group membership rules
versioned_filesstructural rules (type/regex conflicts, capture groups)versioned_filescontent checks: file existence, version field readability, regex pattern matching.changeset/*.mdtargets and overlap rules- Cargo workspace version-group constraints
[source]url scheme security (https://required)