When you run bumpy version (or bumpy status to preview), bumpy reads all pending bump files and builds a release plan — a list of packages to bump and by how much. This document explains the full algorithm, including how bumps propagate to dependent packages and the available settings.
After collecting explicit bumps from bump files, bumpy runs a propagation loop that repeats until stable:
- Phase A — fix out-of-range dependencies (always runs)
- Phase B — enforce fixed/linked group constraints (optional/advanced)
- Phase C — apply cascades and proactive propagation rules (optional/advanced)
For every package in the release plan, bumpy checks each dependent's declared version range. If the new version would fall outside that range, the dependent must be bumped so its package.json stays valid.
The bump type applied to the dependent depends on the dependency type:
| Dependency type | Bump applied to dependent | Why |
|---|---|---|
peerDependencies |
matches the triggering bump | Proportional — a minor bump on the dep → minor bump on the dependent |
dependencies |
patch |
Internal detail — consumers don't see it |
optionalDependencies |
patch |
Internal detail — consumers don't see it |
devDependencies |
(skipped) | Doesn't affect published consumers† |
For peer deps, "matches the triggering bump" means if core gets a minor bump that breaks the range, plugin also gets a minor bump. This keeps version bumps proportional — especially important for 0.x packages where ^ ranges cause minor bumps to go out of range frequently.
†
devDependenciesare skipped because they normally don't ship to consumers. The exception is a dependency that affects your published output — most often one bundled in by a build step (tsup, tsdown, esbuild, rolldown/rollup, Vite,bun build, …), declared underdevDependenciessince it isn't runtime-resolved. List those underreleaseTriggeringDevDeps(or usecascadeFrom) so any bump to them republishes this package. See Configuration.
This phase is a safety net — it cannot be skipped. It ensures that published packages always have valid dependency ranges.
Workspace protocol shorthands (workspace:^, workspace:~, workspace:*) are resolved to actual semver ranges before checking satisfaction. The "current version" used is the dependency's version before bumping — we're checking whether the new (post-bump) version still satisfies the existing range:
In package.json |
Resolved to | Example (dep at 1.2.0) |
|---|---|---|
workspace:^ |
^<currentVersion> |
^1.2.0 |
workspace:~ |
~<currentVersion> |
~1.2.0 |
workspace:* |
(always satisfied) | never triggers propagation |
This resolution is only for range checking — bumpy does not modify the workspace:^ string in the source package.json. At publish time, these protocols are resolved to real ranges either by the package manager (in the default pack mode) or by bumpy itself (in in-place mode or when using custom publish commands).
Full ranges like workspace:^1.2.0 are resolved by stripping the workspace: prefix.
catalog: protocol references (used by pnpm catalogs) are always treated as satisfied — bumpy cannot resolve the catalog to check the actual range, so it never triggers propagation through catalog deps.
npm's ^ operator behaves differently for 0.x versions:
| Range | Means | Example |
|---|---|---|
^1.2.3 |
>=1.2.3 <2.0.0 |
Minor/patch bumps stay in range |
^0.2.3 |
>=0.2.3 <0.3.0 |
Minor bumps break the range |
^0.0.3 |
>=0.0.3 <0.0.4 |
Patch bumps break the range |
This means a minor bump on a 0.x package with ^0.x peer deps will break the range. Since Phase A matches the triggering bump for peer deps, the dependent also gets a minor bump — but if the dependent is at 1.x+, that minor bump is disproportionate to what's really just an internal dependency update. Bumpy warns when this happens.
Tip: If you're seeing unexpected propagation from
0.xpeer deps, consider using explicit ranges (e.g.workspace:>=0.2.0or>=0.2.0) instead ofworkspace:^to widen the range and reduce breakage.
These constraints help keep a group of related packages arbitrarily in sync. This config can be set in root .bumpy/_config.json file, and package names can be names or glob patterns. Note that each setting is an array of arrays, since you can have multiple groups.
Packages in a fixed group always share the same version number. When any package in the group bumps, all packages get the highest bump level.
{ "fixed": [["@myorg/core", "@myorg/types"]] }Example: propagation bumps @myorg/types as patch → @myorg/core also gets a patch bump to stay in sync.
Packages in a linked group share the same bump level but keep independent version numbers. Only packages already in the release plan are affected — linked groups don't pull in packages that have no bump files. Entries can be specific names or glob patterns.
{ "linked": [["@myorg/plugin-*"]] }core gets a minor bump → utils has ^1.0.0 dep on core, goes out of range (Phase A) → utils gets a patch bump → fixed group pulls in types to match (Phase B) → app depends on types and goes out of range (Phase A, next iteration) → app gets a patch bump → stable.
Most users don't need this, but bumpy provides flexible settings for more complex monorepo workflows.
Beyond fixing broken ranges, you may want dependents to re-release even when their ranges are still satisfied — for example, to ensure consumers always get the latest internal dependency versions.
Set updateInternalDependencies in .bumpy/_config.json:
| Value | Phase A (out-of-range) | Phase C (proactive) |
|---|---|---|
"out-of-range" (default) |
Yes | No |
"patch" |
Yes | Yes — triggers when any dependency bumps (patch or higher) |
"minor" |
Yes | Yes — triggers only when a dependency bumps minor or higher |
When Phase C is active, dependency bump rules control which dependency types trigger proactive bumps and what bump level to apply.
Each rule is either false (disabled) or an object with two fields:
trigger— minimum bump level in the dependency that activates propagation ("major","minor", or"patch")bumpAs— what bump to apply to the dependent ("major","minor","patch", or"match")
Global rules are set in .bumpy/_config.json. The following are the built-in defaults — you only need to specify overrides:
{
"dependencyBumpRules": {
"dependencies": { "trigger": "patch", "bumpAs": "patch" },
"peerDependencies": { "trigger": "major", "bumpAs": "match" },
"devDependencies": false,
"optionalDependencies": { "trigger": "minor", "bumpAs": "patch" }
}
}Per-package overrides can be set in package.json["bumpy"] to override the global rules for a specific package (as a dependent):
{
"bumpy": {
"dependencyBumpRules": {
"devDependencies": { "trigger": "patch", "bumpAs": "patch" }
}
}
}For example, a private app might want devDeps to propagate because they're bundled at build time, even though the global default disables devDep propagation.
Rule resolution order (when package A bumps and package B depends on A):
dependencyBumpRules[depType]on package B (most specific)dependencyBumpRules[depType]in root config- Built-in defaults (least specific)
These let you declare explicit cascade relationships between packages. Both always apply regardless of the updateInternalDependencies setting.
cascadeTo— configured on the source package: "when I'm bumped, cascade to these packages"cascadeFrom— configured on the consumer package: "when these packages are bumped, cascade to me"
The simplest form is an array of package names or glob patterns. By default, any bump in the source triggers a matching bump in the target (e.g., minor→minor, patch→patch):
{
"bumpy": {
"cascadeTo": ["@myorg/plugin-*", "@myorg/cli"]
}
}{
"bumpy": {
"cascadeFrom": ["@myorg/vite-integration"]
}
}For more control, use the object form with per-entry rules. Both trigger (default: "patch") and bumpAs (default: "match") are optional:
{
"bumpy": {
"cascadeTo": {
"@myorg/plugin-*": { "trigger": "minor", "bumpAs": "patch" }
}
}
}| Field | Default | Description |
|---|---|---|
trigger |
"patch" |
Minimum bump level in the source that activates the cascade |
bumpAs |
"match" |
What bump to apply to the target ("match" mirrors the source bump level) |
cascadeFrom is useful when a package bundles a dependency at build time (e.g., a devDependency that ends up in the published output) and should be re-released whenever that dependency changes.
These are set directly in bump files for one-off control over a specific release.
none — acknowledges a change without triggering a direct bump. Unlike a real bump type, none doesn't add the package to the release plan on its own. However, cascading bumps from other packages (e.g., out-of-range or proactive propagation) can still bump it normally.
---
'@myorg/core': minor
'@myorg/plugin-a': none
---Bump-file-level cascades — explicitly cascade bumps to other packages with glob support. The difference from listing packages directly in the bump file is that cascaded packages are marked as dependency bumps (not direct changes), which affects how they appear in changelogs and PR comments. These always apply (no trigger threshold check):
---
'@myorg/core':
bump: minor
cascade:
'@myorg/plugin-*': patch
---Compare with listing packages directly — these are treated as independent changes and each gets the bump file's summary in their changelog:
---
'@myorg/core': minor
'@myorg/plugin-a': patch
'@myorg/plugin-b': patch
---Note:
noneand bump-file-level cascades are not available in the interactivebumpy addUI — they are power-user features for bump files and the--packagesCLI flag.