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 = [ `bumpy-frog`, '', '**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); }); }); });