diff --git a/.bumpy/clean-rich-deer.md b/.bumpy/clean-rich-deer.md new file mode 100644 index 0000000..547d30a --- /dev/null +++ b/.bumpy/clean-rich-deer.md @@ -0,0 +1,3 @@ +--- +'@varlock/bumpy': patch +--- diff --git a/packages/bumpy/src/core/changelog-github.ts b/packages/bumpy/src/core/changelog-github.ts index 637b3ef..3341de4 100644 --- a/packages/bumpy/src/core/changelog-github.ts +++ b/packages/bumpy/src/core/changelog-github.ts @@ -1,5 +1,6 @@ import { tryRunArgs } from '../utils/shell.ts'; import type { ChangelogContext, ChangelogFormatter } from './changelog.ts'; +import { getBumpTypeForPackage, sortBumpFilesByType } from './changelog.ts'; /** Authors filtered from "Thanks" attribution by default (e.g. bots) */ /** Authors filtered from "Thanks" attribution by default (e.g. AI/automation bots) */ @@ -51,48 +52,53 @@ export function createGithubFormatter(options: GithubChangelogOptions = {}): Cha const lines: string[] = []; lines.push(`## ${release.newVersion}`); - lines.push(''); - lines.push(`_${date}_`); + lines.push(`${date}`); lines.push(''); const relevantBumpFiles = bumpFiles.filter((bf) => release.bumpFiles.includes(bf.id)); - - if (relevantBumpFiles.length > 0) { - for (const bf of relevantBumpFiles) { - if (!bf.summary) continue; - - // Extract metadata overrides from summary (pr, commit, author lines) - const { cleanSummary, overrides } = extractSummaryMeta(bf.summary); - - // Look up git/PR info, with overrides taking precedence - const gitInfo = resolveBumpFileInfo(bf.id, repoSlug, serverUrl, overrides); - - const summaryLines = cleanSummary.split('\n'); - const firstLine = linkifyIssueRefs(summaryLines[0]!, serverUrl, repoSlug); - - // Build the prefix: PR link, commit link, thanks - const prefix = formatPrefix( - gitInfo, - serverUrl, - repoSlug, - includeCommitLink, - thankContributors, - internalAuthorsSet, - ); - - lines.push(`-${prefix ? ` ${prefix} -` : ''} ${firstLine}`); - - // Include continuation lines - for (let i = 1; i < summaryLines.length; i++) { - if (summaryLines[i]!.trim()) { - lines.push(` ${linkifyIssueRefs(summaryLines[i]!, serverUrl, repoSlug)}`); - } + const sorted = sortBumpFilesByType(relevantBumpFiles, release.name); + + for (const bf of sorted) { + if (!bf.summary) continue; + + const type = getBumpTypeForPackage(bf, release.name); + const tag = type !== release.type ? ` *(${type})*` : ''; + + // Extract metadata overrides from summary (pr, commit, author lines) + const { cleanSummary, overrides } = extractSummaryMeta(bf.summary); + + // Look up git/PR info, with overrides taking precedence + const gitInfo = resolveBumpFileInfo(bf.id, repoSlug, serverUrl, overrides); + + const summaryLines = cleanSummary.split('\n'); + const firstLine = linkifyIssueRefs(summaryLines[0]!, serverUrl, repoSlug); + + // Build the prefix: PR link, commit link, thanks + const { links, thanks } = formatPrefix( + gitInfo, + serverUrl, + repoSlug, + includeCommitLink, + thankContributors, + internalAuthorsSet, + ); + + // Assemble: links, tag, thanks, then summary + const parts = [links, tag, thanks].filter(Boolean); + const hasMeta = parts.length > 0; + lines.push(`- ${parts.join(' ')}${hasMeta ? ' - ' : ''}${firstLine}`); + + // Include continuation lines + for (let i = 1; i < summaryLines.length; i++) { + if (summaryLines[i]!.trim()) { + lines.push(` ${linkifyIssueRefs(summaryLines[i]!, serverUrl, repoSlug)}`); } } } - if (release.isDependencyBump && relevantBumpFiles.length === 0) { - lines.push('- Updated dependencies'); + if (release.isDependencyBump) { + const depTag = release.type !== 'patch' ? ` *(patch)* -` : ''; + lines.push(`-${depTag} Updated dependencies`); } if (release.isCascadeBump && !release.isDependencyBump && relevantBumpFiles.length === 0) { @@ -263,8 +269,8 @@ function findBumpFileCommitInfo(bumpFileId: string, repo?: string): BumpFileGitI // ---- Formatting helpers ---- /** - * Build the prefix portion of a changelog line: PR link, commit link, thanks. - * Matches the format used by @changesets/changelog-github. + * Build the prefix portions of a changelog line, split into links and thanks + * so the bump type tag can be inserted between them. */ function formatPrefix( info: BumpFileGitInfo, @@ -273,23 +279,24 @@ function formatPrefix( includeCommitLink: boolean, thankContributors: boolean, internalAuthors: Set, -): string { - const parts: string[] = []; +): { links: string; thanks: string } { + const linkParts: string[] = []; if (info.prNumber && info.prUrl) { - parts.push(`[#${info.prNumber}](${info.prUrl})`); + linkParts.push(`[#${info.prNumber}](${info.prUrl})`); } if (includeCommitLink && info.commitHash && repo) { const short = info.commitHash.slice(0, 7); - parts.push(`[\`${short}\`](${serverUrl}/${repo}/commit/${info.commitHash})`); + linkParts.push(`[\`${short}\`](${serverUrl}/${repo}/commit/${info.commitHash})`); } + let thanks = ''; if (thankContributors && info.author && !internalAuthors.has(info.author.toLowerCase())) { - parts.push(`Thanks [@${info.author}](${serverUrl}/${info.author})!`); + thanks = `Thanks [@${info.author}](${serverUrl}/${info.author})!`; } - return parts.join(' '); + return { links: linkParts.join(' '), thanks }; } /** diff --git a/packages/bumpy/src/core/changelog.ts b/packages/bumpy/src/core/changelog.ts index 706a190..9b5afb7 100644 --- a/packages/bumpy/src/core/changelog.ts +++ b/packages/bumpy/src/core/changelog.ts @@ -1,7 +1,8 @@ import { resolve, relative } from 'node:path'; import { realpathSync } from 'node:fs'; import { log } from '../utils/logger.ts'; -import type { BumpFile, PlannedRelease, BumpyConfig } from '../types.ts'; +import type { BumpFile, BumpType, PlannedRelease, BumpyConfig } from '../types.ts'; +import { BUMP_LEVELS } from '../types.ts'; // ---- Formatter interface ---- @@ -19,35 +20,52 @@ export interface ChangelogContext { */ export type ChangelogFormatter = (ctx: ChangelogContext) => string | Promise; +// ---- Bump type helpers ---- + +/** Get the bump type a bump file applies to a specific package */ +export function getBumpTypeForPackage(bf: BumpFile, packageName: string): BumpType { + const rel = bf.releases.find((r) => r.name === packageName); + return rel?.type === 'none' ? 'patch' : (rel?.type ?? 'patch'); +} + +/** Sort bump files by bump type for a specific package (major → minor → patch) */ +export function sortBumpFilesByType(bumpFiles: BumpFile[], packageName: string): BumpFile[] { + return [...bumpFiles].sort((a, b) => { + const aLevel = BUMP_LEVELS[getBumpTypeForPackage(a, packageName)]; + const bLevel = BUMP_LEVELS[getBumpTypeForPackage(b, packageName)]; + return bLevel - aLevel; + }); +} + // ---- Built-in formatters ---- -/** Default formatter — version heading, date, bullet points */ +/** Default formatter — version heading with date, bullet points sorted by bump type */ export const defaultFormatter: ChangelogFormatter = (ctx) => { const { release, bumpFiles, date } = ctx; const lines: string[] = []; lines.push(`## ${release.newVersion}`); - lines.push(''); - lines.push(`_${date}_`); + lines.push(`${date}`); lines.push(''); const relevantBumpFiles = bumpFiles.filter((bf) => release.bumpFiles.includes(bf.id)); - - if (relevantBumpFiles.length > 0) { - for (const bf of relevantBumpFiles) { - if (bf.summary) { - const summaryLines = bf.summary.split('\n'); - lines.push(`- ${summaryLines[0]}`); - for (let i = 1; i < summaryLines.length; i++) { - if (summaryLines[i]!.trim()) { - lines.push(` ${summaryLines[i]}`); - } - } + const sorted = sortBumpFilesByType(relevantBumpFiles, release.name); + + for (const bf of sorted) { + if (!bf.summary) continue; + const type = getBumpTypeForPackage(bf, release.name); + const tag = type !== release.type ? `*(${type})* ` : ''; + const summaryLines = bf.summary.split('\n'); + lines.push(`- ${tag}${summaryLines[0]}`); + for (let i = 1; i < summaryLines.length; i++) { + if (summaryLines[i]!.trim()) { + lines.push(` ${summaryLines[i]}`); } } } - if (release.isDependencyBump && relevantBumpFiles.length === 0) { - lines.push('- Updated dependencies'); + if (release.isDependencyBump) { + const tag = release.type !== 'patch' ? `*(patch)* ` : ''; + lines.push(`- ${tag}Updated dependencies`); } if (release.isCascadeBump && !release.isDependencyBump && relevantBumpFiles.length === 0) { @@ -153,7 +171,7 @@ export function prependToChangelog(existingContent: string, newEntry: string): s // Find the first ## after the # header const afterTitle = existingContent.indexOf('\n##'); if (afterTitle !== -1) { - return existingContent.slice(0, afterTitle + 1) + '\n' + newEntry + existingContent.slice(afterTitle + 1); + return existingContent.slice(0, afterTitle + 1) + '\n' + newEntry + '\n' + existingContent.slice(afterTitle + 1); } // No existing entries, append after the title return existingContent.trimEnd() + '\n\n' + newEntry; diff --git a/packages/bumpy/test/core/changelog-github.test.ts b/packages/bumpy/test/core/changelog-github.test.ts index a29b8a7..21e8642 100644 --- a/packages/bumpy/test/core/changelog-github.test.ts +++ b/packages/bumpy/test/core/changelog-github.test.ts @@ -26,7 +26,7 @@ describe('createGithubFormatter', () => { const result = await formatter({ release, bumpFiles, date: '2026-04-14' }); expect(result).toContain('## 1.1.0'); - expect(result).toContain('_2026-04-14_'); + expect(result).toContain('2026-04-14'); expect(result).toContain('Added feature X'); }); diff --git a/packages/bumpy/test/core/changelog.test.ts b/packages/bumpy/test/core/changelog.test.ts index 25123ad..7bfc51a 100644 --- a/packages/bumpy/test/core/changelog.test.ts +++ b/packages/bumpy/test/core/changelog.test.ts @@ -23,9 +23,11 @@ describe('defaultFormatter', () => { const result = await defaultFormatter({ release, bumpFiles, date: '2026-04-14' }); expect(result).toContain('## 1.1.0'); - expect(result).toContain('_2026-04-14_'); + expect(result).toContain('2026-04-14'); expect(result).toContain('- Added new feature'); - expect(result).toContain('- Fixed a bug'); + expect(result).toContain('- *(patch)* Fixed a bug'); + // Minor (matching release type, no tag) should come before patch + expect(result.indexOf('Added new feature')).toBeLessThan(result.indexOf('Fixed a bug')); }); test('formats dependency bump with no bump files', async () => { @@ -115,7 +117,7 @@ describe('generateChangelogEntry', () => { const result = await generateChangelogEntry(release, [], undefined, '2020-01-01'); - expect(result).toContain('_2020-01-01_'); + expect(result).toContain('2020-01-01'); }); });