@ -4,29 +4,76 @@
import * as childProcess from "child_process" ;
const ignoreList = [ "louislam" , "CommanderStorm" , "UptimeKumaBot" , "weblate" , "Copilot" , "autofix-ci[bot]" , "app/copilot-swe-agent" , "app/github-actions" , "github-actions[bot]" ] ;
const ignoreList = [
"louislam" ,
"CommanderStorm" ,
"UptimeKumaBot" ,
"weblate" ,
"Copilot" ,
"autofix-ci[bot]" ,
"app/copilot-swe-agent" ,
"app/github-actions" ,
"github-actions[bot]" ,
] ;
const mergeList = [ "chore: Translations Update from Weblate" , "chore: Update dependencies" ] ;
const template = `
LLM Task : Please help to put above PRs into the following sections based on their content . If a PR fits multiple sections , choose the most relevant one . If a PR doesn 't fit any section, place it in "Others". If there are grammatical errors in the PR titles, please correct them. Don' t change the PR numbers and authors , and keep the format . Output as markdown file format .
Changelog :
# # # 🆕 New Features
# # # 💇 ♀ ️ Improvements
# # # 🐞 Bug Fixes
# # # ⬆ ️ Security Fixes
# # # 🦎 Translation Contributions
# # # Others
- Other small changes , code refactoring and comment / doc updates in this repo :
` ;
const outputFormat = JSON . stringify ( {
improvements : [ 123 , 456 ] ,
newFeatures : [ 789 ] ,
bugFixes : [ 101 , 112 ] ,
securityFixes : [ 131 , 415 ] ,
translationContributions : [ 161 , 718 ] ,
others : [ 192 , 21 ] ,
} ) ;
const prompt = ` Input Data:
\ ` \` \` json
{ { input } }
\ ` \` \`
LLM Task :
- Output a one - line JSON object in the following format :
{ { outputFormat } }
- Empty arrays included if there are no items for that category .
- Exclude reverted pull requests .
- "fix: " type pull requests should be categorized as "bugFixes" .
- "chore: " type pull requests should be categorized as "others"
- "feat: " type pull requests should be categorized as "newFeatures" or "improvements" based on the content of the title , you should determine it .
- "refactor: " type pull requests should be categorized as "improvements" .
` .replace("{{ outputFormat }}", outputFormat);
const categoryList = {
// In case the LLM cannot categorize some items
uncategorized : {
title : "Uncategorized" ,
items : [ ] ,
} ,
newFeatures : {
title : "🆕 New Features" ,
items : [ ] ,
} ,
improvements : {
title : "💇♀️ Improvements" ,
items : [ ] ,
} ,
bugFixes : {
title : "🐞 Bug Fixes" ,
items : [ ] ,
} ,
securityFixes : {
title : "⬆️ Security Fixes" ,
items : [ ] ,
} ,
translationContributions : {
title : "🦎 Translation Contributions" ,
items : [ ] ,
} ,
others : {
title : "Others" ,
items : [ ] ,
} ,
} ;
if ( import . meta . main ) {
await main ( ) ;
@ -38,25 +85,40 @@ if (import.meta.main) {
* /
async function main ( ) {
const previousVersion = process . argv [ 2 ] ;
if ( ! previousVersion ) {
console . error ( "Please provide the previous version as the first argument." ) ;
process . exit ( 1 ) ;
const action = process . argv [ 3 ] ;
const categorizedMap = process . argv [ 4 ] ? JSON . parse ( process . argv [ 4 ] ) : null ;
if ( action === "generate" ) {
console . log ( ` Generating changelog since version ${ previousVersion } ... ` ) ;
console . log ( await generateChangelog ( previousVersion , categorizedMap ) ) ;
} else {
if ( ! previousVersion ) {
console . error ( "Please provide the previous version as the first argument." ) ;
process . exit ( 1 ) ;
}
console . log ( await getPrompt ( previousVersion ) ) ;
}
}
console . log ( ` Generating changelog since version ${ previousVersion } ... ` ) ;
console . log ( await generateChangelog ( previousVersion ) ) ;
/ * *
* Get Prompt for LLM
* @ param { string } previousVersion Previous Version Tag
* @ returns { Promise < string > } Prompt for LLM
* /
export async function getPrompt ( previousVersion ) {
const input = JSON . stringify ( await getPullRequestList ( previousVersion , true ) ) ;
return prompt . replace ( "{{ input }}" , input ) ;
}
/ * *
* Generate Changelog
* @ param { string } previousVersion Previous Version Tag
* @ param { object } categorizedMap It should be generated by the LLM based on the prompt
* @ returns { Promise < string > } Changelog Content
* /
export async function generateChangelog ( previousVersion ) {
export async function generateChangelog ( previousVersion , categorizedMap ) {
const prList = await getPullRequestList ( previousVersion ) ;
const list = [ ] ;
let content = "" ;
let i = 1 ;
for ( const pr of prList ) {
@ -98,20 +160,45 @@ export async function generateChangelog(previousVersion) {
authorPart = ` (Thanks ${ authorPart } ) ` ;
}
content += ` - ${ prPart } ${ item . title } ${ authorPart } \n ` ;
const line = ` - ${ prPart } ${ item . title } ${ authorPart } ` ;
// Determine the category of the item, based on the title and the categorizedMap
let category = "uncategorized" ;
let prNumber = item . numbers [ 0 ] ;
for ( const cat in categorizedMap ) {
if ( categorizedMap [ cat ] . includes ( prNumber ) ) {
category = cat ;
break ;
}
}
categoryList [ category ] . items . push ( line ) ;
}
// Generate markdown
let content = "" ;
for ( const cat in categoryList ) {
content += ` ### ${ categoryList [ cat ] . title } \n ` ;
for ( const item of categoryList [ cat ] . items ) {
content += ` ${ item } \n ` ;
}
content += ` \n ` ;
}
return content + "\n" + template ;
return content ;
}
/ * *
* @ param { string } previousVersion Previous Version Tag
* @ param { boolean } removeAuthor Whether to strip the author field from the returned PR list
* @ returns { Promise < object > } List of Pull Requests merged since previousVersion
* /
async function getPullRequestList ( previousVersion ) {
// Get the date of previousVersion in YYYY-MM-DD format from git
async function getPullRequestList ( previousVersion , removeAuthor = false ) {
// Get the date of previousVersion in iso8601-strict format (2026-02-19T13:34:03+08:00) from git
const previousVersionDate = childProcess
. execSync ( ` git log -1 --format=%cd --date=short ${ previousVersion } ` )
. execSync ( ` git log -1 --format=%cd --date= i so8601-st ric t ${ previousVersion } ` )
. toString ( )
. trim ( ) ;
@ -150,7 +237,15 @@ async function getPullRequestList(previousVersion) {
throw new Error ( ` gh command failed with status ${ ghProcess . status } : ${ ghProcess . stderr } ` ) ;
}
return JSON . parse ( ghProcess . stdout ) ;
const obj = JSON . parse ( ghProcess . stdout ) ;
if ( removeAuthor ) {
for ( const pr of obj ) {
delete pr . author ;
}
}
return obj ;
}
/ * *