Comparison with changesets

A precise, non-marketing map of where molt matches JS changesets, where it deliberately diverges, what it adds, and what it refuses to build.

molt is a faithful port of changesets with documented divergences. This page is the technical reference for exactly which is which. It tracks changesets at 3.0.0-next.9 (the v3 line) and follows v3 semantics, not the older v2 behavior that most online material and both abandoned Python ports describe. If you are coming from changesets, pair this with Migrating from changesets.

The v3 baseline

molt ports v3 semantics. The behaviors below changed between changesets v2 and v3; molt matches the v3 column:

Behaviorv2v3 (what molt follows)
Peer-dependent propagationmajor cascadepatch only -- N/A for molt anyway (no peerDependencies)
version with no changesetsexit 0exit 1
tag commandtaggit-tag
Default base branchmastermain
Legacy v1 changeset formatsupportedremoved -- molt reads the v2/v3 Markdown format only
Publish pipelineone publish steppack -> publish split (build artifacts, then upload)

Ported faithfully

These behaviors are the reason changesets works, and molt keeps them intact. Getting any of them wrong produces silently incorrect version numbers, so they are validated against ported test fixtures.

BehaviorNotes
Changeset file = Markdown + YAML front matterSame format; names matched with PEP 503 normalization
Two-phase add -> versionRecord intent while fresh; consume in a batch
Flatten to the max bump; changelog keeps the unionThree changesets -> one release at the highest bump, every summary preserved
Files on disk -> squash- and rebase-safeIntent lives in a committed file, not in git history
The 3-pass fixpoint release plandetermineDependents -> matchFixedConstraint -> applyLinks, repeated to a fixpoint; order is observable and load-bearing
Dependent propagation + range rewritingOver PEP 508 requirements; a dependent is released when the dependency leaves its declared range
Dependency bumps are always a patch, rendered last in the patch sectionSame rule; add a separate changeset for a louder dependent entry
fixed and linked groupsSame semantics (with clearer names)
status, --since, JSON outputReports pending releases without mutating
Changelog generator pluginsTwo functions per generator; molt owns section layout (Changelog plugins)
Topological publish orderingPublish in dependency order
Trusted publishing via OIDC1:1 with changesets' v3 GitHub workflow
Git policy: implicit git = warn, explicit git = failDetecting changed packages never hard-fails; --since main does

Deliberately different

These diverge because Python's packaging rules force a different answer than JavaScript's. Each is intentional and documented in Design decisions.

Areachangesetsmolt
Version + range mathSemVer / node-semverPEP 440 + PEP 508 via packaging
Prerelease identifiersarbitrary tags (-next.0)fixed vocabulary (aN/bN/rcN/.devN)
Prerelease range opt-inscoped to the same release tuplescoped across the whole specifier set (PEP 440); dependents that opted in are not force-bumped rc0 -> rc1
Prerelease modepre.json repo-global state--pre invocation flag; no persistent state
Snapshotsthrowaway version + npm dist-tag.devN on a separate index; never PyPI by default
Bad release recoveryunpublish / deprecatemolt yank (PEP 592, guided -- PyPI has no yank API)
peerDependenciesfirst-class subsystemdropped -- no Python analogue
dist-tags / --tagcentral to prerelease + snapshot flowsgone -- PyPI has no dist-tags
Dependenciesa map in package.jsona list of PEP 508 strings in pyproject.toml; range rewriting splices the string
Manifest editingJSON reserialize (no comments to lose)comment- and format-preserving TOML via tomlkit
Lockfileleft staleuv.lock updated during version
Workspace modelone package.json universeecosystem backend seam (uv, Poetry, Hatch, PDM, setuptools)
Forgehard-wired to GitHubforge seam; GitHub first
Vocabulary"workspace" = one package; linked/fixed nearly identical"workspace" = the repo; clearer group names (Glossary)

What molt adds

Capabilities changesets lacks -- most cheap to build fresh, expensive to retrofit, which is why they were never added upstream. Several are the community's longest-standing unmet requests (see the Roadmap for the demand history).

Additionchangesets status
Ecosystem / version-source abstractionFormally rejected in 2020, re-requested for six years
Lockfile updates during versionAbsent entirely
Uniform machine-readable plan object on every mutating command; --dry-run = plan + printPartial (publish-plan only, v3)
Non-interactive molt addNot possible today -- unlocks Dependabot/Renovate/codegen
Real changelog templating (Jinja2, dates, sections)Open 7 years (#109)
No pre.json -- prerelease as a flag~15 open issues trace to pre.json
Forge-agnostic integrationOpen 4 years, zero maintainer comments
Single-package + root-workspace as a first-class pathDegenerate special case upstream
molt yank (guided; the yank itself is a browser step)Impossible on npm
Atomic, resumable version (buffer-then-flush)Double-bumps on a retry after mid-run failure
Correct changelog Markdown, emitted directlyNeeds a formatter pass to repair blank lines
Windows-correct from day oneWindows CI added only in 2026-07

What molt refuses

A sharp tool says no. These are deliberate non-goals, not backlog items:

  • Full commit-derived versioning. It destroys the intent-based model; a commit is not a release intent, and conventional-commit prefixes cannot see breaking changes across package boundaries. molt offers one-time changeset seeding from commits as a migration aid only.
  • Executable config. Config is TOML/JSON so other tools can read it without running your code.
  • Arbitrary shell hooks as the primary extension point. Extension happens through Python entry points.
  • Per-tool bespoke integrations (Nx/Turbo-style), one-PR-per-package, chat bots, and config-option creep. Being the best Python release tool beats being a mediocre everything-tool.

Where to go next