Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .bumpy/remove-patch-isolated.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@varlock/bumpy': minor
---

Removed `patch-isolated` bump type. The concept added complexity for minimal benefit — in most monorepos using `^` ranges, a patch bump already stays in range without triggering propagation. Users who need to prevent propagation can use per-package `dependencyBumpRules` config instead.
2 changes: 1 addition & 1 deletion DIFFERENCES_FROM_CHANGESETS.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Key differences from changesets:
- Out-of-range peer dep bumps match the triggering bump level (not always major) — a minor bump on `core` → minor bump on `plugin`, not major
- Dev deps never propagate by default (configurable per-package for bundled devDeps)
- `cascadeTo` config for source-side "when I change, cascade to these packages"
- Per-bump-file `none` and `patch-isolated` to suppress propagation on specific changes
- Per-bump-file `none` to suppress propagation on specific changes
- Warns about `^0.x` caret range gotchas and `workspace:*` on peer deps

See [docs/version-propagation.md](docs/version-propagation.md) for the full algorithm.
Expand Down
13 changes: 6 additions & 7 deletions docs/bump-files.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,12 @@ Fixed locale fallback logic in utils.

### Bump levels

| Level | When to use |
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `major` | Breaking changes |
| `minor` | New features (backwards-compatible) |
| `patch` | Bug fixes, minor improvements |
| `patch-isolated` | Like `patch`, but skips dependency propagation (Phase C). Useful for internal fixes that shouldn't trigger downstream bumps. |
| `none` | Suppresses a bump — used in cascades to exclude specific packages from propagation (advanced) |
| Level | When to use |
| ------- | --------------------------------------------------------------------------------------------- |
| `major` | Breaking changes |
| `minor` | New features (backwards-compatible) |
| `patch` | Bug fixes, minor improvements |
| `none` | Suppresses a bump — used in cascades to exclude specific packages from propagation (advanced) |

## File naming

Expand Down
11 changes: 1 addition & 10 deletions docs/version-propagation.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,15 +163,6 @@ Unlike dependency bump rules (configured on the _dependent_), `cascadeTo` is con

These are set directly in bump files for one-off control over a specific release.

**`patch-isolated`** — bumps the package as a patch but skips all Phase C propagation from it (cascades and proactive bumps). If the bump would break a dependent's declared range, bumpy throws an error rather than silently propagating — you'll need to either widen the range, drop the `-isolated` flag, or explicitly bump the dependent in the bump file.

```yaml
---
'@myorg/utils': patch-isolated
---
Internal refactor, no API changes.
```

**`none`** — suppresses a bump on a package that would otherwise be included via propagation. If skipping the bump would leave a dependent's range broken, bumpy throws an error.

```yaml
Expand Down Expand Up @@ -202,4 +193,4 @@ Compare with listing packages directly — these are treated as independent chan
---
```

> **Note:** `patch-isolated`, `none`, and bump-file-level cascades are not available in the interactive `bumpy add` UI — they are power-user features for bump files and the `--packages` CLI flag.
> **Note:** `none` and bump-file-level cascades are not available in the interactive `bumpy add` UI — they are power-user features for bump files and the `--packages` CLI flag.
24 changes: 3 additions & 21 deletions llms.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,18 +48,7 @@ Bump files are markdown with YAML frontmatter, stored in `.bumpy/<name>.md`.
Added new encryption provider for secrets management.
```

### Isolated bumps (skip dependency propagation)

```yaml
---
'@myorg/utils': patch-isolated
---
Internal refactor — no API changes, dependents don't need to bump.
```

Valid bump types: `major`, `minor`, `patch`, `patch-isolated`, `none`

`patch-isolated` bumps as a patch but skips Phase C propagation. If the bump would break a dependent's range, bumpy throws an error.
Valid bump types: `major`, `minor`, `patch`, `none`

`none` suppresses a bump on a package that would otherwise be included via propagation. If skipping would leave a broken range, bumpy throws an error.

Expand Down Expand Up @@ -527,14 +516,7 @@ Both `workspace:` (pnpm, bun, yarn) and `catalog:` (pnpm, bun) protocols are res

### Internal packages that should never propagate bumps

```yaml
---
'@myorg/internal-utils': patch-isolated
---
Refactored internal helpers.
```

Or permanently via config:
Configure via per-package dependency bump rules:

```json
// In root .bumpy/_config.json
Expand Down Expand Up @@ -693,7 +675,7 @@ This will:
Key behavioral differences after migration:

