diff --git a/.bumpy/remove-patch-isolated.md b/.bumpy/remove-patch-isolated.md new file mode 100644 index 0000000..b048387 --- /dev/null +++ b/.bumpy/remove-patch-isolated.md @@ -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. diff --git a/DIFFERENCES_FROM_CHANGESETS.md b/DIFFERENCES_FROM_CHANGESETS.md index 165e7ca..3573488 100644 --- a/DIFFERENCES_FROM_CHANGESETS.md +++ b/DIFFERENCES_FROM_CHANGESETS.md @@ -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. diff --git a/docs/bump-files.md b/docs/bump-files.md index 99f5df5..942c13e 100644 --- a/docs/bump-files.md +++ b/docs/bump-files.md @@ -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 diff --git a/docs/version-propagation.md b/docs/version-propagation.md index aa66895..e8f2dd5 100644 --- a/docs/version-propagation.md +++ b/docs/version-propagation.md @@ -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 @@ -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. diff --git a/llms.md b/llms.md index 7a2674c..1124ce6 100644 --- a/llms.md +++ b/llms.md @@ -48,18 +48,7 @@ Bump files are markdown with YAML frontmatter, stored in `.bumpy/.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. @@ -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 @@ -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 diff --git a/packages/bumpy/skills/add-change/SKILL.md b/packages/bumpy/skills/add-change/SKILL.md index b62d4cb..c14bb89 100644 --- a/packages/bumpy/skills/add-change/SKILL.md +++ b/packages/bumpy/skills/add-change/SKILL.md @@ -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 diff --git a/packages/bumpy/src/commands/add.ts b/packages/bumpy/src/commands/add.ts index f8f25f9..b03accc 100644 --- a/packages/bumpy/src/commands/add.ts +++ b/packages/bumpy/src/commands/add.ts @@ -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; @@ -99,8 +99,8 @@ export async function addCommand(rootDir: string, opts: AddOptions): Promise { @@ -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 }; + const obj = value as { bump: BumpTypeWithNone; cascade?: Record }; const release: BumpFileReleaseCascade = { name, type: obj.bump, diff --git a/packages/bumpy/src/core/release-plan.ts b/packages/bumpy/src/core/release-plan.ts index ec4de9d..6552596 100644 --- a/packages/bumpy/src/core/release-plan.ts +++ b/packages/bumpy/src/core/release-plan.ts @@ -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; @@ -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); @@ -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, @@ -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, @@ -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 ^ for checking @@ -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; } } } @@ -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, @@ -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')) { @@ -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); @@ -440,7 +419,6 @@ function applyBump( } planned.set(name, { type, - isolated: false, suppressed: false, isDependencyBump, isCascadeBump, diff --git a/packages/bumpy/src/prompts/bump-select.ts b/packages/bumpy/src/prompts/bump-select.ts index 88ee645..2a7e590 100644 --- a/packages/bumpy/src/prompts/bump-select.ts +++ b/packages/bumpy/src/prompts/bump-select.ts @@ -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']; @@ -14,7 +14,7 @@ export interface BumpSelectItem { export interface BumpSelectResult { name: string; - type: BumpTypeWithIsolated; + type: BumpTypeWithNone; } /** @@ -139,7 +139,7 @@ export async function bumpSelectPrompt(items: BumpSelectItem[]): Promise = { patch: 0, @@ -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; @@ -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; // glob pattern → bump type } diff --git a/packages/bumpy/test/core/bump-file.test.ts b/packages/bumpy/test/core/bump-file.test.ts index 9f47e8a..12f9ee1 100644 --- a/packages/bumpy/test/core/bump-file.test.ts +++ b/packages/bumpy/test/core/bump-file.test.ts @@ -21,17 +21,6 @@ Added a new feature to pkg-a expect(bf!.summary).toBe('Added a new feature to pkg-a'); }); - test('parses patch-isolated bump type', () => { - const content = `--- -"pkg-a": patch-isolated ---- - -Internal change -`; - const bf = parseBumpFile(content, 'test-bf'); - expect(bf!.releases[0]!.type).toBe('patch-isolated'); - }); - test('parses none bump type', () => { const content = `--- "pkg-a": minor diff --git a/packages/bumpy/test/core/release-plan.test.ts b/packages/bumpy/test/core/release-plan.test.ts index 63ff79e..0abaa9f 100644 --- a/packages/bumpy/test/core/release-plan.test.ts +++ b/packages/bumpy/test/core/release-plan.test.ts @@ -183,25 +183,6 @@ describe('assembleReleasePlan', () => { expect(plan.warnings[0]).toContain('^0.2.0'); }); - test('Phase A runs even for patch-isolated bumps (but isolated blocks propagation → error)', () => { - const packages = new Map([ - ['core', makePkg('core', '1.0.0')], - ['app', makePkg('app', '1.0.0', { dependencies: { core: '~1.0.0' } })], - ]); - - // patch-isolated on core: 1.0.0 → 1.0.1, satisfies ~1.0.0 → no issue - const bumpFiles: BumpFile[] = [ - { id: 'cs1', releases: [{ name: 'core', type: 'patch-isolated' }], summary: 'Internal' }, - ]; - - const graph = new DependencyGraph(packages); - const plan = assembleReleasePlan(bumpFiles, packages, graph, makeConfig()); - - expect(plan.releases).toHaveLength(1); - expect(plan.releases[0]!.name).toBe('core'); - expect(plan.releases[0]!.newVersion).toBe('1.0.1'); - }); - test('skips propagation when version still satisfies range', () => { const packages = new Map([ ['core', makePkg('core', '1.0.0')], @@ -357,23 +338,6 @@ describe('assembleReleasePlan', () => { expect(appRelease.isDependencyBump).toBe(true); }); - test('proactive propagation skipped for patch-isolated bumps', () => { - const packages = new Map([ - ['core', makePkg('core', '1.0.0')], - ['app', makePkg('app', '2.0.0', { dependencies: { core: '^1.0.0' } })], - ]); - - const bumpFiles: BumpFile[] = [ - { id: 'cs1', releases: [{ name: 'core', type: 'patch-isolated' }], summary: 'Internal' }, - ]; - - const graph = new DependencyGraph(packages); - const plan = assembleReleasePlan(bumpFiles, packages, graph, makeConfig({ updateInternalDependencies: 'patch' })); - - expect(plan.releases).toHaveLength(1); - expect(plan.releases[0]!.name).toBe('core'); - }); - test('peer dependency minor bump does NOT propagate by default (out-of-range mode, range satisfied)', () => { const packages = new Map([ ['core', makePkg('core', '1.0.0')], @@ -442,61 +406,6 @@ describe('assembleReleasePlan', () => { }); }); - // ---- patch-isolated behavior ---- - - describe('patch-isolated', () => { - test('patch-isolated skips Phase C propagation', () => { - const packages = new Map([ - ['core', makePkg('core', '1.0.0')], - ['app', makePkg('app', '2.0.0', { dependencies: { core: '^1.0.0' } })], - ]); - - const bumpFiles: BumpFile[] = [ - { id: 'cs1', releases: [{ name: 'core', type: 'patch-isolated' }], summary: 'Internal' }, - ]; - - const graph = new DependencyGraph(packages); - const plan = assembleReleasePlan(bumpFiles, packages, graph, makeConfig()); - - expect(plan.releases).toHaveLength(1); - expect(plan.releases[0]!.name).toBe('core'); - expect(plan.releases[0]!.newVersion).toBe('1.0.1'); - }); - - test('non-isolated bump file overrides isolated for same package', () => { - const packages = new Map([ - ['core', makePkg('core', '1.0.0')], - ['app', makePkg('app', '2.0.0', { dependencies: { core: '^1.0.0' } })], - ]); - - const bumpFiles: BumpFile[] = [ - { id: 'cs1', releases: [{ name: 'core', type: 'patch-isolated' }], summary: 'Internal' }, - { id: 'cs2', releases: [{ name: 'core', type: 'patch' }], summary: 'Fix' }, - ]; - - const graph = new DependencyGraph(packages); - const plan = assembleReleasePlan(bumpFiles, packages, graph, makeConfig({ updateInternalDependencies: 'patch' })); - - expect(plan.releases).toHaveLength(2); // core + app (no longer isolated) - }); - - test('patch-isolated that would break range throws error', () => { - const packages = new Map([ - ['core', makePkg('core', '1.0.0')], - ['app', makePkg('app', '1.0.0', { dependencies: { core: '1.0.0' } })], // exact range - ]); - - const bumpFiles: BumpFile[] = [ - { id: 'cs1', releases: [{ name: 'core', type: 'patch-isolated' }], summary: 'Internal' }, - ]; - - const graph = new DependencyGraph(packages); - expect(() => assembleReleasePlan(bumpFiles, packages, graph, makeConfig())).toThrow( - /patch-isolated.*break.*range/, - ); - }); - }); - // ---- none behavior ---- describe('none suppression', () => {