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');
});
});