- Out-of-range peer dep bumps match the triggering bump level (not always major)
- Use `patch-isolated` to skip Phase C propagation, or `none` to suppress a propagated bump
- Use `none` to suppress a propagated bump
- Per-package config moves to `package.json["bumpy"]` instead of root config only

## AI Integration
Expand Down
6 changes: 0 additions & 6 deletions packages/bumpy/skills/add-change/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,6 @@ For each affected package, choose the appropriate bump level:
| **minor** | New features: added exports, new options, new functionality |
| **patch** | Bug fixes, internal refactors, documentation, dependency updates |

Use `patch-isolated` if the change is purely internal and dependents should NOT be bumped (skips Phase C propagation). If the bump would break a dependent's declared range, bumpy will throw an error. Use this for:

- Internal refactors with no API changes
- Dev tooling / test changes
- Documentation-only changes

Use `none` in a bump file to suppress a bump on a package that would otherwise be included via propagation. If skipping would leave a broken range, bumpy throws an error.

### 4. Write a clear summary
Expand Down
10 changes: 5 additions & 5 deletions packages/bumpy/src/commands/add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ import { matchGlob } from '../core/config.ts';
import { getChangedFiles } from '../core/git.ts';
import { bumpSelectPrompt } from '../prompts/bump-select.ts';
import type { BumpSelectItem } from '../prompts/bump-select.ts';
import type { BumpType, BumpTypeWithIsolated, BumpFileRelease, BumpFileReleaseCascade } from '../types.ts';
import type { BumpType, BumpTypeWithNone, BumpFileRelease, BumpFileReleaseCascade } from '../types.ts';

