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

Upgrading to 0.11

Version 0.11 makes the CLI output human-first, tightens publish readiness before any registry is touched, and updates several library APIs. Most repositories only need the CLI and changeset-authoring sections; Rust consumers should also review the library API section.

Each section states who is affected, what changed, and the exact update step. The complete change list for this release lives in changelog.md.

CLI results are human-first by default

Affects: scripts, CI jobs, and agents that parse default output.

Commands now print concise human-readable text by default. The previous default was Markdown, which a terminal rendered and a pipe received as Markdown text. One format now has one meaning:

  • the default and --format text return concise human-readable text;
  • --format markdown always returns raw Markdown and is never terminal-rendered;
  • --format json and --format json-min return structured, ANSI-free data.

Update steps:

  • Replace parsers of the old default output with --format json (complete report) or --format json-min (one line).

  • monochange step config prints a short workspace summary. Use --format json for the complete resolved configuration.

  • --jq now requires JSON output, so it can never run a command and only then discover that the result was not JSON:

    # before
    monochange <command> --jq '<query>'
    
    # after
    monochange <command> --format json --jq '<query>'
    
  • --quiet now changes output only. It no longer silently turns a real operation into a dry run, so add --dry-run explicitly:

    # before: this performed a dry run
    monochange run release --quiet
    
    # after
    monochange run release --dry-run --quiet
    

Workflows that set an explicit format default in monochange.toml keep that choice.

check failures exit non-zero in every format

Affects: CI jobs that run monochange check with JSON output.

A lint failure previously returned status 0 under --format json and --format json-min. Every format now exits 1 when lint errors exist, and the complete lint report still arrives on stdout:

monochange check --format json
echo $? # 1 when lint errors exist

CI can stop on the exit status without parsing error_count first.

Progress and diagnostics are safe for automation

