chore: improve changelog generator (#7058)
This commit is contained in:
@@ -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 = `
|
||||
const outputFormat = JSON.stringify({
|
||||
improvements: [123, 456],
|
||||
newFeatures: [789],
|
||||
bugFixes: [101, 112],
|
||||
securityFixes: [131, 415],
|
||||
translationContributions: [161, 718],
|
||||
others: [192, 21],
|
||||
});
|
||||
|
||||
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.
|
||||
const prompt = `Input Data:
|
||||
\`\`\`json
|
||||
{{ input }}
|
||||
\`\`\`
|
||||
|
||||
Changelog:
|
||||
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);
|
||||
|
||||
### 🆕 New Features
|
||||
|
||||
### 💇♀️ Improvements
|
||||
|
||||
### 🐞 Bug Fixes
|
||||
|
||||
### ⬆️ Security Fixes
|
||||
|
||||
### 🦎 Translation Contributions
|
||||
|
||||
### Others
|
||||
- Other small changes, code refactoring and comment/doc updates in this repo:
|
||||
`;
|
||||
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];
|
||||
const action = process.argv[3];
|
||||
const categorizedMap = process.argv[4] ? JSON.parse(process.argv[4]) : null;
|
||||
|
||||
if (!previousVersion) {
|
||||
console.error("Please provide the previous version as the first argument.");
|
||||
process.exit(1);
|
||||
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);
|
||||
}
|
||||
|
||||
return content + "\n" + template;
|
||||
// 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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}`);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user