Skip to content

Commit f011a0e

Browse files
committed
Improve changelog formatters
- Date rendered as <sub> tag below version heading for cleaner look - Entries sorted by bump type (major → minor → patch) - Inline *(type)* tag shown when entry type differs from release type - Dependency/cascade bumps tagged appropriately - Extra newline in prependToChangelog for proper section spacing - Split formatPrefix into links/thanks for tag insertion between them
1 parent 36a0b18 commit f011a0e

4 files changed

Lines changed: 92 additions & 65 deletions

File tree

packages/bumpy/src/core/changelog-github.ts

Lines changed: 50 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { tryRunArgs } from '../utils/shell.ts';
22
import type { ChangelogContext, ChangelogFormatter } from './changelog.ts';
3+
import { getBumpTypeForPackage, sortBumpFilesByType } from './changelog.ts';
34

45
/** Authors filtered from "Thanks" attribution by default (e.g. bots) */
56
/** Authors filtered from "Thanks" attribution by default (e.g. AI/automation bots) */
@@ -51,48 +52,53 @@ export function createGithubFormatter(options: GithubChangelogOptions = {}): Cha
5152

5253
const lines: string[] = [];
5354
lines.push(`## ${release.newVersion}`);
54-
lines.push('');
55-
lines.push(`_${date}_`);
55+
lines.push(`<sub>${date}</sub>`);
5656
lines.push('');
5757

