mirror of
https://github.com/louislam/uptime-kuma.git
synced 2026-08-07 10:04:59 +00:00
chore: Full auto release process (#7657)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user