interface AddOptions {
packages?: string; // "pkg-a:minor,pkg-b:patch-isolated"
packages?: string; // "pkg-a:minor,pkg-b:patch"
message?: string;
name?: string;
empty?: boolean;
Expand Down Expand Up @@ -99,8 +99,8 @@ export async function addCommand(rootDir: string, opts: AddOptions): Promise<voi
for (const { name, type: bumpType } of bumpSelections) {
const release: BumpFileRelease = { name, type: bumpType };

// Offer cascade options if the package has dependents and bump is not isolated
if (!bumpType.endsWith('-isolated')) {
// Offer cascade options if the package has dependents
{
const dependents = depGraph.getDependents(name);
const pkg = pkgs.get(name)!;
const cascadeTargets = pkg.bumpy?.cascadeTo;
Expand Down Expand Up @@ -212,6 +212,6 @@ function parsePackagesFlag(input: string): BumpFileRelease[] {
if (!name || !type) {
throw new Error(`Invalid package format: "${entry}". Expected "name:bumpType"`);
}
return { name: name.trim(), type: type.trim() as BumpTypeWithIsolated };
return { name: name.trim(), type: type.trim() as BumpTypeWithNone };
});
}
4 changes: 2 additions & 2 deletions packages/bumpy/src/commands/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { writeBumpFile } from '../core/bump-file.ts';
import { getBumpyDir } from '../core/config.ts';
import { ensureDir } from '../utils/fs.ts';
import { slugify, randomName } from '../utils/names.ts';
import type { BumpType, BumpTypeWithIsolated, BumpyConfig, BumpFileRelease, WorkspacePackage } from '../types.ts';
import type { BumpType, BumpTypeWithNone, BumpyConfig, BumpFileRelease, WorkspacePackage } from '../types.ts';

interface GenerateOptions {
from?: string; // git ref to start from (default: auto-detect last version tag)
Expand Down Expand Up @@ -116,7 +116,7 @@ export async function generateCommand(rootDir: string, opts: GenerateOptions): P
const summaryLines: string[] = [];

for (const [name, info] of releaseMap) {
releases.push({ name, type: info.type as BumpTypeWithIsolated });
releases.push({ name, type: info.type as BumpTypeWithNone });
for (const msg of info.messages) {
summaryLines.push(`- ${name}: ${msg}`);
}
Expand Down
5 changes: 2 additions & 3 deletions packages/bumpy/src/commands/migrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { getBumpyDir } from '../core/config.ts';
import { writeBumpFile } from '../core/bump-file.ts';
import { p, unwrap } from '../utils/clack.ts';
import { initCommand } from './init.ts';
import type { BumpFileRelease, BumpTypeWithIsolated } from '../types.ts';
import type { BumpFileRelease, BumpTypeWithNone } from '../types.ts';

interface MigrateOptions {
force?: boolean;
Expand Down Expand Up @@ -94,7 +94,6 @@ export async function migrateCommand(rootDir: string, opts: MigrateOptions): Pro
log.dim('Review .bumpy/_config.json and adjust settings as needed.');
log.dim('Key differences from changesets:');
log.dim(' - Out-of-range peer dep bumps match the triggering bump level (not always major)');
log.dim(" - Use 'patch-isolated' to skip Phase C propagation");
log.dim(" - Use 'none' in a bump file to suppress a propagated bump");
log.dim(' - Per-package config goes in package.json["bumpy"]');
}
Expand Down Expand Up @@ -166,7 +165,7 @@ function parseChangesetFile(content: string): { releases: BumpFileRelease[]; sum
if (type === 'none') continue;

if (['major', 'minor', 'patch'].includes(type)) {
releases.push({ name, type: type as BumpTypeWithIsolated });
releases.push({ name, type: type as BumpTypeWithNone });
}
}

Expand Down
6 changes: 3 additions & 3 deletions packages/bumpy/src/core/bump-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import yaml from 'js-yaml';
import { readText, writeText, listFiles, removeFile } from '../utils/fs.ts';
import { getBumpyDir } from './config.ts';
import { tryRunArgs } from '../utils/shell.ts';
import type { BumpFile, BumpFileRelease, BumpFileReleaseCascade, BumpType, BumpTypeWithIsolated } from '../types.ts';
import type { BumpFile, BumpFileRelease, BumpFileReleaseCascade, BumpType, BumpTypeWithNone } from '../types.ts';

/** Read all bump files from .bumpy/ directory, sorted by git creation order */
export async function readBumpFiles(rootDir: string): Promise<BumpFile[]> {
Expand Down Expand Up @@ -83,10 +83,10 @@ export function parseBumpFile(content: string, id: string): BumpFile | null {
for (const [name, value] of Object.entries(parsed)) {
if (typeof value === 'string') {
// Simple format: "pkg-name": minor
releases.push({ name, type: value as BumpTypeWithIsolated });
releases.push({ name, type: value as BumpTypeWithNone });
} else if (value && typeof value === 'object') {
// Nested format: "pkg-name": { bump: minor, cascade: { ... } }
const obj = value as { bump: BumpTypeWithIsolated; cascade?: Record<string, BumpType> };
const obj = value as { bump: BumpTypeWithNone; cascade?: Record<string, BumpType> };
const release: BumpFileReleaseCascade = {
name,
type: obj.bump,
Expand Down
24 changes: 1 addition & 23 deletions packages/bumpy/src/core/release-plan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,11 @@ import {
DEFAULT_BUMP_RULES,
bumpLevel,
maxBump,
parseIsolatedBump,
hasCascade,
} from '../types.ts';

interface PlannedBump {
type: BumpType;
isolated: boolean;
/** Explicit 'none' from bump file — suppresses propagation bumps */
suppressed: boolean;
isDependencyBump: boolean;
Expand Down Expand Up @@ -56,7 +54,7 @@ export function assembleReleasePlan(
for (const bf of bumpFiles) {
for (const release of bf.releases) {
if (!packages.has(release.name)) continue;
const { bump, isolated } = parseIsolatedBump(release.type);
const bump = release.type;

if (bump === 'none') {
suppressedPackages.add(release.name);
Expand All @@ -66,13 +64,10 @@ export function assembleReleasePlan(
const existing = planned.get(release.name);
if (existing) {
existing.type = maxBump(existing.type, bump);
// If ANY bump file is non-isolated, the package is non-isolated
if (!isolated) existing.isolated = false;
existing.bumpFiles.add(bf.id);
} else {
planned.set(release.name, {
type: bump,
isolated,
suppressed: false,
isDependencyBump: false,
isCascadeBump: false,
Expand Down Expand Up @@ -101,7 +96,6 @@ export function assembleReleasePlan(
// This prevents propagation from adding this package
planned.set(name, {
type: 'patch', // placeholder, won't be used
isolated: false,
suppressed: true,
isDependencyBump: false,
isCascadeBump: false,
Expand Down Expand Up @@ -155,15 +149,6 @@ export function assembleReleasePlan(
);
}

// Check if isolated would break range
if (bump.isolated) {
throw new Error(
`'patch-isolated' bump for '${pkgName}' would break the range '${dep.versionRange}' ` +
`declared by '${dep.name}'. Either widen the range, drop '-isolated', ` +
`or explicitly bump '${dep.name}' in the bump file.`,
);
}

// Warn about ^0.x peer dep propagation
if (dep.depType === 'peerDependencies' && depBump !== 'patch') {
// Resolve workspace:^ to ^<version> for checking
Expand All @@ -189,13 +174,11 @@ export function assembleReleasePlan(
// Phase B: Enforce fixed/linked group constraints
for (const group of config.fixed) {
let groupBump: BumpType | undefined;
let groupIsolated = true;
for (const nameOrGlob of group) {
for (const [name, bump] of planned) {
if (bump.suppressed) continue;
if (matchGlob(name, nameOrGlob)) {
groupBump = maxBump(groupBump, bump.type);
if (!bump.isolated) groupIsolated = false;
}
}
}
Expand All @@ -209,13 +192,11 @@ export function assembleReleasePlan(
const newType = maxBump(existing.type, groupBump);
if (newType !== existing.type) {
existing.type = newType;
existing.isolated = groupIsolated;
changed = true;
}
} else {
planned.set(name, {
type: groupBump,
isolated: groupIsolated,
suppressed: false,
isDependencyBump: false,
isCascadeBump: false,
Expand Down Expand Up @@ -257,7 +238,6 @@ export function assembleReleasePlan(
if (config.updateInternalDependencies !== 'out-of-range') {
for (const [pkgName, bump] of planned) {
if (bump.suppressed) continue;
if (bump.isolated) continue;

// Check minimum threshold for proactive propagation
if (config.updateInternalDependencies === 'minor' && bumpLevel(bump.type) < bumpLevel('minor')) {
Expand Down Expand Up @@ -317,7 +297,6 @@ export function assembleReleasePlan(
// Even in out-of-range mode, still apply bump file cascades and cascadeTo
for (const [pkgName, bump] of planned) {
if (bump.suppressed) continue;
if (bump.isolated) continue;

// Bump-file-level cascade overrides always apply
const bfOverrides = cascadeOverrides.get(pkgName);
Expand Down Expand Up @@ -440,7 +419,6 @@ function applyBump(
}
planned.set(name, {
type,
isolated: false,
suppressed: false,
isDependencyBump,
isCascadeBump,
Expand Down
8 changes: 4 additions & 4 deletions packages/bumpy/src/prompts/bump-select.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import * as readline from 'node:readline';
import pc from 'picocolors';
import type { BumpTypeWithIsolated } from '../types.ts';
import type { BumpTypeWithNone } from '../types.ts';

export type BumpLevel = BumpTypeWithIsolated | 'none';
export type BumpLevel = BumpTypeWithNone | 'none';

const LEVELS: BumpLevel[] = ['none', 'patch', 'minor', 'major'];

Expand All @@ -14,7 +14,7 @@ export interface BumpSelectItem {

export interface BumpSelectResult {
name: string;
type: BumpTypeWithIsolated;
type: BumpTypeWithNone;
}

/**
Expand Down Expand Up @@ -139,7 +139,7 @@ export async function bumpSelectPrompt(items: BumpSelectItem[]): Promise<BumpSel
const results: BumpSelectResult[] = [];
for (let i = 0; i < items.length; i++) {
if (levels[i] !== 'none') {
results.push({ name: items[i]!.name, type: levels[i] as BumpTypeWithIsolated });
results.push({ name: items[i]!.name, type: levels[i] as BumpTypeWithNone });
}
}
finish(results);
Expand Down
16 changes: 3 additions & 13 deletions packages/bumpy/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// ---- Bump types ----

export type BumpType = 'major' | 'minor' | 'patch';
export type BumpTypeWithIsolated = BumpType | 'patch-isolated' | 'none';
export type BumpTypeWithNone = BumpType | 'none';

export const BUMP_LEVELS: Record<BumpType, number> = {
patch: 0,
Expand All @@ -13,16 +13,6 @@ export function bumpLevel(type: BumpType): number {
return BUMP_LEVELS[type];
}

export function parseIsolatedBump(type: BumpTypeWithIsolated): { bump: BumpType | 'none'; isolated: boolean } {
if (type === 'none') {
return { bump: 'none', isolated: false };
}
if (type.endsWith('-isolated')) {
return { bump: type.replace('-isolated', '') as BumpType, isolated: true };
}
return { bump: type as BumpType, isolated: false };
}

export function maxBump(a: BumpType | undefined, b: BumpType): BumpType {
if (!a) return b;
return bumpLevel(a) >= bumpLevel(b) ? a : b;
Expand Down Expand Up @@ -155,12 +145,12 @@ export const DEFAULT_CONFIG: BumpyConfig = {

export interface BumpFileReleaseSimple {
name: string;
type: BumpTypeWithIsolated;
type: BumpTypeWithNone;
}

export interface BumpFileReleaseCascade {
name: string;
type: BumpTypeWithIsolated;
type: BumpTypeWithNone;
cascade: Record<string, BumpType>; // glob pattern → bump type
}

Expand Down
Loading