Affects: CI log scrapers and automation that watched progress output.

  • Captured and CI output never contains cursor-control sequences such as ESC[2K. Progress is emitted as complete, deterministic lines.
  • MONOCHANGE_NO_PROGRESS=1 now also suppresses lint progress, as was already documented.
  • Failures print a stable diagnostic code, the relevant command or file context, and a suggested next action.
  • --progress-format json emits a newline-delimited event stream for tooling that wants machine-readable progress.
  • --log-level debug stays a maintainer trace and now disables animation.

If a script detected progress by scraping spinner frames or control sequences, switch it to --progress-format json.

Publishing defaults and readiness

Longer default publish timeout (schema v0.6)

Affects: releases that publish to slow registries.

The default publish.timeout.timeout_seconds rises from 60 to 300 because crates.io server-side verification can take minutes. The timeout is a ceiling, not a delay: publishes that finish quickly are unaffected. The configuration and release-record schemas advance to v0.6; existing records migrate automatically and need no action.

Override the default per ecosystem or per package, and set timeout_seconds = 0 to disable the ceiling entirely:

[ecosystems.cargo.publish.timeout]
timeout_seconds = 300
retries = 2

Readiness blocks packages that cannot publish (artifact v3)

Affects: workflows that run monochange step publish-readiness or monochange step publish-packages.

Publish readiness now verifies trusted publishing for every selected package and validates the publication order against the workspace dependency graph before any registry state changes. A real publish run executes every readiness check before the first publish command, so a package that cannot publish aborts the run instead of failing midway.

Update steps:

  • The readiness artifact schema advances from 2 to 3. Regenerate stored readiness artifacts after upgrading.
  • For each package with publish.trusted_publishing = true, make sure repository, workflow, and the optional environment resolve from configuration, source settings, or the CI environment, and that the workflow file exists under .github/workflows/.
  • npm, crates.io, and pub.dev only accept trusted publishing for packages that already exist. Bootstrap first-time packages with monochange step placeholder-publish, otherwise readiness blocks them with guidance.

Changeset authoring rules tightened

Affects: changeset authors and repositories using the changesets/recommended lint preset.

The preset now requires an H1 source summary (# Outcome) and rejects a first description sentence that merely repeats the summary. Repositories that do not use the preset can enable the standalone rule:

[lint.rules]
"changesets/summary-description" = "error"

Existing changeset files keep working, but new ones must lead with the H1 summary. The rendered changelog keeps choosing its own heading depth.

Library API updates

Affects: Rust consumers of the published crates.

PackagePublishSummary reports domain statuses

Replace expected, succeeded, and skipped with the explicit counters:

#![allow(unused)]
fn main() {
let summary = report.summary();
assert_eq!(summary.total(), report.packages.len());
assert_eq!(summary.published, 3);
assert_eq!(summary.already_exists, 2);
}

planned rows no longer collapse into skipped: use published, already_exists, blocked, and not_attempted for the states you care about.

Release notes stay structured until rendered

ReleaseNotesDocument is generic over its entry type. The previous Vec<String> of rendered Markdown entries still works, while typed entries expose summary, details, packages, change type, bump, stream, style, and provenance as fields:

#![allow(unused)]
fn main() {
let notes: ReleaseNotesDocument<ReleaseNotesEntry> = ReleaseNotesDocument {
	title: "1.2.3".into(),
	summary: vec![],
	sections: vec![ReleaseNotesSection {
		title: "Fixes".into(),
		collapsed: false,
		entries: vec![ReleaseNotesEntry {
			summary: "Keep JSON failures non-zero".into(),
			details_markdown: Some("CI can trust the exit status.".into()),
			packages: vec![],
			change_type: Some("fix".into()),
			bump: BumpSeverity::Patch,
			stream: "default".into(),
			style: ReleaseNoteEntryStyle::Compact,
			provenance: ReleaseNoteProvenance::default(),
		}],
	}],
};
let legacy = notes.to_legacy_markdown(&ChangelogStyle::default());
}

Built-in section names no longer contain emoji. Repositories that want emoji put them in their configured section headings.

SemanticChange is non-exhaustive

Custom semantic analyzers construct findings with SemanticChange::new and attach compiler evidence with with_assessment instead of using a struct literal:

#![allow(unused)]
fn main() {
let change = SemanticChange::new(
	SemanticChangeCategory::PublicApi,
	SemanticChangeKind::Modified,
	"function",
	"parse",
	"function `parse` changed",
	"src/lib.rs",
)
.with_assessment(SemanticChangeAssessment::new(
	SemanticAnalysisOutcome::Breaking,
	BumpSeverity::Major,
	ApiConfidence::High,
	SemanticAnalyzerEvidence::new(
		"example/analyzer",
		"example-engine",
		SemanticAnalysisCompleteness::Complete,
		"all public entrypoints checked",
	),
));
}

CargoSemanticAnalyzer uses constructors

EcosystemSettings gains a semver_checks field, so struct literals must initialize it. CargoSemanticAnalyzer is no longer a unit struct; construct it explicitly:

#![allow(unused)]
fn main() {
let disabled = monochange_cargo::semantic_analyzer();
let with_settings = monochange_cargo::semantic_analyzer_with_settings(settings);
}

Exact ranges replace merge-base semantics for custom ranges

ChangeFrame::CustomRange now compares exactly base..head, while pull request frames keep merge-base semantics. Tools that compare several git frames should build one AnalysisSession and reuse it, and shared package path policy moved into monochange_core::PackagePathMatcher:

#![allow(unused)]
fn main() {
use monochange_analysis::{AnalysisConfig, AnalysisSession, ChangeFrame};

let session = AnalysisSession::new(root, AnalysisConfig::default())?;
let pull_request = session.analyze(&ChangeFrame::CustomRange {
	base: "origin/main".into(),
	head: "HEAD".into(),
})?;
}
#![allow(unused)]
fn main() {
use monochange_core::{PackagePathMatch, PackagePathMatcher};

let matcher = PackagePathMatcher::new(
	"web",
	"packages/web".as_ref(),
	&["shared/schema/**".into()],
	&["fixtures/**".into()],
);
assert_eq!(
	matcher.classify("shared/schema/api.json".as_ref()),
	PackagePathMatch::Touched,
);
}

Machine-readable schema versions

Affects: tools that pin monochange’s JSON schemas.

ArtifactPreviousCurrentAction
Configuration and release-record schema0.50.6None; records migrate automatically
Publish-readiness artifact23Regenerate stored artifacts
Change-classification reportolder3Update parsers; coverage may carry a checks array