@@ -114,15 +114,30 @@ function extractConventionalSubject(line) {
114114 return m ? m [ 3 ] . trim ( ) : line . trim ( ) ;
115115}
116116
117+ /** Remove version heading lines like "1.0.0-dev.183 (2025-08-28)" (with or without leading ### or links). */
118+ function stripVersionHeadings ( body ) {
119+ const lines = body . split ( '\n' ) ;
120+ const cleaned = [ ] ;
121+ for ( let raw of lines ) {
122+ let line = raw . replace ( / \[ ( [ ^ \] ] + ) \] \( [ ^ ) ] + \) / g, '$1' ) . trim ( ) ; // unwrap links
123+ line = line . replace ( / ^ # { 1 , 6 } \s * / , '' ) ; // drop leading markdown hashes
124+ const isVersionHeading = / ^ v ? \d + \. \d + \. \d + (?: - [ 0 - 9 A - Z a - z . - ] + ) ? (?: \s * \( \d { 4 } - \d { 2 } - \d { 2 } \) ) ? $ / . test ( line ) ;
125+ if ( isVersionHeading ) continue ;
126+ cleaned . push ( raw ) ; // keep original spacing
127+ }
128+ // trim leading/trailing blank lines
129+ return cleaned . join ( '\n' ) . replace ( / ^ \s + | \s + $ / g, '' ) . trim ( ) ;
130+ }
131+
117132/**
118133 * Collect notes between fromTag…toTag for a given repo:
119134 * - Gather conventional commit headers via compare API
120- * - Pull target tag’s release body (if present)
121- * - Dedupe: if a release bullet matches a commit subject, drop the commit line
135+ * - Pull target tag’s release body (if present) and strip version headings
136+ * - Dedupe: if body already mentions a commit subject, drop that commit line
122137 */
123138async function collectNotesForRange ( repo , fromTag , toTag ) {
124139 const commitLines = [ ] ;
125- const commitSubjects = new Set ( ) ;
140+ const commitSubjects = [ ] ;
126141
127142 // 1) Commit headers from compare API
128143 try {
@@ -134,8 +149,7 @@ async function collectNotesForRange(repo, fromTag, toTag) {
134149 const first = msg . split ( '\n' ) [ 0 ] . trim ( ) ;
135150 if ( / ^ ( f e a t | f i x | p e r f | r e f a c t o r | d o c s | c h o r e | b u i l d | c i ) ( \( .+ \) ) ? : / i. test ( first ) ) {
136151 commitLines . push ( `- ${ first } ` ) ;
137- const subj = extractConventionalSubject ( first ) ;
138- commitSubjects . add ( normalize ( subj ) ) ;
152+ commitSubjects . push ( normalize ( extractConventionalSubject ( first ) ) ) ;
139153 }
140154 }
141155 } catch {
@@ -147,41 +161,29 @@ async function collectNotesForRange(repo, fromTag, toTag) {
147161 try {
148162 const releases = await ghJson ( `/repos/${ OWNER } /${ repo } /releases?per_page=100` ) ;
149163 const r = releases . find ( ( x ) => x . tag_name === toTag && x . body && x . body . trim ( ) ) ;
150- if ( r ) releaseBody = r . body . trim ( ) ;
164+ if ( r ) releaseBody = stripVersionHeadings ( r . body . trim ( ) ) ;
151165 } catch {
152166 // ignore
153167 }
154168
155- // 3) If we have a release body, dedupe overlapping bullets from commitLines
169+ // 3) Dedupe commit lines if body already contains the same subjects (even without bullets)
156170 if ( releaseBody ) {
157- const releaseSubjects = new Set ( ) ;
158- for ( const rawLine of releaseBody . split ( '\n' ) ) {
159- const line = rawLine . trim ( ) ;
160- // bullets like "- text" or "* text"
161- const m = line . match ( / ^ [ - * ] \s + ( .+ ) $ / ) ;
162- if ( m ) {
163- const cleaned = normalize ( m [ 1 ] ) ;
164- if ( cleaned ) releaseSubjects . add ( cleaned ) ;
165- }
166- }
167-
168- // Filter out commit lines whose subject appears in the release body bullets
169- const filteredCommitLines = commitLines . filter ( ( line ) => {
170- const subj = extractConventionalSubject ( line . replace ( / ^ - + \s * / , '' ) ) ;
171- return ! releaseSubjects . has ( normalize ( subj ) ) ;
171+ const bodyNorm = normalize ( releaseBody ) ;
172+ const filtered = commitLines . filter ( ( line ) => {
173+ const subj = normalize ( extractConventionalSubject ( line . replace ( / ^ - + \s * / , '' ) ) ) ;
174+ return ! bodyNorm . includes ( subj ) ;
172175 } ) ;
173176
174- // Merge with spacing: commits (if any), blank line, then release body
175177 const merged = [ ] ;
176- if ( filteredCommitLines . length ) merged . push ( ...filteredCommitLines ) ;
178+ if ( filtered . length ) merged . push ( ...filtered ) ;
177179 if ( releaseBody ) {
178- if ( merged . length ) merged . push ( '' ) ; // blank line before release body
180+ if ( merged . length ) merged . push ( '' ) ; // blank line before body
179181 merged . push ( releaseBody ) ;
180182 }
181183 return merged . length ? merged . join ( '\n' ) : null ;
182184 }
183185
184- // If no release body, just return commit lines (or null)
186+ // If no body, return commits
185187 return commitLines . length ? commitLines . join ( '\n' ) : null ;
186188}
187189
@@ -218,7 +220,7 @@ async function collectNotesForRange(repo, fromTag, toTag) {
218220 return ;
219221 }
220222
221- // Ensure spacing: two blank lines before and between sections, so it never sticks to main changelog
223+ // Two blank lines before and between sections so it never sticks to the main changelog
222224 const output = `\n\n${ sections . join ( '\n\n' ) } \n` ;
223225 writeFileSync ( 'UI_CHANGELOG.md' , output ) ;
224226 console . log ( 'UI_CHANGELOG.md written.' ) ;
0 commit comments