chore: Full auto release process (#7657)

This commit is contained in:
Louis Lam
2026-08-01 17:21:34 +08:00
committed by GitHub
parent 7bbc6f3287
commit d14d4de706
7 changed files with 163 additions and 33 deletions
+1
View File
@@ -83,6 +83,7 @@ jobs:
DRY_RUN: ${{ inputs.dry_run }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_RUN_ID: ${{ github.run_id }}
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
run: npm run release-beta
- name: Upload dist.tar.gz as artifact
+1
View File
@@ -83,6 +83,7 @@ jobs:
DRY_RUN: ${{ inputs.dry_run }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_RUN_ID: ${{ github.run_id }}
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
run: npm run release-final
- name: Upload dist.tar.gz as artifact
+2 -2
View File
@@ -195,7 +195,7 @@ export async function generateChangelog(previousVersion, categorizedMap) {
* @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, removeAuthor = false) {
export 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=iso8601-strict ${previousVersion}`)
@@ -287,7 +287,7 @@ async function getAuthorList(prID) {
* @param {Set<string>} authorSet Set of Authors
* @returns {Set<string>} New Set with mainAuthor at the front
*/
async function mainAuthorToFront(mainAuthor, authorSet) {
export async function mainAuthorToFront(mainAuthor, authorSet) {
if (ignoreList.includes(mainAuthor)) {
return authorSet;
}
+3
View File
@@ -78,3 +78,6 @@ if (!dryRun) {
// Create dist.tar.gz
await createDistTarGz();
// Auto-finish: generate changelog, squash merge PR (non-dry-run only), create draft release with dist.tar.gz
await import("./finish.mjs");
+3
View File
@@ -84,5 +84,8 @@ if (!dryRun) {
// Create dist.tar.gz
await createDistTarGz();
// Auto-finish: generate changelog, squash merge PR (non-dry-run only), create draft release with dist.tar.gz
await import("./finish.mjs");
// Removed update wiki to keep it simple
// Do this in the wiki repo instead
+128
View File
@@ -0,0 +1,128 @@
import "dotenv/config";
import * as childProcess from "child_process";
import fs from "fs";
import { generateChangelog, getPrompt } from "../generate-changelog.mjs";
const version = process.env.RELEASE_VERSION || process.env.RELEASE_BETA_VERSION;
const previousVersion = process.env.RELEASE_PREVIOUS_VERSION;
const dryRun = process.env.DRY_RUN === "true";
const isBeta = !!process.env.RELEASE_BETA_VERSION;
const prNumberFile = "./tmp/pr-number.txt";
const distTarGz = "./tmp/dist.tar.gz";
if (!version) {
console.error("RELEASE_VERSION is required");
process.exit(1);
}
if (!previousVersion) {
console.error("RELEASE_PREVIOUS_VERSION is required");
process.exit(1);
}
console.log(`Finishing release ${version}...`);
/**
* @param cmd
*/
function execSync(cmd) {
if (dryRun) {
console.log(`[DRY RUN] ${cmd}`);
} else {
childProcess.execSync(cmd, { stdio: "inherit" });
}
}
// Read PR number
if (!fs.existsSync(prNumberFile)) {
console.error(`PR number file not found: ${prNumberFile}`);
console.error("The PR must be created by the release script first.");
process.exit(1);
}
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",
["-p", llmPrompt, "-f", "text", "-q", "-m", "opencode/big-pickle"],
{
encoding: "utf-8",
timeout: 120000,
cwd: process.cwd(),
env: process.env,
}
);
if (result.status === 0 && result.stdout) {
// Extract JSON from opencode's text output
const jsonMatch = result.stdout.match(/\{[^{}]*"improvements"[^{}]*\}/s) || result.stdout.match(/\{[\s\S]*\}/);
if (jsonMatch) {
try {
categorizedMap = JSON.parse(jsonMatch[0]);
console.log("LLM categorization (opencode) applied.");
} catch (e) {
console.warn("Failed to parse opencode JSON output:", e.message);
console.warn("Raw output:", result.stdout.slice(0, 500));
}
} else {
console.warn("Could not find JSON in opencode output.");
}
} 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...");
const changelog = await generateChangelog(previousVersion, categorizedMap);
console.log("Changelog generated.");
// 2. Squash merge the PR
console.log(`Squash merging PR #${prNumber}...`);
execSync(`gh pr merge ${prNumber} --squash --delete-branch --subject "Update to ${version}" --admin`);
// 3. Create draft release with changelog and dist.tar.gz
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.`);
+25 -31
View File
@@ -1,7 +1,6 @@
import "dotenv/config";
import * as childProcess from "child_process";
import semver from "semver";
import { getPrompt } from "../generate-changelog.mjs";
import fs from "fs";
import tar from "tar";
@@ -305,11 +304,9 @@ export async function createDistTarGz() {
* @param {boolean} dryRun Still create the PR, but add "[DRY RUN]" to the title
* @param {string} branchName The branch name to use for the PR head (defaults to "release")
* @param {string} githubRunId The GitHub Actions run ID for linking to artifacts
* @returns {Promise<void>}
* @returns {Promise<number>} The PR number
*/
export async function createReleasePR(version, previousVersion, dryRun, branchName = "release", githubRunId = null) {
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
@@ -317,33 +314,17 @@ export async function createReleasePR(version, previousVersion, dryRun, branchNa
? `https://github.com/louislam/uptime-kuma/actions/runs/${githubRunId}/workflow`
: `https://github.com/louislam/uptime-kuma/actions/workflows/beta-release.yml`;
const tmpDir = "./tmp";
if (!fs.existsSync(tmpDir)) {
fs.mkdirSync(tmpDir, { recursive: true });
}
const body = `## Release ${version}
This PR prepares the release for version ${version}.
### Manual Steps Required
- [ ] Merge this PR (squash and merge)
- [ ] Create a new release on GitHub with the tag \`${version}\`.
- [ ] Ask any LLM to categorize the changelog into sections.
- [ ] Place the changelog in the release note.
- [ ] Download the \`dist.tar.gz\` artifact from the [workflow run](${artifactLink}) and upload it to the release.
- [ ] (Beta only) Set prerelease
- [ ] Publish the release note on GitHub.
### Ask LLM to categorize the changelog
\`\`\`md
${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
The \`dist.tar.gz\` archive will be available as an artifact in the workflow run.
The \`dist.tar.gz\` archive will be available as an artifact in the [workflow run](${artifactLink}).
`;
// Create the PR using gh CLI
@@ -365,17 +346,30 @@ The \`dist.tar.gz\` archive will be available as an artifact in the workflow run
const result = childProcess.spawnSync("gh", args, {
encoding: "utf-8",
stdio: "inherit",
env: {
...process.env,
GH_TOKEN: process.env.GH_TOKEN || process.env.GITHUB_TOKEN,
},
stdio: "pipe",
});
if (result.status !== 0) {
console.error(result.stderr);
console.error("Failed to create pull request");
process.exit(1);
}
const prUrl = result.stdout.trim();
console.log(prUrl);
// Extract PR number from URL (e.g., https://github.com/louislam/uptime-kuma/pull/1234)
const prNumberMatch = prUrl.match(/\/pull\/(\d+)/);
const prNumber = prNumberMatch ? parseInt(prNumberMatch[1], 10) : null;
if (prNumber) {
console.log(`PR number: ${prNumber}`);
// Save PR number to file for the finish script
fs.writeFileSync(`${tmpDir}/pr-number.txt`, String(prNumber));
} else {
console.warn("Could not extract PR number from URL, auto-finish will not be possible");
}
console.log("Successfully created draft pull request");
return prNumber;
}