chore: improve changelog generator (#7058)

pull/6886/merge
Louis Lam 2 months ago committed by GitHub
parent bdcbd4c886
commit 5c81277702
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -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=iso8601-strict ${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;
}
/**

@ -1,7 +1,7 @@
import "dotenv/config";
import * as childProcess from "child_process";
import semver from "semver";
import { generateChangelog } from "../generate-changelog.mjs";
import { getPrompt } from "../generate-changelog.mjs";
import fs from "fs";
import tar from "tar";
@ -308,15 +308,15 @@ export async function createDistTarGz() {
* @returns {Promise<void>}
*/
export async function createReleasePR(version, previousVersion, dryRun, branchName = "release", githubRunId = null) {
const changelog = await generateChangelog(previousVersion);
const prompt = await getPrompt(previousVersion);
const title = dryRun ? `chore: update to ${version} (dry run)` : `chore: update to ${version}`;
// Build the artifact link - use direct run link if available, otherwise link to workflow file
const artifactLink = githubRunId
const artifactLink = githubRunId
? `https://github.com/louislam/uptime-kuma/actions/runs/${githubRunId}/workflow`
: `https://github.com/louislam/uptime-kuma/actions/workflows/beta-release.yml`;
const body = `## Release ${version}
This PR prepares the release for version ${version}.
@ -330,10 +330,16 @@ This PR prepares the release for version ${version}.
- [ ] (Beta only) Set prerelease
- [ ] Publish the release note on GitHub.
### Changelog
### Ask LLM to categorize the changelog
\`\`\`md
${changelog}
${prompt}
\`\`\`
Run the following command to generate the changelog with the categorized map from LLM:
\`\`\`bash
npm run generate-changelog ${previousVersion} generate 'JSON_MAPPING_BY_LLM_HERE'
\`\`\`
### Release Artifacts
@ -341,7 +347,19 @@ The \`dist.tar.gz\` archive will be available as an artifact in the workflow run
`;
// Create the PR using gh CLI
const args = ["pr", "create", "--title", title, "--body", body, "--base", "master", "--head", branchName, "--draft"];
const args = [
"pr",
"create",
"--title",
title,
"--body",
body,
"--base",
"master",
"--head",
branchName,
"--draft",
];
console.log(`Creating draft PR: ${title}`);

Loading…
Cancel
Save