Skip to content

Commit dbbb719

Browse files
theoephraimclaude
andcommitted
Add bumpy check command for pre-push hook changeset verification
Compares changed files on branch vs base, maps them to packages, and exits non-zero if any changed packages are missing changesets. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent ec6b25b commit dbbb719

4 files changed

Lines changed: 117 additions & 0 deletions

File tree

DIFFERENCES_FROM_CHANGESETS.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,12 @@ Custom changelog formatters with full context (release info, changesets, dates).
121121

122122
`bumpy ci check` and `bumpy ci release` handle PR checks and release automation without needing a separate GitHub Action or bot installation. Just `bunx @varlock/bumpy ci check` in any workflow.
123123

124+
### Local changeset verification
125+
126+
`bumpy check` verifies that all changed packages on the current branch have corresponding changesets. Designed for pre-push hooks — compares your branch to the base branch, maps changed files to packages, and exits non-zero if any are missing. No GitHub API needed.
127+
128+
Changesets has no built-in equivalent — users rely on the CI bot comment to catch missing changesets after pushing.
129+
124130
---
125131

126132
## Planned / Not Yet Implemented

llms.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,20 @@ JSON output shape:
313313
}
314314
```
315315

316+
### `bumpy check`
317+
318+
Verify that all changed packages on the current branch have corresponding changesets. Compares files changed vs the base branch, maps them to managed packages, and exits non-zero if any are missing changesets.
319+
320+
Designed for pre-push hooks — no GitHub API needed.
321+
322+
```yaml
323+
# lefthook.yml
324+
pre-push:
325+
jobs:
326+
- name: bumpy-check
327+
run: bunx @varlock/bumpy check
328+
```
329+
316330
### `bumpy version`
317331

318332
Apply all pending changesets: bump versions in `package.json`, update `CHANGELOG.md`, delete consumed changeset files. Optionally creates a git commit if `commit: true` in config.

packages/bumpy/src/cli.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,13 @@ async function main() {
8989
break;
9090
}
9191

92+
case 'check': {
93+
const rootDir = await findRoot();
94+
const { checkCommand } = await import('./commands/check.ts');
95+
await checkCommand(rootDir);
96+
break;
97+
}
98+
9299
case 'ci': {
93100
const rootDir = await findRoot();
94101
const subcommand = args[1];
@@ -178,6 +185,7 @@ function printHelp() {
178185
add Create a new changeset
179186
generate Generate changeset from conventional commits
180187
status Show pending releases
188+
check Verify changed packages have changesets (for pre-push hooks)
181189
version Apply changesets and bump versions
182190
publish Publish versioned packages
183191
ci check PR check — report pending releases, comment on PR
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { relative } from 'node:path';
2+
import { log, colorize } from '../utils/logger.ts';
3+
import { loadConfig } from '../core/config.ts';
4+
import { discoverWorkspace } from '../core/workspace.ts';
5+
import { readChangesets } from '../core/changeset.ts';
6+
import { tryRun } from '../utils/shell.ts';
7+
import type { WorkspacePackage } from '../types.ts';
8+
9+
/**
10+
* Local check: detect which packages have changed on this branch
11+
* and verify they have corresponding changesets.
12+
* Designed for pre-push hooks — no GitHub API needed.
13+
*/
14+
export async function checkCommand(rootDir: string): Promise<void> {
15+
const config = await loadConfig(rootDir);
16+
const { packages } = await discoverWorkspace(rootDir, config);
17+
const changesets = await readChangesets(rootDir);
18+
19+
// Find which packages already have changesets
20+
const coveredPackages = new Set<string>();
21+
for (const cs of changesets) {
22+
for (const release of cs.releases) {
23+
coveredPackages.add(release.name);
24+
}
25+
}
26+
27+
// Find which packages have changed on this branch vs base
28+
const baseBranch = config.baseBranch;
29+
const changedFiles = getChangedFiles(rootDir, baseBranch);
30+
31+
if (changedFiles.length === 0) {
32+
log.info('No changed files detected.');
33+
return;
34+
}
35+
36+
const changedPackages = findChangedPackages(changedFiles, packages, rootDir);
37+
38+
if (changedPackages.length === 0) {
39+
log.info('No managed packages have changed.');
40+
return;
41+
}
42+
43+
// Check which changed packages are missing changesets
44+
const missing = changedPackages.filter((name) => !coveredPackages.has(name));
45+
46+
if (missing.length === 0) {
47+
log.success(`All ${changedPackages.length} changed package(s) have changesets.`);
48+
return;
49+
}
50+
51+
// Report missing
52+
log.warn(`${missing.length} changed package(s) missing changesets:\n`);
53+
for (const name of missing) {
54+
console.log(` ${colorize(name, 'yellow')}`);
55+
}
56+
console.log();
57+
log.dim('Run `bumpy add` to create a changeset, or `bumpy add --empty` if no release is needed.');
58+
process.exit(1);
59+
}
60+
61+
/** Get files changed on this branch compared to the base branch */
62+
function getChangedFiles(rootDir: string, baseBranch: string): string[] {
63+
// Try merge-base first (works on branches)
64+
const mergeBase = tryRun(`git merge-base HEAD origin/${baseBranch}`, { cwd: rootDir });
65+
const ref = mergeBase || `origin/${baseBranch}`;
66+
const diff = tryRun(`git diff --name-only ${ref}`, { cwd: rootDir });
67+
if (!diff) return [];
68+
return diff.split('\n').filter(Boolean);
69+
}
70+
71+
/** Map changed files to the packages they belong to */
72+
function findChangedPackages(
73+
changedFiles: string[],
74+
packages: Map<string, WorkspacePackage>,
75+
rootDir: string,
76+
): string[] {
77+
const changed = new Set<string>();
78+
79+
for (const file of changedFiles) {
80+
for (const [name, pkg] of packages) {
81+
const pkgRelDir = relative(rootDir, pkg.dir);
82+
if (file.startsWith(pkgRelDir + '/')) {
83+
changed.add(name);
84+
}
85+
}
86+
}
87+
88+
return [...changed];
89+
}

0 commit comments

Comments
 (0)