chore: Fix full auto release process (#7663)

This commit is contained in:
Louis Lam
2026-08-02 00:36:57 +08:00
committed by GitHub
parent d9a60dfc73
commit f766c38fa6
7 changed files with 145 additions and 95 deletions
+1 -1
View File
@@ -84,7 +84,7 @@ jobs:
RELEASE_BETA_VERSION: ${{ inputs.version }} RELEASE_BETA_VERSION: ${{ inputs.version }}
RELEASE_PREVIOUS_VERSION: ${{ inputs.previous_version }} RELEASE_PREVIOUS_VERSION: ${{ inputs.previous_version }}
DRY_RUN: ${{ inputs.dry_run }} DRY_RUN: ${{ inputs.dry_run }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.RELEASE_PAT }}
GITHUB_RUN_ID: ${{ github.run_id }} GITHUB_RUN_ID: ${{ github.run_id }}
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
run: npm run release-beta run: npm run release-beta
+1 -1
View File
@@ -84,7 +84,7 @@ jobs:
RELEASE_VERSION: ${{ inputs.version }} RELEASE_VERSION: ${{ inputs.version }}
RELEASE_PREVIOUS_VERSION: ${{ inputs.previous_version }} RELEASE_PREVIOUS_VERSION: ${{ inputs.previous_version }}
DRY_RUN: ${{ inputs.dry_run }} DRY_RUN: ${{ inputs.dry_run }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.RELEASE_PAT }}
GITHUB_RUN_ID: ${{ github.run_id }} GITHUB_RUN_ID: ${{ github.run_id }}
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
run: npm run release-final run: npm run release-final
+70 -5
View File
@@ -27,11 +27,7 @@ const outputFormat = JSON.stringify({
others: [192, 21], others: [192, 21],
}); });
const prompt = `Input Data: const prompt = `Input Data: {{ input }}
\`\`\`json
{{ input }}
\`\`\`
LLM Task: LLM Task:
- Output a one-line JSON object in the following format: - Output a one-line JSON object in the following format:
{{ outputFormat }} {{ outputFormat }}
@@ -190,6 +186,75 @@ export async function generateChangelog(previousVersion, categorizedMap) {
return content; return content;
} }
/**
* Generate Changelog using AI
* @param {string} previousVersion Previous Version Tag
* @returns {Promise<string>} Changelog Content
*/
export async function generateChangelogAI(previousVersion) {
// 1. Generate changelog
let categorizedMap = null;
console.log("Running opencode to categorize PRs...");
const llmPrompt = (await getPrompt(previousVersion)).replaceAll("\n", " ");
console.log(llmPrompt);
console.log("Running opencode with the above prompt...");
try {
const result = childProcess.spawnSync(
"opencode",
["run", "-m", "opencode/big-pickle", "--format", "json", llmPrompt],
{
encoding: "utf-8",
timeout: 120000,
shell: true,
cwd: process.cwd(),
env: process.env,
}
);
if (result.status === 0 && result.stdout) {
// Parse NDJSON output: find "type":"text" line
for (const line of result.stdout.trim().split("\n")) {
try {
const obj = JSON.parse(line);
if (obj.type === "text" && obj.part?.text) {
const jsonMatch = obj.part.text.match(/\{[\s\S]*\}/);
if (jsonMatch) {
categorizedMap = JSON.parse(jsonMatch[0]);
console.log("LLM categorization applied.");
break;
}
}
} catch {
// skip unparseable lines
}
}
if (!categorizedMap) {
console.warn("No JSON found in opencode response.");
console.warn(result.stdout);
}
} else {
console.warn("opencode failed or returned no output (status:", result.status, ")");
if (result.stderr) {
console.warn("stderr:", result.stderr.slice(0, 500));
}
}
} catch (e) {
console.warn("Failed to run opencode:", e.message);
}
if (!categorizedMap) {
categorizedMap = {};
console.log("OpenCode unavailable, using uncategorized fallback.");
}
return await generateChangelog(previousVersion, categorizedMap);
}
/** /**
* @param {string} previousVersion Previous Version Tag * @param {string} previousVersion Previous Version Tag
* @param {boolean} removeAuthor Whether to strip the author field from the returned PR list * @param {boolean} removeAuthor Whether to strip the author field from the returned PR list
+19
View File
@@ -0,0 +1,19 @@
import { createRelease } from "./lib.mjs";
import { generateChangelogAI } from "../generate-changelog.mjs";
const version = process.env.RELEASE_VERSION || process.env.RELEASE_BETA_VERSION;
const previousVersion = process.env.RELEASE_PREVIOUS_VERSION;
if (!version) {
console.error("RELEASE_VERSION is required");
process.exit(1);
}
if (!previousVersion) {
console.error("RELEASE_PREVIOUS_VERSION is required");
process.exit(1);
}
const isBeta = !!process.env.RELEASE_BETA_VERSION;
const changelog = await generateChangelogAI(previousVersion);
await createRelease(version, changelog, isBeta, "");
+4 -87
View File
@@ -1,7 +1,8 @@
import "dotenv/config"; import "dotenv/config";
import * as childProcess from "child_process"; import * as childProcess from "child_process";
import fs from "fs"; import fs from "fs";
import { generateChangelog, getPrompt } from "../generate-changelog.mjs"; import { generateChangelogAI } from "../generate-changelog.mjs";
import { createRelease } from "./lib.mjs";
const version = process.env.RELEASE_VERSION || process.env.RELEASE_BETA_VERSION; const version = process.env.RELEASE_VERSION || process.env.RELEASE_BETA_VERSION;
const previousVersion = process.env.RELEASE_PREVIOUS_VERSION; const previousVersion = process.env.RELEASE_PREVIOUS_VERSION;
@@ -41,96 +42,12 @@ if (!fs.existsSync(prNumberFile)) {
} }
const prNumber = fs.readFileSync(prNumberFile, "utf-8").trim(); const prNumber = fs.readFileSync(prNumberFile, "utf-8").trim();
// 1. Generate changelog
let categorizedMap = null;
console.log("Running opencode to categorize PRs...");
const llmPrompt = await getPrompt(previousVersion);
try {
const result = childProcess.spawnSync(
"opencode",
["run", "-m", "opencode/big-pickle", "--format", "json", llmPrompt],
{
encoding: "utf-8",
timeout: 120000,
cwd: process.cwd(),
env: process.env,
}
);
if (result.status === 0 && result.stdout) {
// Parse NDJSON output: find "type":"text" line
for (const line of result.stdout.trim().split("\n")) {
try {
const obj = JSON.parse(line);
if (obj.type === "text" && obj.part?.text) {
const jsonMatch = obj.part.text.match(/\{[\s\S]*\}/);
if (jsonMatch) {
categorizedMap = JSON.parse(jsonMatch[0]);
console.log("LLM categorization applied.");
break;
}
}
} catch {
// skip unparseable lines
}
}
if (!categorizedMap) {
console.warn("No JSON found in opencode response.");
console.warn("Last 500 chars:", result.stdout.slice(-500));
}
} else {
console.warn("opencode failed or returned no output (status:", result.status, ")");
if (result.stderr) {
console.warn("stderr:", result.stderr.slice(0, 500));
}
}
} catch (e) {
console.warn("Failed to run opencode:", e.message);
}
if (!categorizedMap) {
categorizedMap = {};
console.log("OpenCode unavailable, using uncategorized fallback.");
}
console.log("Generating changelog..."); console.log("Generating changelog...");
const changelog = await generateChangelog(previousVersion, categorizedMap); const changelog = await generateChangelogAI(previousVersion);
console.log("Changelog generated."); console.log("Changelog generated.");
// 2. Squash merge the PR // 2. Squash merge the PR
console.log(`Squash merging PR #${prNumber}...`); console.log(`Squash merging PR #${prNumber}...`);
execSync(`gh pr merge ${prNumber} --squash --delete-branch --subject "Update to ${version}" --admin`); execSync(`gh pr merge ${prNumber} --squash --delete-branch --subject "Update to ${version}" --admin`);
// 3. Create draft release with changelog and dist.tar.gz await createRelease(version, changelog, isBeta, distTarGz);
console.log(`Creating draft release ${version}...`);
if (!fs.existsSync(distTarGz)) {
console.error(`dist.tar.gz not found: ${distTarGz}`);
process.exit(1);
}
const releaseBody = `## ${version}
${changelog}`;
const releaseArgs = ["release", "create", version, distTarGz, "--draft", "--title", version, "--notes", releaseBody];
if (isBeta) {
releaseArgs.push("--prerelease");
}
const result = childProcess.spawnSync("gh", releaseArgs, {
encoding: "utf-8",
stdio: "inherit",
});
if (result.status !== 0) {
console.error("Failed to create release");
process.exit(1);
}
console.log(`Release ${version} is ready (draft).`);
console.log("Next steps:");
console.log(` 1. Review the draft release: https://github.com/louislam/uptime-kuma/releases/tag/${version}`);
console.log(` 2. Edit if needed and publish.`);
+48
View File
@@ -373,3 +373,51 @@ The \`dist.tar.gz\` archive will be available as an artifact in the [workflow ru
console.log("Successfully created draft pull request"); console.log("Successfully created draft pull request");
return prNumber; return prNumber;
} }
/**
* @param {string} version
* @param {string} distTarGz If empty, it will not be uploaded to the release
* @param {string} changelog
* @param {boolean} isBeta
* @returns {Promise<void>}
*/
export async function createRelease(version, changelog, isBeta = false, distTarGz = undefined) {
// 3. Create draft release with changelog and dist.tar.gz
console.log(`Creating draft release ${version}...`);
const releaseBody = `## ${version}
${changelog}`;
let releaseArgs = ["release", "create", version];
if (distTarGz) {
if (!fs.existsSync(distTarGz)) {
console.error(`dist.tar.gz not found: ${distTarGz}`);
process.exit(1);
}
releaseArgs.push(distTarGz);
}
releaseArgs = releaseArgs.concat(["--draft", "--title", version, "--notes", releaseBody]);
if (isBeta) {
releaseArgs.push("--prerelease");
}
const result = childProcess.spawnSync("gh", releaseArgs, {
encoding: "utf-8",
stdio: "inherit",
});
if (result.status !== 0) {
console.error("Failed to create release");
process.exit(1);
}
console.log(`Release ${version} is ready (draft).`);
console.log("Next steps:");
console.log(` 1. Review the draft release: https://github.com/louislam/uptime-kuma/releases/tag/${version}`);
console.log(` 2. Edit if needed and publish.`);
}
+2 -1
View File
@@ -66,7 +66,8 @@
"start-dev-container": "cd docker && docker-compose -f docker-compose-dev.yml up --force-recreate", "start-dev-container": "cd docker && docker-compose -f docker-compose-dev.yml up --force-recreate",
"rebase-pr-to-1.23.X": "node extra/rebase-pr.js 1.23.X", "rebase-pr-to-1.23.X": "node extra/rebase-pr.js 1.23.X",
"reset-migrate-aggregate-table-state": "node extra/reset-migrate-aggregate-table-state.js", "reset-migrate-aggregate-table-state": "node extra/reset-migrate-aggregate-table-state.js",
"generate-changelog": "node ./extra/generate-changelog.mjs" "generate-changelog": "node ./extra/generate-changelog.mjs",
"create-release": "node ./extra/release/create-release.mjs"
}, },
"dependencies": { "dependencies": {
"@grpc/grpc-js": "~1.8.22", "@grpc/grpc-js": "~1.8.22",