11// scripts/aggregate-ui-changelog.js
2- // Node 18+ (or Bun). Requires GITHUB_TOKEN.
2+ // Node 18+ (or Bun). Requires GITHUB_TOKEN with read access to the UI repos .
33
44import { execSync } from 'node:child_process' ;
55import { readdirSync , writeFileSync } from 'node:fs' ;
66
7+ // Look for bundled UI zips inside the Pano module
78const UI_DIR = 'Pano/src/main/resources/UIFiles' ;
9+
10+ // Organization / repository mapping
811const OWNER = 'PanoMC' ;
912const REPOS = {
1013 'panel-ui' : 'panel-ui' ,
@@ -18,16 +21,23 @@ if (!token) {
1821 process . exit ( 1 ) ;
1922}
2023
24+ /**
25+ * Parse zip file name: "<component>-v1.0.0-dev.34.zip" (the "v" is optional)
26+ */
2127function parseZip ( name ) {
2228 const m = name . match ( / ^ ( [ a - z 0 - 9 - ] + ) - ( (?: v ) ? \d + \. \d + \. \d + (?: - [ a - z 0 - 9 . ] + ) ? ) \. z i p $ / i) ;
2329 if ( ! m ) return null ;
2430 return { comp : m [ 1 ] , version : m [ 2 ] . startsWith ( 'v' ) ? m [ 2 ] : `v${ m [ 2 ] } ` } ;
2531}
2632
33+ /**
34+ * Read current UI zip versions from working tree
35+ */
2736function listCurrentVersions ( ) {
2837 const entries = readdirSync ( UI_DIR , { withFileTypes : true } )
2938 . filter ( ( d ) => d . isFile ( ) && d . name . endsWith ( '.zip' ) )
3039 . map ( ( d ) => d . name ) ;
40+
3141 const map = { } ;
3242 for ( const f of entries ) {
3343 const p = parseZip ( f ) ;
@@ -36,6 +46,9 @@ function listCurrentVersions() {
3646 return map ;
3747}
3848
49+ /**
50+ * Get the previous tag (semantic-release last tag)
51+ */
3952function getPrevTag ( ) {
4053 try {
4154 return execSync ( 'git describe --tags --abbrev=0 HEAD^' , { encoding : 'utf8' } ) . trim ( ) ;
@@ -48,6 +61,9 @@ function getPrevTag() {
4861 }
4962}
5063
64+ /**
65+ * Read UI zip versions from the previous tag’s tree without checkout
66+ */
5167function listPreviousVersionsFromTag ( prevTag ) {
5268 if ( ! prevTag ) return { } ;
5369 let listing = '' ;
@@ -65,6 +81,9 @@ function listPreviousVersionsFromTag(prevTag) {
6581 return map ;
6682}
6783
84+ /**
85+ * Minimal GitHub API helper
86+ */
6887async function ghJson ( path , params = { } ) {
6988 const url = `https://api.github.com${ path } ` ;
7089 const res = await fetch ( url , {
@@ -82,10 +101,18 @@ async function ghJson(path, params = {}) {
82101 return await res . json ( ) ;
83102}
84103
85- // Normalize UI repo release body into clean bullet lines
104+ /**
105+ * Normalize a UI repo release body to clean bullet lines:
106+ * - drop any heading lines (start with #) and section names (Features, Bug Fixes, etc.)
107+ * - drop version/date headings
108+ * - drop code blocks
109+ * - flatten to "- ..." bullets
110+ * - de-duplicate
111+ */
86112function normalizeReleaseBody ( body ) {
87113 const out = [ ] ;
88114 let inCode = false ;
115+
89116 for ( const raw of body . split ( '\n' ) ) {
90117 const line = raw . replace ( / \s + $ / , '' ) ; // rtrim
91118 const t = line . trim ( ) ;
@@ -100,33 +127,28 @@ function normalizeReleaseBody(body) {
100127 // skip empty
101128 if ( ! t ) continue ;
102129
103- // drop version/date headings & section headers
130+ // drop any markdown headings
131+ if ( / ^ # { 1 , 6 } \s * / . test ( t ) ) continue ;
132+
133+ // drop common section headers and separators
104134 if (
105- / ^ # { 1 , 6 } \s * ( v ? \d + \. \d + \. \d + (?: - [ ^ \s ) ] + ) ? ) ( \s * \( \d { 4 } - \d { 2 } - \d { 2 } \) ) ? / i. test ( t ) ||
106- / ^ ( v ? \d + \. \d + \. \d + (?: - [ ^ \s ) ] + ) ? ) ( \s * \( \d { 4 } - \d { 2 } - \d { 2 } \) ) ? $ / . test ( t ) ||
107- / ^ # { 1 , 6 } \s * ( f e a t u r e s | b u g f i x e s | f i x e s | p e r f o r m a n c e i m p r o v e m e n t s | r e v e r t s | c h o r e s | c h o r e | c i | b u i l d ) \s * $ / i. test ( t ) ||
108- / ^ ( f e a t u r e s | b u g f i x e s | f i x e s | p e r f o r m a n c e i m p r o v e m e n t s | r e v e r t s | c h o r e s | c h o r e | c i | b u i l d ) \s * $ / i. test ( t ) ||
109- / ^ - { 3 , } $ / . test ( t )
135+ / ^ ( f e a t u r e s | b u g f i x e s | f i x ( e s ) ? | p e r f o r m a n c e i m p r o v e m e n t s | r e v e r t s | c h o r e s ? | c i | b u i l d | c h a n g e s ) \s * : ? $ / i. test ( t ) ||
136+ / ^ [ - _ ] { 3 , } $ / . test ( t )
110137 ) continue ;
111138
112- // normalize list items / commit-like lines
113- let item = t . replace ( / ^ [ - * ] \s + / , '' ) ; // strip leading list marker
114- item = item . replace ( / ^ \d + \. \s + / , '' ) ; // strip ordered list
139+ // drop version/date headings like "1.0.0 (2025-08-28)" or "v1.2.3"
140+ if ( / ^ ( v ? \d + \. \d + \. \d + (?: - [ ^ \s ) ] + ) ? ) ( \s * \( \d { 4 } - \d { 2 } - \d { 2 } \) ) ? $ / i. test ( t ) ) continue ;
141+
142+ // normalize list items / ordered items
143+ let item = t . replace ( / ^ [ - * ] \s + / , '' ) ; // strip leading list marker
144+ item = item . replace ( / ^ \d + \. \s + / , '' ) ; // strip ordered list
115145 item = item . replace ( / ^ \s + / , '' ) ;
116146
117- // if it looks like a conventional header or a plain message, bullet it
118- if ( ! / ^ [ - * ] \s + / . test ( t ) ) {
119- 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 ( item ) ) {
120- out . push ( `- ${ item } ` ) ;
121- } else {
122- out . push ( `- ${ item } ` ) ;
123- }
124- } else {
125- out . push ( `- ${ item } ` ) ;
126- }
147+ // bulletize anything left (including conventional headers)
148+ out . push ( `- ${ item } ` ) ;
127149 }
128150
129- // dedupe (case-insensitive)
151+ // de-duplicate (case-insensitive)
130152 const seen = new Set ( ) ;
131153 const uniq = [ ] ;
132154 for ( const l of out ) {
@@ -139,11 +161,16 @@ function normalizeReleaseBody(body) {
139161 return uniq ;
140162}
141163
164+ /**
165+ * Collect notes between fromTag…toTag for a repo:
166+ * 1) Conventional commit headers from compare API
167+ * 2) Target tag’s normalized GitHub Release body (if exists)
168+ */
142169async function collectNotesForRange ( repo , fromTag , toTag ) {
143- const set = new Set ( ) ; // for dedupe
170+ const set = new Set ( ) ;
144171 const lines = [ ] ;
145172
146- // 1) Conventional commit headers via compare API
173+ // 1) Conventional commits via compare API
147174 try {
148175 const cmp = await ghJson (
149176 `/repos/${ OWNER } /${ repo } /compare/${ encodeURIComponent ( fromTag ) } ...${ encodeURIComponent ( toTag ) } `
@@ -164,7 +191,7 @@ async function collectNotesForRange(repo, fromTag, toTag) {
164191 // ignore
165192 }
166193
167- // 2) Append normalized release body (toTag), if any
194+ // 2) Append normalized release body of toTag
168195 try {
169196 const releases = await ghJson ( `/repos/${ OWNER } /${ repo } /releases?per_page=100` ) ;
170197 for ( const r of releases ) {
@@ -186,6 +213,9 @@ async function collectNotesForRange(repo, fromTag, toTag) {
186213 return lines . length ? lines . join ( '\n' ) : null ;
187214}
188215
216+ /**
217+ * Main
218+ */
189219( async ( ) => {
190220 const current = listCurrentVersions ( ) ;
191221 const prevTag = getPrevTag ( ) ;
@@ -201,9 +231,10 @@ async function collectNotesForRange(repo, fromTag, toTag) {
201231 const notes = await collectNotesForRange ( repo , oldV , nowV ) ;
202232 if ( ! notes ) continue ;
203233
234+ // Section title WITHOUT "###" — use bold line instead
204235 sections . push (
205236 [
206- `### ${ comp } : ${ oldV } → ${ nowV } ` ,
237+ `** ${ comp } : ${ oldV } → ${ nowV } ** ` ,
207238 '' ,
208239 notes
209240 ] . join ( '\n' )
@@ -216,7 +247,7 @@ async function collectNotesForRange(repo, fromTag, toTag) {
216247 return ;
217248 }
218249
219- // Visible spacing in GitHub release body
250+ // Visible spacing in GitHub release body (don’t rely on collapsed newlines)
220251 const SPACER = '\n<br/>\n<br/>\n' ;
221252 const output = `${ SPACER } ${ sections . join ( SPACER ) } \n` ;
222253
0 commit comments