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/require-bump-file.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@varlock/bumpy': minor
---

Added `requireBumpFile: true` per-package config — `bumpy check` and `bumpy ci check` always fail when such a package changed but no bump file lists it, even if other bump files (or an empty bump file) are present. List it with type `none` to acknowledge a change without releasing.
2 changes: 1 addition & 1 deletion docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ With `--snapshot <name>`, publish derives a throwaway prerelease version per pen

Verify that changed packages on the current branch have corresponding bump files. Compares your branch to the base branch, maps changed files to packages, and checks for matching bump files.

By default, exits non-zero only if **no** bump files exist at all (matching changesets behavior). Use `--strict` to require every changed package to be covered.
By default, exits non-zero only if **no** bump files exist at all (matching changesets behavior). Use `--strict` to require every changed package to be covered, or set [`requireBumpFile`](./configuration.md#requirebumpfile--packages-that-must-always-be-bumped) on specific packages.

```bash
bumpy check
Expand Down
56 changes: 40 additions & 16 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,22 +178,23 @@ Per-package settings can be defined in two places:

Globs in per-package options (`changedFilePatterns`, `sourcePaths`) are always relative to the package's directory, wherever they're declared.

| Option | Type | Description |
| -------------------------- | -------------------------- | ---------------------------------------------------------------------------------------- |
| `managed` | `boolean` | Opt this package in or out of versioning |
| `directBump` | `boolean` | When `false`, the package only receives propagated bumps — never direct ones (see below) |
| `access` | `"public" \| "restricted"` | Override the global access level |
| `publishCommand` | `string \| string[]` | Custom command(s) to publish this package (replaces npm publish) |
| `buildCommand` | `string` | Command to run before publishing |
| `registry` | `string` | Custom npm registry URL |
| `skipNpmPublish` | `boolean` | Don't publish to npm (still creates git tags) |
| `checkPublished` | `string` | Custom command that outputs the currently published version |
| `changedFilePatterns` | `string[]` | Glob patterns for changed-file detection (replaces root setting, not merged) |
| `sourcePaths` | `string[]` | Globs for sources outside the package dir (see below) |
| `dependencyBumpRules` | `object` | Per-package override for dependency propagation rules |
| `cascadeTo` | `object` | Explicit cascade targets — glob pattern mapped to `{ trigger, bumpAs }` |
| `cascadeFrom` | `object` | Explicit cascade sources — glob pattern mapped to `{ trigger, bumpAs }` |
| `releaseTriggeringDevDeps` | `string[]` | devDependencies that affect published output — a change requires a release (see below) |
| Option | Type | Description |
| -------------------------- | -------------------------- | ---------------------------------------------------------------------------------------------- |
| `managed` | `boolean` | Opt this package in or out of versioning |
| `directBump` | `boolean` | When `false`, the package only receives propagated bumps — never direct ones (see below) |
| `requireBumpFile` | `boolean` | When `true`, `bumpy check` always fails if the package changed without a bump file (see below) |
| `access` | `"public" \| "restricted"` | Override the global access level |
| `publishCommand` | `string \| string[]` | Custom command(s) to publish this package (replaces npm publish) |
| `buildCommand` | `string` | Command to run before publishing |
| `registry` | `string` | Custom npm registry URL |
| `skipNpmPublish` | `boolean` | Don't publish to npm (still creates git tags) |
| `checkPublished` | `string` | Custom command that outputs the currently published version |
| `changedFilePatterns` | `string[]` | Glob patterns for changed-file detection (replaces root setting, not merged) |
| `sourcePaths` | `string[]` | Globs for sources outside the package dir (see below) |
| `dependencyBumpRules` | `object` | Per-package override for dependency propagation rules |
| `cascadeTo` | `object` | Explicit cascade targets — glob pattern mapped to `{ trigger, bumpAs }` |
| `cascadeFrom` | `object` | Explicit cascade sources — glob pattern mapped to `{ trigger, bumpAs }` |
| `releaseTriggeringDevDeps` | `string[]` | devDependencies that affect published output — a change requires a release (see below) |

### `sourcePaths` — sources outside the package directory

Expand Down Expand Up @@ -236,6 +237,29 @@ Effects:
- A bump file that directly names one (with a type other than `none`) is an error at plan time.
- `bumpy check` treats changes in a `directBump: false` package as covered when a bump file covers any other member of its fixed group, and points there when one is missing.

### `requireBumpFile` — packages that must always be bumped

By default `bumpy check` only fails when a branch has no bump file at all — a changed package missing from an otherwise-present bump file is just a warning. That's the right default for most packages, but some are only released when explicitly bumped (e.g. native helper binaries versioned independently of the package that uses them), so a forgotten bump silently skips a needed release.

Mark those packages `requireBumpFile: true`:

```jsonc
{
"packages": {
"@mycli/native-helper-*": { "requireBumpFile": true },
},
}
```

When such a package changed on the branch, `bumpy check` and `bumpy ci check` fail unless a bump file lists it — regardless of other bump files on the branch. The PR comment shows it as blocking.

- An empty bump file (`bumpy add --empty`) does **not** cover it.
- Listing it with type `none` does — use that to acknowledge a change that doesn't need a release.
- Propagated bumps (cascades, fixed/linked groups, dependency bumps) don't count; it must be listed explicitly.
- `--no-fail` still downgrades the failure to a warning.

The error names each package and prints the command to fix it, e.g. `bumpy add --packages "@mycli/native-helper-darwin:patch"` (or `:none`).

### Custom commands and `allowCustomCommands`

The `publishCommand`, `buildCommand`, and `checkPublished` fields run shell commands during publishing. Because these execute with CI credentials, bumpy distinguishes between two trust levels:
Expand Down
4 changes: 4 additions & 0 deletions packages/bumpy/config-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,10 @@
"type": "boolean",
"description": "When false, this package can never be bumped directly by a bump file — it only receives propagated bumps (fixed/linked group, cascade, dependency). Use for derived artifacts like platform binary packages kept in a fixed group with their core package."
},
"requireBumpFile": {
"type": "boolean",
"description": "When true, `bumpy check` and `bumpy ci check` always fail if this package changed but no bump file lists it — regardless of other bump files on the branch. An empty bump file does not satisfy it; listing the package with type `none` does. Propagated bumps don't count."
},
"access": {
"type": "string",
"enum": ["public", "restricted"],
Expand Down
40 changes: 34 additions & 6 deletions packages/bumpy/src/commands/check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ interface CheckOptions {
* Designed for pre-push hooks — no GitHub API needed.
*
* Default: at least one bump file must exist, uncovered packages are warned.
* Packages with `requireBumpFile: true` must always be listed in a bump file.
* --strict: every changed package must be covered.
* --no-fail: warn only, never exit 1.
* --hook pre-commit: only staged + committed bump files count.
Expand Down Expand Up @@ -172,10 +173,17 @@ export async function checkCommand(rootDir: string, opts: CheckOptions = {}): Pr
config,
);

// `requireBumpFile` packages must be listed in a bump file — nothing else covers them,
// including a fixed-group member's bump for a directBump: false package
const requiredMissing = changedPackages.filter(
(name) => packages.get(name)?.bumpy?.requireBumpFile && !coveredPackages.has(name),
);
for (const name of requiredMissing) if (!missing.includes(name)) missing.push(name);

// An empty bump file covers all remaining packages (in non-strict mode)
// It acts as a blanket acknowledgment that non-publishable changes are expected
const hasAnyCoverage = effectiveBumpFiles.length > 0 || hasEmptyBumpFile;
const allCovered = missing.length === 0 || (hasEmptyBumpFile && !opts.strict);
const allCovered = missing.length === 0 || (hasEmptyBumpFile && !opts.strict && requiredMissing.length === 0);

if (allCovered) {
if (hasEmptyBumpFile && missing.length > 0) {
Expand All @@ -197,11 +205,14 @@ export async function checkCommand(rootDir: string, opts: CheckOptions = {}): Pr
// Determine failure behavior
// - No coverage at all: fail by default (unless --no-fail)
// - Partial coverage: fail only with --strict (unless --no-fail)
const willFail = !opts.noFail && (opts.strict || !hasAnyCoverage);

(willFail ? log.error : log.warn)(`${missing.length} changed package(s) missing bump files:\n`);
for (const name of missing) {
const hint = hints.get(name);
// - A `requireBumpFile` package is missing: always fail (unless --no-fail)
const willFail = !opts.noFail && (opts.strict || !hasAnyCoverage || requiredMissing.length > 0);

// With an empty bump file, only the required packages are still uncovered
const reported = hasEmptyBumpFile && !opts.strict ? requiredMissing : missing;
(willFail ? log.error : log.warn)(`${reported.length} changed package(s) missing bump files:\n`);
for (const name of reported) {
const hint = hints.get(name) ?? (requiredMissing.includes(name) ? 'requires a bump file (requireBumpFile)' : '');
console.log(` ${colorize(name, 'yellow')}${hint ? ` — ${hint}` : ''}`);
}

Expand All @@ -215,6 +226,11 @@ export async function checkCommand(rootDir: string, opts: CheckOptions = {}): Pr
}

console.log();
if (requiredMissing.length > 0) {
log.dim('These packages require their own bump file — an empty bump file does not cover them. Run:');
for (const line of requiredBumpFileCommands(requiredMissing)) log.dim(` ${line}`);
console.log();
}
if (opts.strict) {
log.dim(
"Run `bumpy add` to create a bump file. Use bump type `none` for packages that changed but don't need a bump.",
Expand Down Expand Up @@ -246,6 +262,18 @@ function printBumpFileList(
}
}

/**
* Commands that satisfy `requireBumpFile` for the given packages — a patch bump, or a
* `none` entry to acknowledge the change without releasing.
*/
export function requiredBumpFileCommands(names: string[], runCmd = 'bumpy'): string[] {
const list = (type: string) => names.map((name) => `${name}:${type}`).join(',');
return [
`${runCmd} add --packages "${list('patch')}" # release it`,
`${runCmd} add --packages "${list('none')}" # acknowledge without releasing`,
];
}

/**
* Coverage adjustment for `directBump: false` packages. Such a package never gets its
* own bump file — its changes ship by bumping another member of its fixed group. So a
Expand Down
Loading
Loading