diff --git a/.bumpy/recover-bump-files-past-version-commit.md b/.bumpy/recover-bump-files-past-version-commit.md new file mode 100644 index 0000000..ec191df --- /dev/null +++ b/.bumpy/recover-bump-files-past-version-commit.md @@ -0,0 +1,5 @@ +--- +'@varlock/bumpy': patch +--- + +Fixed GitHub release notes coming up empty (`No changelog entries.`) when the publish ran several commits after the version commit — e.g. a retry after the first publish was blocked and unrelated fixes landed on main. Bump-file recovery assumed the version commit was always `HEAD~1..HEAD`; it now locates the most recent commit that actually deleted bump files and recovers their content from that commit's parent, so release notes are populated regardless of how far HEAD has moved past versioning. diff --git a/packages/bumpy/src/core/bump-file.ts b/packages/bumpy/src/core/bump-file.ts index b33bcab..c1b670a 100644 --- a/packages/bumpy/src/core/bump-file.ts +++ b/packages/bumpy/src/core/bump-file.ts @@ -256,22 +256,40 @@ export async function writeBumpFile( } /** - * Recover bump files that were deleted in the HEAD commit (version commit). + * Recover bump files that were deleted by the version commit. * Used during the publish-only flow (after version PR merge) to provide * bump file context for GitHub release body generation. + * + * Usually the version commit is HEAD (publish runs right after the version PR + * merges), but when publish runs several commits later — e.g. a retry after the + * first attempt was blocked and unrelated fixes landed on main — HEAD has moved + * past it. So we locate the most recent commit that actually deleted bump files + * rather than assuming it's HEAD~1..HEAD; the latter silently recovers nothing + * once HEAD diverges, leaving releases with no changelog body. */ export function recoverDeletedBumpFiles(rootDir: string): BumpFile[] { - // Find .bumpy/*.md files deleted in the HEAD commit - const deleted = tryRunArgs(['git', 'diff', '--diff-filter=D', '--name-only', 'HEAD~1', 'HEAD', '--', '.bumpy/*.md'], { + // The most recent commit that deleted any .bumpy/*.md file — i.e. the version + // commit that consumed them. + const versionCommit = tryRunArgs(['git', 'log', '--diff-filter=D', '--format=%H', '-n', '1', '--', '.bumpy/*.md'], { cwd: rootDir, - }); + }) + ?.split('\n') + .filter(Boolean)[0] + ?.trim(); + if (!versionCommit) return []; + + // Read the deleted files' content from the commit's parent. + const parent = `${versionCommit}~1`; + const deleted = tryRunArgs( + ['git', 'diff', '--diff-filter=D', '--name-only', parent, versionCommit, '--', '.bumpy/*.md'], + { cwd: rootDir }, + ); if (!deleted) return []; const bumpFiles: BumpFile[] = []; for (const filePath of deleted.split('\n').filter(Boolean)) { if (filePath.endsWith('README.md')) continue; - // Read the file content from the parent commit - const content = tryRunArgs(['git', 'show', `HEAD~1:${filePath}`], { cwd: rootDir }); + const content = tryRunArgs(['git', 'show', `${parent}:${filePath}`], { cwd: rootDir }); if (!content) continue; const { bumpFile } = parseBumpFile(content, fileToId(filePath)); if (bumpFile) bumpFiles.push(bumpFile); diff --git a/packages/bumpy/test/core/bump-file-channels.test.ts b/packages/bumpy/test/core/bump-file-channels.test.ts index ac03688..84aaac5 100644 --- a/packages/bumpy/test/core/bump-file-channels.test.ts +++ b/packages/bumpy/test/core/bump-file-channels.test.ts @@ -114,4 +114,28 @@ describe('recoverDeletedBumpFiles with channel dirs', () => { await cleanupTempDir(dir); } }); + + test('recovers from the version commit even when HEAD has moved past it', async () => { + const dir = await createTempGitRepo(); + try { + await writeBumpFileAt(dir, '.bumpy/feature.md', 'pkg-a', 'minor'); + gitInDir(['add', '-A'], dir); + gitInDir(['commit', '-m', 'add bump file'], dir); + gitInDir(['rm', '-r', '.bumpy'], dir); + gitInDir(['commit', '-m', 'version packages'], dir); + // Unrelated commits land on main after the version commit (e.g. CI fixes + // before a publish retry), so the version commit is no longer HEAD. + await writeFile(resolve(dir, 'ci-fix.txt'), 'fix'); + gitInDir(['add', '-A'], dir); + gitInDir(['commit', '-m', 'fix ci'], dir); + await writeFile(resolve(dir, 'ci-fix-2.txt'), 'fix again'); + gitInDir(['add', '-A'], dir); + gitInDir(['commit', '-m', 'fix ci again'], dir); + + const recovered = recoverDeletedBumpFiles(dir); + expect(recovered.map((bf) => bf.id)).toEqual(['feature']); + } finally { + await cleanupTempDir(dir); + } + }); });