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

Improve empty bump file handling — show file links and list alongside valid bump files
4 changes: 2 additions & 2 deletions packages/bumpy/src/commands/check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
60 changes: 53 additions & 7 deletions packages/bumpy/src/commands/ci.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
}
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -492,6 +507,7 @@ function formatReleasePlanComment(
pm: PackageManager,
warnings: string[] = [],
parseErrors: string[] = [],
emptyBumpFileIds: string[] = [],
): string {
const repo = process.env.GITHUB_REPOSITORY;
const lines: string[] = [];
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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 = [
`<a href="https://bumpy.varlock.dev"><img src="${FROG_IMG_BASE}/frog-neutral.png" alt="bumpy-frog" width="60" align="left" style="image-rendering: pixelated;" title="Hi! I'm bumpy!" /></a>`,
'',
'**This PR includes an empty bump file — no version bump is needed.** :white_check_mark:',
'<br clear="left" />',
'\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');
}

Expand Down
11 changes: 5 additions & 6 deletions packages/bumpy/src/core/bump-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,14 +240,14 @@ export function filterBranchBumpFiles(
changedFiles: string[],
rootDir?: string,
parseErrors: string[] = [],
): { branchBumpFiles: BumpFile[]; branchBumpFileIds: Set<string>; hasEmptyBumpFile: boolean } {
): { branchBumpFiles: BumpFile[]; branchBumpFileIds: Set<string>; 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);
Expand All @@ -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 };
}
18 changes: 9 additions & 9 deletions packages/bumpy/test/core/check.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)', () => {
Expand All @@ -76,25 +76,25 @@ 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 () => {
// empty-one.md does NOT exist on disk (was deleted)
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);
});
});
});