5858
const relevantBumpFiles = bumpFiles.filter((bf) => release.bumpFiles.includes(bf.id));
59-
60-
if (relevantBumpFiles.length > 0) {
61-
for (const bf of relevantBumpFiles) {
62-
if (!bf.summary) continue;
63-
64-
// Extract metadata overrides from summary (pr, commit, author lines)
65-
const { cleanSummary, overrides } = extractSummaryMeta(bf.summary);
66-
67-
// Look up git/PR info, with overrides taking precedence
68-
const gitInfo = resolveBumpFileInfo(bf.id, repoSlug, serverUrl, overrides);
69-
70-
const summaryLines = cleanSummary.split('\n');
71-
const firstLine = linkifyIssueRefs(summaryLines[0]!, serverUrl, repoSlug);
72-
73-
// Build the prefix: PR link, commit link, thanks
74-
const prefix = formatPrefix(
75-
gitInfo,
76-
serverUrl,
77-
repoSlug,
78-
includeCommitLink,
79-
thankContributors,
80-
internalAuthorsSet,
81-
);
82-
83-
lines.push(`-${prefix ? ` ${prefix} -` : ''} ${firstLine}`);
84-
85-
// Include continuation lines
86-
for (let i = 1; i < summaryLines.length; i++) {
87-
if (summaryLines[i]!.trim()) {
88-
lines.push(` ${linkifyIssueRefs(summaryLines[i]!, serverUrl, repoSlug)}`);
89-
}
59+
const sorted = sortBumpFilesByType(relevantBumpFiles, release.name);
60+
61+
for (const bf of sorted) {
62+
if (!bf.summary) continue;
63+
64+
const type = getBumpTypeForPackage(bf, release.name);
65+
const tag = type !== release.type ? ` *(${type})*` : '';
66+
67+
// Extract metadata overrides from summary (pr, commit, author lines)
68+
const { cleanSummary, overrides } = extractSummaryMeta(bf.summary);
69+
70+
// Look up git/PR info, with overrides taking precedence
71+
const gitInfo = resolveBumpFileInfo(bf.id, repoSlug, serverUrl, overrides);
72+
73+
const summaryLines = cleanSummary.split('\n');
74+
const firstLine = linkifyIssueRefs(summaryLines[0]!, serverUrl, repoSlug);
75+
76+
// Build the prefix: PR link, commit link, thanks
77+
const { links, thanks } = formatPrefix(
78+
gitInfo,
79+
serverUrl,
80+
repoSlug,
81+
includeCommitLink,
82+
thankContributors,
83+
internalAuthorsSet,
84+
);
85+
86+
// Assemble: links, tag, thanks, then summary
87+
const parts = [links, tag, thanks].filter(Boolean);
88+
const hasMeta = parts.length > 0;
89+
lines.push(`- ${parts.join(' ')}${hasMeta ? ' - ' : ''}${firstLine}`);
90+
91+
// Include continuation lines
92+
for (let i = 1; i < summaryLines.length; i++) {
93+
if (summaryLines[i]!.trim()) {
94+
lines.push(` ${linkifyIssueRefs(summaryLines[i]!, serverUrl, repoSlug)}`);
9095
}
9196
}
9297
}
9398

94-
if (release.isDependencyBump && relevantBumpFiles.length === 0) {
95-
lines.push('- Updated dependencies');
99+
if (release.isDependencyBump) {
100+
const depTag = release.type !== 'patch' ? ` *(patch)* -` : '';
101+
lines.push(`-${depTag} Updated dependencies`);
96102
}
97103

98104
if (release.isCascadeBump && !release.isDependencyBump && relevantBumpFiles.length === 0) {
@@ -263,8 +269,8 @@ function findBumpFileCommitInfo(bumpFileId: string, repo?: string): BumpFileGitI
263269
// ---- Formatting helpers ----
264270

265271
/**
266-
* Build the prefix portion of a changelog line: PR link, commit link, thanks.
267-
* Matches the format used by @changesets/changelog-github.
272+
* Build the prefix portions of a changelog line, split into links and thanks
273+
* so the bump type tag can be inserted between them.
268274
*/
269275
function formatPrefix(
270276
info: BumpFileGitInfo,
@@ -273,23 +279,24 @@ function formatPrefix(
273279
includeCommitLink: boolean,
274280
thankContributors: boolean,
275281
internalAuthors: Set<string>,
276-
): string {
277-
const parts: string[] = [];
282+
): { links: string; thanks: string } {
283+
const linkParts: string[] = [];
278284

279285
if (info.prNumber && info.prUrl) {
280-
parts.push(`[#${info.prNumber}](${info.prUrl})`);
286+
linkParts.push(`[#${info.prNumber}](${info.prUrl})`);
281287
}
282288

283289
if (includeCommitLink && info.commitHash && repo) {
284290
const short = info.commitHash.slice(0, 7);
285-
parts.push(`[\`${short}\`](${serverUrl}/${repo}/commit/${info.commitHash})`);
291+
linkParts.push(`[\`${short}\`](${serverUrl}/${repo}/commit/${info.commitHash})`);
286292
}
287293

294+
let thanks = '';
288295
if (thankContributors && info.author && !internalAuthors.has(info.author.toLowerCase())) {
289-
parts.push(`Thanks [@${info.author}](${serverUrl}/${info.author})!`);
296+
thanks = `Thanks [@${info.author}](${serverUrl}/${info.author})!`;
290297
}
291298

292-
return parts.join(' ');
299+
return { links: linkParts.join(' '), thanks };
293300
}
294301

295302
/**

packages/bumpy/src/core/changelog.ts

Lines changed: 36 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { resolve, relative } from 'node:path';
22
import { realpathSync } from 'node:fs';
33
import { log } from '../utils/logger.ts';
4-
import type { BumpFile, PlannedRelease, BumpyConfig } from '../types.ts';
4+
import type { BumpFile, BumpType, PlannedRelease, BumpyConfig } from '../types.ts';
5+
import { BUMP_LEVELS } from '../types.ts';
56

67
// ---- Formatter interface ----
78

@@ -19,35 +20,52 @@ export interface ChangelogContext {
1920
*/
2021
export type ChangelogFormatter = (ctx: ChangelogContext) => string | Promise<string>;
2122

23+
// ---- Bump type helpers ----
24+
25+
/** Get the bump type a bump file applies to a specific package */
26+
export function getBumpTypeForPackage(bf: BumpFile, packageName: string): BumpType {
27+
const rel = bf.releases.find((r) => r.name === packageName);
28+
return rel?.type === 'none' ? 'patch' : (rel?.type ?? 'patch');
29+
}
30+
31+
/** Sort bump files by bump type for a specific package (major → minor → patch) */
32+
export function sortBumpFilesByType(bumpFiles: BumpFile[], packageName: string): BumpFile[] {
33+
return [...bumpFiles].sort((a, b) => {
34+
const aLevel = BUMP_LEVELS[getBumpTypeForPackage(a, packageName)];
35+
const bLevel = BUMP_LEVELS[getBumpTypeForPackage(b, packageName)];
36+
return bLevel - aLevel;
37+
});
38+
}
39+
2240
// ---- Built-in formatters ----
2341

24-
/** Default formatter — version heading, date, bullet points */
42+
/** Default formatter — version heading with date, bullet points sorted by bump type */
2543
export const defaultFormatter: ChangelogFormatter = (ctx) => {
2644
const { release, bumpFiles, date } = ctx;
2745
const lines: string[] = [];
2846
lines.push(`## ${release.newVersion}`);
29-
lines.push('');
30-
lines.push(`_${date}_`);
47+
lines.push(`<sub>${date}</sub>`);
3148
lines.push('');
3249

3350
const relevantBumpFiles = bumpFiles.filter((bf) => release.bumpFiles.includes(bf.id));
34-
35-
if (relevantBumpFiles.length > 0) {
36-
for (const bf of relevantBumpFiles) {
37-
if (bf.summary) {
38-
const summaryLines = bf.summary.split('\n');
39-
lines.push(`- ${summaryLines[0]}`);
40-
for (let i = 1; i < summaryLines.length; i++) {
41-
if (summaryLines[i]!.trim()) {
42-
lines.push(` ${summaryLines[i]}`);
43-
}
44-
}
51+
const sorted = sortBumpFilesByType(relevantBumpFiles, release.name);
52+
53+
for (const bf of sorted) {
54+
if (!bf.summary) continue;
55+
const type = getBumpTypeForPackage(bf, release.name);
56+
const tag = type !== release.type ? `*(${type})* ` : '';
57+
const summaryLines = bf.summary.split('\n');
58+
lines.push(`- ${tag}${summaryLines[0]}`);
59+
for (let i = 1; i < summaryLines.length; i++) {
60+
if (summaryLines[i]!.trim()) {
61+
lines.push(` ${summaryLines[i]}`);
4562
}
4663
}
4764
}
4865

49-
if (release.isDependencyBump && relevantBumpFiles.length === 0) {
50-
lines.push('- Updated dependencies');
66+
if (release.isDependencyBump) {
67+
const tag = release.type !== 'patch' ? `*(patch)* ` : '';
68+
lines.push(`- ${tag}Updated dependencies`);
5169
}
5270

5371
if (release.isCascadeBump && !release.isDependencyBump && relevantBumpFiles.length === 0) {
@@ -153,7 +171,7 @@ export function prependToChangelog(existingContent: string, newEntry: string): s
153171
// Find the first ## after the # header
154172
const afterTitle = existingContent.indexOf('\n##');
155173
if (afterTitle !== -1) {
156-
return existingContent.slice(0, afterTitle + 1) + '\n' + newEntry + existingContent.slice(afterTitle + 1);
174+
return existingContent.slice(0, afterTitle + 1) + '\n' + newEntry + '\n' + existingContent.slice(afterTitle + 1);
157175
}
158176
// No existing entries, append after the title
159177
return existingContent.trimEnd() + '\n\n' + newEntry;

packages/bumpy/test/core/changelog-github.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ describe('createGithubFormatter', () => {
2626
const result = await formatter({ release, bumpFiles, date: '2026-04-14' });
2727

2828
expect(result).toContain('## 1.1.0');
29-
expect(result).toContain('_2026-04-14_');
29+
expect(result).toContain('<sub>2026-04-14</sub>');
3030
expect(result).toContain('Added feature X');
3131
});
3232

packages/bumpy/test/core/changelog.test.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,11 @@ describe('defaultFormatter', () => {
2323
const result = await defaultFormatter({ release, bumpFiles, date: '2026-04-14' });
2424

2525
expect(result).toContain('## 1.1.0');
26-
expect(result).toContain('_2026-04-14_');
26+
expect(result).toContain('<sub>2026-04-14</sub>');
2727
expect(result).toContain('- Added new feature');
28-
expect(result).toContain('- Fixed a bug');
28+
expect(result).toContain('- *(patch)* Fixed a bug');
29+
// Minor (matching release type, no tag) should come before patch
30+
expect(result.indexOf('Added new feature')).toBeLessThan(result.indexOf('Fixed a bug'));
2931
});
3032

3133
test('formats dependency bump with no bump files', async () => {
@@ -115,7 +117,7 @@ describe('generateChangelogEntry', () => {
115117

116118
const result = await generateChangelogEntry(release, [], undefined, '2020-01-01');
117119

118-
expect(result).toContain('_2020-01-01_');
120+
expect(result).toContain('<sub>2020-01-01</sub>');
119121
});
120122
});
121123

0 commit comments

Comments
 (0)