diff --git a/.bumpy/empty-bump-file-improvements.md b/.bumpy/empty-bump-file-improvements.md
new file mode 100644
index 0000000..c2ca4bc
--- /dev/null
+++ b/.bumpy/empty-bump-file-improvements.md
@@ -0,0 +1,5 @@
+---
+'@varlock/bumpy': patch
+---
+
+Improve empty bump file handling — show file links and list alongside valid bump files
diff --git a/packages/bumpy/src/commands/check.ts b/packages/bumpy/src/commands/check.ts
index d1c077c..c1f6875 100644
--- a/packages/bumpy/src/commands/check.ts
+++ b/packages/bumpy/src/commands/check.ts
@@ -42,10 +42,10 @@ export async function checkCommand(rootDir: string, opts: CheckOptions = {}): Pr
}
process.exit(1);
}
- const { branchBumpFiles, hasEmptyBumpFile } = filterBranchBumpFiles(allBumpFiles, changedFiles, rootDir);
+ const { branchBumpFiles, emptyBumpFileIds } = filterBranchBumpFiles(allBumpFiles, changedFiles, rootDir);
// If an empty bump file exists on this branch, the check passes
- if (hasEmptyBumpFile) {
+ if (emptyBumpFileIds.length > 0) {
log.success('Empty bump file found — no releases needed.');
return;
}
diff --git a/packages/bumpy/src/commands/ci.ts b/packages/bumpy/src/commands/ci.ts
index d0c2534..c9dca2a 100644
--- a/packages/bumpy/src/commands/ci.ts
+++ b/packages/bumpy/src/commands/ci.ts
@@ -110,7 +110,7 @@ export async function ciCheckCommand(rootDir: string, opts: CheckOptions): Promi
// Filter to only bump files added/modified in this PR
const changedFiles = getChangedFiles(rootDir, config.baseBranch);
- const { branchBumpFiles: prBumpFiles, hasEmptyBumpFile } = filterBranchBumpFiles(
+ const { branchBumpFiles: prBumpFiles, emptyBumpFileIds } = filterBranchBumpFiles(
allBumpFiles,
changedFiles,
rootDir,
@@ -126,10 +126,16 @@ export async function ciCheckCommand(rootDir: string, opts: CheckOptions): Promi
if (prBumpFiles.length === 0) {
// An empty bump file signals intentionally no releases needed
- if (hasEmptyBumpFile && parseErrors.length === 0) {
+ if (emptyBumpFileIds.length > 0 && parseErrors.length === 0) {
log.success('Empty bump file found — no releases needed.');
if (shouldComment && prNumber) {
- await postOrUpdatePrComment(prNumber, formatEmptyBumpFileComment(), rootDir, opts.patComments);
+ const prBranch = detectPrBranch(rootDir);
+ await postOrUpdatePrComment(
+ prNumber,
+ formatEmptyBumpFileComment(emptyBumpFileIds, prNumber, prBranch),
+ rootDir,
+ opts.patComments,
+ );
}
return;
}
@@ -175,7 +181,16 @@ export async function ciCheckCommand(rootDir: string, opts: CheckOptions): Promi
// Comment on PR
if (shouldComment && prNumber) {
const prBranch = detectPrBranch(rootDir);
- const comment = formatReleasePlanComment(plan, prBumpFiles, prNumber, prBranch, pm, plan.warnings, parseErrors);
+ const comment = formatReleasePlanComment(
+ plan,
+ prBumpFiles,
+ prNumber,
+ prBranch,
+ pm,
+ plan.warnings,
+ parseErrors,
+ emptyBumpFileIds,
+ );
await postOrUpdatePrComment(prNumber, comment, rootDir, opts.patComments);
}
@@ -492,6 +507,7 @@ function formatReleasePlanComment(
pm: PackageManager,
warnings: string[] = [],
parseErrors: string[] = [],
+ emptyBumpFileIds: string[] = [],
): string {
const repo = process.env.GITHUB_REPOSITORY;
const lines: string[] = [];
@@ -540,6 +556,19 @@ function formatReleasePlanComment(
}
lines.push(`- ${parts.join(' ')}`);
}
+ for (const id of emptyBumpFileIds) {
+ const filename = `${id}.md`;
+ const parts: string[] = [`\`${filename}\` _(empty — no release)_`];
+ if (repo) {
+ parts.push(
+ `([view diff](https://github.com/${repo}/pull/${prNumber}/changes#diff-${sha256Hex(`.bumpy/${filename}`)}))`,
+ );
+ if (prBranch) {
+ parts.push(`([edit](https://github.com/${repo}/edit/${prBranch}/.bumpy/${filename}))`);
+ }
+ }
+ lines.push(`- ${parts.join(' ')}`);
+ }
lines.push('');
if (parseErrors.length > 0) {
@@ -600,15 +629,32 @@ function formatBumpFileErrorsComment(errors: string[], prBranch: string | null,
return lines.join('\n');
}
-function formatEmptyBumpFileComment(): string {
+function formatEmptyBumpFileComment(emptyBumpFileIds: string[], prNumber: string, prBranch: string | null): string {
+ const repo = process.env.GITHUB_REPOSITORY;
const lines = [
`
`,
'',
'**This PR includes an empty bump file — no version bump is needed.** :white_check_mark:',
'
',
- '\n---',
- `_This comment is maintained by [bumpy](https://bumpy.varlock.dev)._`,
+ '',
];
+
+ for (const id of emptyBumpFileIds) {
+ const filename = `${id}.md`;
+ const parts: string[] = [`\`${filename}\``];
+ if (repo) {
+ parts.push(
+ `([view diff](https://github.com/${repo}/pull/${prNumber}/changes#diff-${sha256Hex(`.bumpy/${filename}`)}))`,
+ );
+ if (prBranch) {
+ parts.push(`([edit](https://github.com/${repo}/edit/${prBranch}/.bumpy/${filename}))`);
+ }
+ }
+ lines.push(`- ${parts.join(' ')}`);
+ }
+
+ lines.push('\n---');
+ lines.push(`_This comment is maintained by [bumpy](https://bumpy.varlock.dev)._`);
return lines.join('\n');
}
diff --git a/packages/bumpy/src/core/bump-file.ts b/packages/bumpy/src/core/bump-file.ts
index 0e5bfc2..5c49c53 100644
--- a/packages/bumpy/src/core/bump-file.ts
+++ b/packages/bumpy/src/core/bump-file.ts
@@ -240,14 +240,14 @@ export function filterBranchBumpFiles(
changedFiles: string[],
rootDir?: string,
parseErrors: string[] = [],
-): { branchBumpFiles: BumpFile[]; branchBumpFileIds: Set; hasEmptyBumpFile: boolean } {
+): { branchBumpFiles: BumpFile[]; branchBumpFileIds: Set; emptyBumpFileIds: string[] } {
const branchBumpFileIds = extractBumpFileIdsFromChangedFiles(changedFiles);
const branchBumpFiles = allBumpFiles.filter((bf) => branchBumpFileIds.has(bf.id));
- // Check if any changed bump file IDs that didn't parse still exist on disk (= empty bump file).
+ // Find changed bump file IDs that didn't parse but still exist on disk (= empty bump file).
// Deleted bump files (from other branches) should not count.
// Files that produced parse errors are broken, not intentionally empty.
- let hasEmptyBumpFile = false;
+ const emptyBumpFileIds: string[] = [];
if (rootDir) {
const parsedIds = new Set(branchBumpFiles.map((bf) => bf.id));
const bumpyDir = getBumpyDir(rootDir);
@@ -256,12 +256,11 @@ export function filterBranchBumpFiles(
// Check if this file produced parse errors — if so, it's broken, not empty
const hasErrors = parseErrors.some((e) => e.includes(`"${id}"`));
if (!hasErrors) {
- hasEmptyBumpFile = true;
- break;
+ emptyBumpFileIds.push(id);
}
}
}
}
- return { branchBumpFiles, branchBumpFileIds, hasEmptyBumpFile };
+ return { branchBumpFiles, branchBumpFileIds, emptyBumpFileIds };
}
diff --git a/packages/bumpy/test/core/check.test.ts b/packages/bumpy/test/core/check.test.ts
index 775355a..37799a1 100644
--- a/packages/bumpy/test/core/check.test.ts
+++ b/packages/bumpy/test/core/check.test.ts
@@ -49,12 +49,12 @@ describe('filterBranchBumpFiles', () => {
expect(branchBumpFiles).toHaveLength(2);
});
- test('hasEmptyBumpFile is false when no rootDir provided', () => {
+ test('emptyBumpFileIds is false when no rootDir provided', () => {
const all = [makeBumpFile('change-a', [{ name: 'pkg-a', type: 'minor' }])];
const changed = ['.bumpy/change-a.md', '.bumpy/empty-one.md'];
- const { hasEmptyBumpFile } = filterBranchBumpFiles(all, changed);
- expect(hasEmptyBumpFile).toBe(false);
+ const { emptyBumpFileIds } = filterBranchBumpFiles(all, changed);
+ expect(emptyBumpFileIds).toHaveLength(0);
});
describe('with rootDir (empty bump file detection)', () => {
@@ -76,8 +76,8 @@ describe('filterBranchBumpFiles', () => {
const all = [makeBumpFile('change-a', [{ name: 'pkg-a', type: 'minor' }])];
const changed = ['.bumpy/change-a.md', '.bumpy/empty-one.md'];
- const { hasEmptyBumpFile } = filterBranchBumpFiles(all, changed, tmpDir);
- expect(hasEmptyBumpFile).toBe(true);
+ const { emptyBumpFileIds } = filterBranchBumpFiles(all, changed, tmpDir);
+ expect(emptyBumpFileIds).toHaveLength(1);
});
test('does not detect deleted bump file as empty', async () => {
@@ -85,16 +85,16 @@ describe('filterBranchBumpFiles', () => {
const all = [makeBumpFile('change-a', [{ name: 'pkg-a', type: 'minor' }])];
const changed = ['.bumpy/change-a.md', '.bumpy/empty-one.md'];
- const { hasEmptyBumpFile } = filterBranchBumpFiles(all, changed, tmpDir);
- expect(hasEmptyBumpFile).toBe(false);
+ const { emptyBumpFileIds } = filterBranchBumpFiles(all, changed, tmpDir);
+ expect(emptyBumpFileIds).toHaveLength(0);
});
test('does not flag non-empty bump files as empty', async () => {
const all = [makeBumpFile('change-a', [{ name: 'pkg-a', type: 'minor' }])];
const changed = ['.bumpy/change-a.md'];
- const { hasEmptyBumpFile } = filterBranchBumpFiles(all, changed, tmpDir);
- expect(hasEmptyBumpFile).toBe(false);
+ const { emptyBumpFileIds } = filterBranchBumpFiles(all, changed, tmpDir);
+ expect(emptyBumpFileIds).toHaveLength(0);
});
});
});