diff --git a/changes/release-notes-attribution-placement.md b/changes/release-notes-attribution-placement.md index 9c70edec..f49354ee 100644 --- a/changes/release-notes-attribution-placement.md +++ b/changes/release-notes-attribution-placement.md @@ -2,3 +2,4 @@ type: fixed area: release - 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. diff --git a/docs/RELEASING.md b/docs/RELEASING.md index a7e3e4de..e8bf698f 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -61,8 +61,10 @@ committed file — so review it before committing. If you add more `changes/*.md` fragments for a later beta/RC, rerun `bun run changelog:prerelease-notes --version `; the generator uses - the existing prerelease notes as the baseline and asks Claude to merge only - the new fragment material. Do not run `bun run changelog:build`. + the existing prerelease notes as the baseline only when their hidden + `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`. 7. Push commit + tag. diff --git a/release/prerelease-notes.md b/release/prerelease-notes.md index 3b021110..fb1b4db1 100644 --- a/release/prerelease-notes.md +++ b/release/prerelease-notes.md @@ -1,5 +1,7 @@ > This is a prerelease build for testing. Stable changelog and docs-site updates remain pending until the final stable release. + + ## Highlights ### Changed diff --git a/scripts/build-changelog.test.ts b/scripts/build-changelog.test.ts index 9d6e443e..779e530b 100644 --- a/scripts/build-changelog.test.ts +++ b/scripts/build-changelog.test.ts @@ -605,6 +605,7 @@ test('writePrereleaseNotesForVersion writes cumulative beta notes without mutati const prereleaseNotes = fs.readFileSync(outputPath, 'utf8'); assert.match(prereleaseNotes, /^> This is a prerelease build for testing\./m); + assert.match(prereleaseNotes, //); assert.match(prereleaseNotes, /## Highlights\n### Added\n- Polished: added entry\./); assert.match(prereleaseNotes, /### Fixed\n- Polished: fixed entry\./); 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 = [ '> This is a prerelease build for testing. Stable changelog and docs-site updates remain pending until the final stable release.', '', + '', + '', '## Highlights', '### Added', '- 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 () => { const { writePrereleaseNotesForVersion } = await loadModule(); const workspace = createWorkspace('prerelease-net-outcome-prompt'); @@ -686,6 +744,8 @@ test('writePrereleaseNotesForVersion prompts Claude to revise stale prerelease b 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.', diff --git a/scripts/build-changelog.ts b/scripts/build-changelog.ts index 65197d15..226c988c 100644 --- a/scripts/build-changelog.ts +++ b/scripts/build-changelog.ts @@ -93,6 +93,40 @@ function isSupportedPrereleaseVersion(version: string): boolean { 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 ``; +} + +function extractPrereleaseBaseVersionMarker(notes: string): string | null { + return ( + //u.exec(notes)?.[1] ?? null + ); +} + +function stripPrereleaseMetadata(notes: string): string { + return notes + .replace(/\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( options: Pick, ): void { @@ -669,13 +703,16 @@ function renderReleaseNotes( disclaimer?: string; contributions?: Contribution[]; contributorSections?: string[]; + metadata?: string[]; }, ): string { const prefix = options?.disclaimer ? [options.disclaimer, ''] : []; + const metadata = options?.metadata?.length ? [...options.metadata, ''] : []; const contributorSections = options?.contributorSections ?? renderContributorsSections(options?.contributions ?? []); return [ ...prefix, + ...metadata, '## Highlights', changes, '', @@ -705,6 +742,7 @@ function writeReleaseNotesFile( outputPath?: string; contributions?: Contribution[]; contributorSections?: string[]; + metadata?: string[]; }, ): string { 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 existingReleaseNotes = existsSync(prereleaseNotesPath) - ? readFileSync(prereleaseNotesPath, 'utf8') + ? resolveReusablePrereleaseNotes(readFileSync(prereleaseNotesPath, 'utf8'), version) : undefined; const changes = polishFragmentsWithClaude(fragments, { 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.', outputPath: PRERELEASE_NOTES_PATH, contributions, + metadata: [renderPrereleaseBaseVersionMarker(version)], }); }