fix(changelog): summarize prerelease notes as net outcome

This commit is contained in:
2026-05-24 19:10:53 -07:00
parent b1bdeabca8
commit d6ff50455a
4 changed files with 151 additions and 2 deletions
+2
View File
@@ -34,11 +34,13 @@ Rules:
How fragments turn into a release: How fragments turn into a release:
- At release time, `bun run changelog:build` (and `bun run changelog:prerelease-notes`) pipes every pending fragment through `claude -p` to merge related items, drop noise, and rewrite into a clean user-facing release body. Write fragments as raw, informative notes — don't worry about polished prose, deduping across PRs, or line-by-line phrasing. The polish step handles all of that. - At release time, `bun run changelog:build` (and `bun run changelog:prerelease-notes`) pipes every pending fragment through `claude -p` to merge related items, drop noise, and rewrite into a clean user-facing release body. Write fragments as raw, informative notes — don't worry about polished prose, deduping across PRs, or line-by-line phrasing. The polish step handles all of that.
- The polish step treats pending fragments as the final release outcome, not prerelease history. If a feature is added and then renamed or fixed before the stable cut, ship the final feature bullet instead of separate prerelease-only breaking/fix entries.
- `internal` fragments stay in `CHANGELOG.md` (inside a collapsed `<details>` block) but are dropped from the GitHub release notes entirely. - `internal` fragments stay in `CHANGELOG.md` (inside a collapsed `<details>` block) but are dropped from the GitHub release notes entirely.
- The polished `CHANGELOG.md` and `release/release-notes.md` are committed and reviewed before tagging — edit the Markdown by hand if Claude misses something. - The polished `CHANGELOG.md` and `release/release-notes.md` are committed and reviewed before tagging — edit the Markdown by hand if Claude misses something.
Prerelease notes: Prerelease notes:
- prerelease tags like `v0.11.3-beta.1` and `v0.11.3-rc.1` reuse the current pending fragments to generate `release/prerelease-notes.md` - prerelease tags like `v0.11.3-beta.1` and `v0.11.3-rc.1` reuse the current pending fragments to generate `release/prerelease-notes.md`
- existing prerelease notes are a reviewed baseline; later prerelease runs should replace stale beta/RC wording with the current outcome instead of appending fix churn
- prerelease note generation does not consume fragments and does not update `CHANGELOG.md` or `docs-site/changelog.md` - prerelease note generation does not consume fragments and does not update `CHANGELOG.md` or `docs-site/changelog.md`
- the final stable release is the point where `bun run changelog:build` consumes fragments into the stable changelog and release notes - the final stable release is the point where `bun run changelog:build` consumes fragments into the stable changelog and release notes
+4
View File
@@ -0,0 +1,4 @@
type: changed
area: release
- Release-note polishing now treats pending fragments and reviewed prerelease notes as a cumulative final outcome, so prerelease-only fixes or breakages collapse into the final user-facing change.
+136
View File
@@ -374,6 +374,74 @@ test('writeChangelogArtifacts renders breaking changes section above type sectio
} }
}); });
test('writeChangelogArtifacts prompts Claude to summarize the final stable outcome instead of prerelease churn', async () => {
const { writeChangelogArtifacts } = await loadModule();
const workspace = createWorkspace('stable-outcome-prompt');
const projectRoot = path.join(workspace, 'SubMiner');
fs.mkdirSync(projectRoot, { recursive: true });
fs.mkdirSync(path.join(projectRoot, 'changes'), { recursive: true });
fs.writeFileSync(path.join(projectRoot, 'CHANGELOG.md'), '# Changelog\n', 'utf8');
fs.writeFileSync(
path.join(projectRoot, 'changes', '001.md'),
[
'type: added',
'area: config',
'',
'- Added a dedicated Config window with launcher entry points.',
].join('\n'),
'utf8',
);
fs.writeFileSync(
path.join(projectRoot, 'changes', '002.md'),
[
'type: changed',
'area: config',
'breaking: true',
'',
'- Renamed the Config window to Settings window and changed the launcher entry point to `subminer settings`.',
].join('\n'),
'utf8',
);
fs.writeFileSync(
path.join(projectRoot, 'changes', '003.md'),
[
'type: fixed',
'area: config',
'',
'- Fixed Settings window search and live subtitle CSS saves.',
].join('\n'),
'utf8',
);
try {
const stub = defaultStubClaude();
writeChangelogArtifacts({
cwd: projectRoot,
version: '0.12.0',
date: '2026-05-24',
deps: { runClaude: stub.runClaude },
});
const prompts = stub.calls.map((call) => call.input);
assert.equal(prompts.length, 2, 'expected changelog and release-notes prompts');
for (const prompt of prompts) {
assert.match(prompt, /Treat the fragment list as one cumulative release outcome/);
assert.match(
prompt,
/only if the final release requires action from users upgrading from the previous stable release/,
);
assert.match(prompt, /Config window.*Settings window/s);
assert.match(
prompt,
/Multiple fixes within the same prerelease cycle should collapse into one current-state bullet/,
);
}
} finally {
fs.rmSync(workspace, { recursive: true, force: true });
}
});
test('verifyChangelogFragments rejects invalid metadata', async () => { test('verifyChangelogFragments rejects invalid metadata', async () => {
const { verifyChangelogFragments } = await loadModule(); const { verifyChangelogFragments } = await loadModule();
const workspace = createWorkspace('lint-invalid'); const workspace = createWorkspace('lint-invalid');
@@ -575,6 +643,74 @@ test('writePrereleaseNotesForVersion reuses existing prerelease notes when addin
} }
}); });
test('writePrereleaseNotesForVersion prompts Claude to revise stale prerelease bullets instead of appending fix churn', async () => {
const { writePrereleaseNotesForVersion } = await loadModule();
const workspace = createWorkspace('prerelease-net-outcome-prompt');
const projectRoot = path.join(workspace, 'SubMiner');
const existingNotes = [
'> This is a prerelease build for testing. Stable changelog and docs-site updates remain pending until the final stable release.',
'',
'## Highlights',
'### Added',
'- Config Window: Previous beta entry.',
'',
'## Installation',
'',
'See the README and docs/installation guide for full setup steps.',
'',
'## Assets',
'',
'- Linux: `SubMiner.AppImage`',
'',
].join('\n');
fs.mkdirSync(path.join(projectRoot, 'changes'), { recursive: true });
fs.mkdirSync(path.join(projectRoot, 'release'), { recursive: true });
fs.writeFileSync(
path.join(projectRoot, 'package.json'),
JSON.stringify({ name: 'subminer', version: '0.12.0-beta.2' }, null, 2),
'utf8',
);
fs.writeFileSync(path.join(projectRoot, 'release', 'prerelease-notes.md'), existingNotes, 'utf8');
fs.writeFileSync(
path.join(projectRoot, 'changes', '001.md'),
[
'type: changed',
'area: config',
'breaking: true',
'',
'- Renamed the Config window to Settings window.',
].join('\n'),
'utf8',
);
fs.writeFileSync(
path.join(projectRoot, 'changes', '002.md'),
['type: fixed', 'area: config', '', '- Fixed Settings window search.'].join('\n'),
'utf8',
);
try {
const stub = recordingRunClaude(() => '### Added\n- Settings Window: Current beta state.');
writePrereleaseNotesForVersion({
cwd: projectRoot,
version: '0.12.0-beta.2',
deps: { runClaude: stub.runClaude },
});
assert.equal(stub.calls.length, 1, 'prerelease should issue exactly one Claude call');
const prompt = stub.calls[0]!.input;
assert.match(prompt, /EXISTING PRERELEASE NOTES/);
assert.match(prompt, /Existing prerelease notes are a baseline, not an immutable changelog/);
assert.match(prompt, /replace stale beta or RC wording/);
assert.match(
prompt,
/Multiple fixes within the same prerelease cycle should collapse into one current-state bullet/,
);
} finally {
fs.rmSync(workspace, { recursive: true, force: true });
}
});
test('writePrereleaseNotesForVersion supports rc prereleases', async () => { test('writePrereleaseNotesForVersion supports rc prereleases', async () => {
const { writePrereleaseNotesForVersion } = await loadModule(); const { writePrereleaseNotesForVersion } = await loadModule();
const workspace = createWorkspace('prerelease-rc-notes'); const workspace = createWorkspace('prerelease-rc-notes');
+9 -2
View File
@@ -235,6 +235,13 @@ const POLISH_PROMPT_INSTRUCTIONS = `You are formatting a software release change
You will receive a list of FRAGMENT entries below. Each fragment has metadata (type, area, breaking) and one or more bullet points written by the engineer who shipped that change. Your job is to merge, dedupe, and rewrite these fragments into a polished, user-facing release body. You will receive a list of FRAGMENT entries below. Each fragment has metadata (type, area, breaking) and one or more bullet points written by the engineer who shipped that change. Your job is to merge, dedupe, and rewrite these fragments into a polished, user-facing release body.
## Release Outcome Rules
- Treat the fragment list as one cumulative release outcome, not a chronological log of beta/RC churn.
- Put a fragment in ### Breaking Changes only if the final release requires action from users upgrading from the previous stable release. A breaking: true marker is a warning to preserve and evaluate the substance, not an automatic section choice.
- If a breaking or fixed fragment only changes behavior introduced by another pending fragment in the same release cycle, merge it into the final Added or Changed bullet. Example: if fragments first add a Config window and later rename or fix it as a Settings window, output one Settings Window bullet under Added, not separate Config window, Breaking Changes, or Fixed bullets.
- Multiple fixes within the same prerelease cycle should collapse into one current-state bullet that describes the final behavior.
## Output Rules ## Output Rules
1. Output Markdown ONLY. No preamble, no commentary, no closing remarks. Start directly with the first section heading. 1. Output Markdown ONLY. No preamble, no commentary, no closing remarks. Start directly with the first section heading.
@@ -258,7 +265,7 @@ You will receive a list of FRAGMENT entries below. Each fragment has metadata (t
- Be written in user-facing language. Drop implementation jargon, internal class names, file paths, and PR numbers. - Be written in user-facing language. Drop implementation jargon, internal class names, file paths, and PR numbers.
- Be merged with related bullets when possible. If five fragments all touch Windows overlay z-order/focus/restore, write one or two bullets that summarize the overall improvement instead of five. - Be merged with related bullets when possible. If five fragments all touch Windows overlay z-order/focus/restore, write one or two bullets that summarize the overall improvement instead of five.
- Drop bullets that only describe PR housekeeping, CodeRabbit follow-ups, or test-only changes that don't affect users. - Drop bullets that only describe PR housekeeping, CodeRabbit follow-ups, or test-only changes that don't affect users.
- Preserve the substance of every breaking change in ### Breaking Changes. Do not soften or omit them. - Preserve the substance of breaking changes that remain breaking after applying the Release Outcome Rules. Do not soften or omit them.
5. Do not invent features. Every bullet must be grounded in the input fragments. 5. Do not invent features. Every bullet must be grounded in the input fragments.
6. Do not include the version heading (## v...) — that wrapper is added by the caller. 6. Do not include the version heading (## v...) — that wrapper is added by the caller.
@@ -371,7 +378,7 @@ function polishFragmentsWithClaude(
? [ ? [
'## Existing Prerelease Notes', '## Existing Prerelease Notes',
'', '',
'The input includes EXISTING PRERELEASE NOTES before the fragment list. Reuse those highlight bullets as the baseline, preserve their meaning and wording where possible, then merge in only new or changed fragment material. Deduplicate instead of restating existing bullets. Output only the final highlights body using the section headings above; do not include the prerelease disclaimer, Installation, or Assets sections.', 'The input includes EXISTING PRERELEASE NOTES before the fragment list. Existing prerelease notes are a baseline, not an immutable changelog. Reuse reviewed highlight bullets when they still describe the current outcome, but replace stale beta or RC wording when new fragments supersede it. Merge in only new or changed fragment material, and deduplicate instead of restating existing bullets. Output only the final highlights body using the section headings above; do not include the prerelease disclaimer, Installation, or Assets sections.',
'', '',
].join('\n') ].join('\n')
: ''; : '';