@@ -288,7 +288,6 @@ After the workflow completes, expect **installer and updater** artifacts similar
288288| Platform | Kind | Typical pattern |
289289| ------------------- | -------------- | --------------- |
290290| macOS Apple Silicon | DMG | \`*.dmg\` (arm64) |
291- | macOS Intel | DMG | \`*.dmg\` (x64) |
292291| macOS | ZIP (updater) | \`*.zip\` |
293292| Linux x64 | AppImage | \`*.AppImage\` |
294293| Windows x64 | NSIS installer | \`*.exe\` |
@@ -297,7 +296,7 @@ After the workflow completes, expect **installer and updater** artifacts similar
297296
298297| File | Purpose |
299298| ------------------ | --------------------------------------------------------- |
300- | \`latest-mac.yml\` | macOS update manifest (merged from per-arch builds in CI) |
299+ | \`latest-mac.yml\` | macOS update manifest |
301300| \`latest-linux.yml\` | Linux update manifest |
302301| \`latest.yml\` | Windows update manifest |
303302| \`*.blockmap\` | Differential download block maps |
@@ -326,6 +325,16 @@ function updateChangelog(rootDir: string, version: string, section: string): voi
326325 const changelogPath = resolve ( rootDir , "CHANGELOG.md" ) ;
327326 let content = readFileSync ( changelogPath , "utf8" ) ;
328327
328+ const escapedVersion = version . replace ( / [ . * + ? ^ $ { } ( ) | [ \] \\ ] / g, "\\$&" ) ;
329+ const sectionRe = new RegExp (
330+ `\\n## \\[${ escapedVersion } \\][^\\n]*\\n[\\s\\S]*?(?=\\n## \\[|\\n\\[|$)` ,
331+ "g" ,
332+ ) ;
333+ content = content . replace ( sectionRe , "\n" ) ;
334+
335+ const linkRe = new RegExp ( `\\n\\[${ escapedVersion } \\]: .*` , "g" ) ;
336+ content = content . replace ( linkRe , "" ) ;
337+
329338 // Insert new section after ## [Unreleased] block
330339 const unreleasedIndex = content . indexOf ( "## [Unreleased]" ) ;
331340 if ( unreleasedIndex === - 1 ) {
@@ -336,7 +345,7 @@ function updateChangelog(rootDir: string, version: string, section: string): voi
336345 const afterUnreleased = content . indexOf ( "\n## [" , unreleasedIndex + 1 ) ;
337346 const insertAt = afterUnreleased !== - 1 ? afterUnreleased : content . length ;
338347
339- content = content . slice ( 0 , insertAt ) + "\n" + section + "\n" + content . slice ( insertAt ) ;
348+ content = content . slice ( 0 , insertAt ) + section + "\n" + content . slice ( insertAt ) ;
340349
341350 // Add the version comparison link at the bottom
342351 const versionLink = `[${ version } ]: ${ REPO_URL } /releases/tag/v${ version } ` ;
@@ -356,6 +365,13 @@ function updateReleasesReadme(rootDir: string, version: string, shortDescription
356365 const readmePath = resolve ( rootDir , "docs/releases/README.md" ) ;
357366 let content = readFileSync ( readmePath , "utf8" ) ;
358367
368+ // Remove any pre-existing row for this version to keep release notes index idempotent.
369+ const existingVersionRow = new RegExp (
370+ `^\\| \\[${ version . replace ( / [ . * + ? ^ $ { } ( ) | [ \] \\ ] / g, "\\$&" ) } \\]\\(v${ version . replace ( / [ . * + ? ^ $ { } ( ) | [ \] \\ ] / g, "\\$&" ) } \\.md\\) \\| .*?\\|$\\n?` ,
371+ "gm" ,
372+ ) ;
373+ content = content . replace ( existingVersionRow , "" ) ;
374+
359375 // Find the table header separator line (| --- | --- | --- |)
360376 const separatorRe = / ^ \| [ - ] + \| [ - ] + \| [ - ] + \| $ / m;
361377 const match = content . match ( separatorRe ) ;
0 commit comments