Skip to main content
Version: Next

Commit message reference

Write your commit messages in a strict superset of Conventional Commits that adds the monorepo dimension. This format tells dispat which packages a change releases, how far it reaches, and which channel it lands on.

The pkg/ccme package parses your messages. A message holds one or more units separated by a line of ---. Each unit has its own header, body, and footers.

<type>[(<scope-set>)][<directives>][!]: <description>

[body]

[footers]

The type decides the version bump. You can override these defaults in parser.types:

TypeBump
featminor
fix, perf, revertpatch
any other known typenone
any type with !major

Scope sets

TermResolves toUnknown name
corethat packageerror
core,uibotherror
*every package in the workspace(n/a)
@acme/*every package matching the globwarning if it matches nothing
.the packages owning the commit's changed files(n/a)
-appremoves app from the set; exclusions always winwarning

Omit the parentheses to derive the set from changed files using the longest matching path prefix, meaning a file under a nested package belongs to the inner package only. A package declaring a src owns only the files inside that sub-folder. dispat reports a unit as inert if it resolves to no packages.

Inline directives

Write inline directives between the scope-set and the :. Every directive has an equivalent footer. Stating both is redundant, and contradicting them causes an error.

WrittenFooter equivalentMeaning
!BREAKING CHANGE: <text>Raises the unit's own bump to major.
^Propagate-Depth: 1Reach direct consumers.
^^Propagate-Depth: allReach the whole transitive closure.
+NPropagate-Depth: NReach consumers up to N edges away. +0 is no propagation.
^minorPropagate: minorThe bump consumers take. Default patch.
^nonePropagate: nonePropagate nothing, whatever the depth.
%betaChannel: betaThe unit's own packages move to that channel.
%%betaPropagate-Channel: betaThe channel handed to the consumers reached below.
++NPropagate-Channel-Depth: NHow far the channel travels. Default 0.

Both depths default to 0, and neither bounds the other. A unit reaches nobody on either axis until you say so.

The same commit planned twice: as feat(core) only core releases, and amended to feat(core)^^ the whole consumer closure joins the plan while utils, a provider, stays unchanged.

Footers

FooterEffect
BREAKING CHANGE: <text>Major bump plus the text in the changelog. Case is significant; near-misses are warned about.
Propagate: <bump>none, patch, minor, major or inherit (copy the unit's own bump).
Propagate-Depth: <N|all>Bump-axis reach.
Propagate-Scope: <scope-set>Intersects the reached set. If nothing survives, a warning and no propagation.
Propagate-Channel: <value>inherit (default), none, stable, a channel name, or a <from>><to> transition.
Propagate-Channel-Depth: <N|all>Channel-axis reach.
Propagate-Channel-Scope: <scope-set>Restricts the channel axis. Defaults to Propagate-Scope.
Channel: <value>The unit's own channel, same grammar.
Release-As: <none|auto|version>Release control; see below.
Reverts: <sha>Informational for the bump; suppresses both changelog entries. See corrections.
Edits: <sha[#n]|*>Restate the records named, as this unit. See corrections.
Deletes: <sha[#n]|*>Discard the records named. See corrections.

Channels and prereleases

dispat reads a package's channel from its baseline tag. For example, 1.5.0-beta.3 is on beta, 1.4.2 is on stable, and an untagged package is on stable. Prerelease versions use a <major>.<minor>.<patch>-<channel>.<counter> format with a separate numeric counter, so beta.10 sorts above beta.9.

feat(core)%beta: core enters the beta line; nothing else moves
feat(core)^%beta: the same: the caret reaches the consumers and every
one is suppressed, because a stable consumer cannot
resolve a beta release
feat(core)^%beta++1: core and its direct consumers enter beta together
feat(core)^: an established train stays on beta with no directive
release(core)%stable: graduate core
release(core)%beta>stable%%beta>stable++*:
graduate core and everything still on beta behind it

A <from>><to> transition matches against the baseline channel, making the directive idempotent. Packages that already graduated do not match, so the same directive works perfectly on the first run and the fifth. A bare stable arriving by propagation never graduates a dependant, because only a direct directive or a transition naming the train can do that.

A train converges exactly like stable releases do, meaning work a prerelease has already published cannot release again, and a re-run with no new commits finds nothing to do. dispat computes the version of the next prerelease and the graduation over the whole train, so a breaking change shipped in beta.0 keeps the target at the next major. A Release-As consumed by a prerelease loses force, and a cancel cannot retract train-published work (a live cancel aimed at published work warns W170).

Convergence happens quietly. The tag contains the directive that started the train, so dispat does not re-report it as "already on beta" (W199) on later runs. That warning is reserved for a fresh directive pointing where the package already is, and a spent cancel is not re-reported either.

Release control

WrittenEffect
release(<pkg>) + Release-As: noneHold: the bump is retained and reported, not released, until a later directive lifts it.
release(<pkg>) + Release-As: autoResume: release at the max() of everything accumulated, catch-up included.
release(<pkg>) + Release-As: <version>Pin an exact version.
cancel(<pkg>)Discard the package's unreleased metadata. Irreversible; never reaches a published tag.

dispat rejects a pin if it fails to move the package forward, falls below what pending commits require, or raises the major version more than one above the computed version. A rejected pin has a unit-scoped blast radius: dispat reports the error (stopping the run under commitErrors: "error"), ignores the bad directive, and falls back to the computed version while leaving other valid units unaffected. A pin sets the exact version and channel, so Release-As: 2.0.0-rc.0 enters the rc line and Release-As: 2.0.0 graduates, but the commit type still declares the bump size.

A cancel is irreversible and only reaches backwards. It discards contributions from ancestor commits, but work landing afterwards accumulates normally. A cancel on a provider that has already published is a no-op and says so, because the version is public and the right target for stopping a pending catch-up is the consumer.

A cancel names nothing and discards everything behind it. To act on one record rather than the whole ledger, Edits restates a named commit's record and Deletes discards it. Both directives reach only unreleased work and can be undone by a later correction, as explained in Correcting a release record.