fix: scope prerelease note reuse by version

This commit is contained in:
2026-06-14 18:04:03 -07:00
parent 8d73de8731
commit aa8eb753f6
5 changed files with 107 additions and 3 deletions
@@ -2,3 +2,4 @@ type: fixed
area: release area: release
- Kept the GitHub release `What's Changed` and `New Contributors` attribution sections when CI regenerates release notes from the committed changelog. - Kept the GitHub release `What's Changed` and `New Contributors` attribution sections when CI regenerates release notes from the committed changelog.
- Scoped prerelease note reuse to the same base version so a new beta line starts from current fragments instead of stale notes from older prereleases.
+4 -2
View File
@@ -61,8 +61,10 @@
committed file — so review it before committing. If you add more committed file — so review it before committing. If you add more
`changes/*.md` fragments for a later beta/RC, rerun `changes/*.md` fragments for a later beta/RC, rerun
`bun run changelog:prerelease-notes --version <version>`; the generator uses `bun run changelog:prerelease-notes --version <version>`; the generator uses
the existing prerelease notes as the baseline and asks Claude to merge only the existing prerelease notes as the baseline only when their hidden
the new fragment material. Do not run `bun run changelog:build`. `prerelease-base-version` marker matches the current base version, and asks
Claude to merge only the new fragment material. Do not run
`bun run changelog:build`.
6. Tag the commit: `git tag v<version>`. 6. Tag the commit: `git tag v<version>`.
7. Push commit + tag. 7. Push commit + tag.
+2
View File
@@ -1,5 +1,7 @@
> This is a prerelease build for testing. Stable changelog and docs-site updates remain pending until the final stable release. > This is a prerelease build for testing. Stable changelog and docs-site updates remain pending until the final stable release.
<!-- prerelease-base-version: 0.17.0 -->
## Highlights ## Highlights
### Changed ### Changed
+60
View File
@@ -605,6 +605,7 @@ test('writePrereleaseNotesForVersion writes cumulative beta notes without mutati
const prereleaseNotes = fs.readFileSync(outputPath, 'utf8'); const prereleaseNotes = fs.readFileSync(outputPath, 'utf8');
assert.match(prereleaseNotes, /^> This is a prerelease build for testing\./m); assert.match(prereleaseNotes, /^> This is a prerelease build for testing\./m);
assert.match(prereleaseNotes, /<!-- prerelease-base-version: 0\.11\.3 -->/);
assert.match(prereleaseNotes, /## Highlights\n### Added\n- Polished: added entry\./); assert.match(prereleaseNotes, /## Highlights\n### Added\n- Polished: added entry\./);
assert.match(prereleaseNotes, /### Fixed\n- Polished: fixed entry\./); assert.match(prereleaseNotes, /### Fixed\n- Polished: fixed entry\./);
assert.match(prereleaseNotes, /## Installation\n\nSee the README and docs\/installation guide/); assert.match(prereleaseNotes, /## Installation\n\nSee the README and docs\/installation guide/);
@@ -620,6 +621,8 @@ test('writePrereleaseNotesForVersion reuses existing prerelease notes when addin
const existingNotes = [ const existingNotes = [
'> This is a prerelease build for testing. Stable changelog and docs-site updates remain pending until the final stable release.', '> This is a prerelease build for testing. Stable changelog and docs-site updates remain pending until the final stable release.',
'', '',
'<!-- prerelease-base-version: 0.11.3 -->',
'',
'## Highlights', '## Highlights',
'### Added', '### Added',
'- Overlay: Previous beta entry.', '- Overlay: Previous beta entry.',
@@ -679,6 +682,61 @@ test('writePrereleaseNotesForVersion reuses existing prerelease notes when addin
} }
}); });
test('writePrereleaseNotesForVersion ignores unmarked prerelease notes from an older release line', async () => {
const { writePrereleaseNotesForVersion } = await loadModule();
const workspace = createWorkspace('prerelease-ignore-unmarked-old-notes');
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',
'- Settings Window: Previous release line entry.',
'',
'## Installation',
'',
'See the README and docs/installation guide for full setup steps.',
'',
].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.17.0-beta.1' }, 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: overlay',
'',
'- Replaced subtitle delay actions with native mpv keybindings.',
].join('\n'),
'utf8',
);
try {
const stub = defaultStubClaude();
const outputPath = writePrereleaseNotesForVersion({
cwd: projectRoot,
version: '0.17.0-beta.1',
deps: { runClaude: stub.runClaude },
});
assert.equal(stub.calls.length, 1, 'prerelease should issue exactly one Claude call');
assert.doesNotMatch(stub.calls[0]!.input, /EXISTING PRERELEASE NOTES/);
assert.doesNotMatch(stub.calls[0]!.input, /Settings Window: Previous release line entry/);
const prereleaseNotes = fs.readFileSync(outputPath, 'utf8');
assert.match(prereleaseNotes, /### Changed\n- Polished: changed entry\./);
} finally {
fs.rmSync(workspace, { recursive: true, force: true });
}
});
test('writePrereleaseNotesForVersion prompts Claude to revise stale prerelease bullets instead of appending fix churn', async () => { test('writePrereleaseNotesForVersion prompts Claude to revise stale prerelease bullets instead of appending fix churn', async () => {
const { writePrereleaseNotesForVersion } = await loadModule(); const { writePrereleaseNotesForVersion } = await loadModule();
const workspace = createWorkspace('prerelease-net-outcome-prompt'); const workspace = createWorkspace('prerelease-net-outcome-prompt');
@@ -686,6 +744,8 @@ test('writePrereleaseNotesForVersion prompts Claude to revise stale prerelease b
const existingNotes = [ const existingNotes = [
'> This is a prerelease build for testing. Stable changelog and docs-site updates remain pending until the final stable release.', '> This is a prerelease build for testing. Stable changelog and docs-site updates remain pending until the final stable release.',
'', '',
'<!-- prerelease-base-version: 0.12.0 -->',
'',
'## Highlights', '## Highlights',
'### Added', '### Added',
'- Config Window: Previous beta entry.', '- Config Window: Previous beta entry.',
+40 -1
View File
@@ -93,6 +93,40 @@ function isSupportedPrereleaseVersion(version: string): boolean {
return /^\d+\.\d+\.\d+-(beta|rc)\.\d+$/u.test(normalizeVersion(version)); return /^\d+\.\d+\.\d+-(beta|rc)\.\d+$/u.test(normalizeVersion(version));
} }
function resolvePrereleaseBaseVersion(version: string): string {
const match = /^(\d+\.\d+\.\d+)-(?:beta|rc)\.\d+$/u.exec(normalizeVersion(version));
if (!match) {
throw new Error(
`Unsupported prerelease version (${version}). Expected x.y.z-beta.N or x.y.z-rc.N.`,
);
}
return match[1]!;
}
function renderPrereleaseBaseVersionMarker(version: string): string {
return `<!-- prerelease-base-version: ${resolvePrereleaseBaseVersion(version)} -->`;
}
function extractPrereleaseBaseVersionMarker(notes: string): string | null {
return (
/<!--\s*prerelease-base-version:\s*(\d+\.\d+\.\d+)\s*-->/u.exec(notes)?.[1] ?? null
);
}
function stripPrereleaseMetadata(notes: string): string {
return notes
.replace(/<!--\s*prerelease-base-version:\s*\d+\.\d+\.\d+\s*-->\s*/u, '')
.trim();
}
function resolveReusablePrereleaseNotes(notes: string, version: string): string | undefined {
const existingBaseVersion = extractPrereleaseBaseVersionMarker(notes);
if (existingBaseVersion !== resolvePrereleaseBaseVersion(version)) {
return undefined;
}
return stripPrereleaseMetadata(notes);
}
function verifyRequestedVersionMatchesPackageVersion( function verifyRequestedVersionMatchesPackageVersion(
options: Pick<ChangelogOptions, 'cwd' | 'version' | 'deps'>, options: Pick<ChangelogOptions, 'cwd' | 'version' | 'deps'>,
): void { ): void {
@@ -669,13 +703,16 @@ function renderReleaseNotes(
disclaimer?: string; disclaimer?: string;
contributions?: Contribution[]; contributions?: Contribution[];
contributorSections?: string[]; contributorSections?: string[];
metadata?: string[];
}, },
): string { ): string {
const prefix = options?.disclaimer ? [options.disclaimer, ''] : []; const prefix = options?.disclaimer ? [options.disclaimer, ''] : [];
const metadata = options?.metadata?.length ? [...options.metadata, ''] : [];
const contributorSections = const contributorSections =
options?.contributorSections ?? renderContributorsSections(options?.contributions ?? []); options?.contributorSections ?? renderContributorsSections(options?.contributions ?? []);
return [ return [
...prefix, ...prefix,
...metadata,
'## Highlights', '## Highlights',
changes, changes,
'', '',
@@ -705,6 +742,7 @@ function writeReleaseNotesFile(
outputPath?: string; outputPath?: string;
contributions?: Contribution[]; contributions?: Contribution[];
contributorSections?: string[]; contributorSections?: string[];
metadata?: string[];
}, },
): string { ): string {
const mkdirSync = deps?.mkdirSync ?? fs.mkdirSync; const mkdirSync = deps?.mkdirSync ?? fs.mkdirSync;
@@ -1038,7 +1076,7 @@ export function writePrereleaseNotesForVersion(options?: ChangelogOptions): stri
const prereleaseNotesPath = path.join(cwd, PRERELEASE_NOTES_PATH); const prereleaseNotesPath = path.join(cwd, PRERELEASE_NOTES_PATH);
const existingReleaseNotes = existsSync(prereleaseNotesPath) const existingReleaseNotes = existsSync(prereleaseNotesPath)
? readFileSync(prereleaseNotesPath, 'utf8') ? resolveReusablePrereleaseNotes(readFileSync(prereleaseNotesPath, 'utf8'), version)
: undefined; : undefined;
const changes = polishFragmentsWithClaude(fragments, { const changes = polishFragmentsWithClaude(fragments, {
mode: 'release-notes', mode: 'release-notes',
@@ -1052,6 +1090,7 @@ export function writePrereleaseNotesForVersion(options?: ChangelogOptions): stri
'> This is a prerelease build for testing. Stable changelog and docs-site updates remain pending until the final stable release.', '> This is a prerelease build for testing. Stable changelog and docs-site updates remain pending until the final stable release.',
outputPath: PRERELEASE_NOTES_PATH, outputPath: PRERELEASE_NOTES_PATH,
contributions, contributions,
metadata: [renderPrereleaseBaseVersionMarker(version)],
}); });
} }