diff --git a/.github/workflows/release-beta.yml b/.github/workflows/release-beta.yml index 2e4c15138..b1d0f46db 100644 --- a/.github/workflows/release-beta.yml +++ b/.github/workflows/release-beta.yml @@ -84,7 +84,7 @@ jobs: RELEASE_BETA_VERSION: ${{ inputs.version }} RELEASE_PREVIOUS_VERSION: ${{ inputs.previous_version }} DRY_RUN: ${{ inputs.dry_run }} - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ secrets.RELEASE_PAT }} GITHUB_RUN_ID: ${{ github.run_id }} OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} run: npm run release-beta diff --git a/.github/workflows/release-final.yml b/.github/workflows/release-final.yml index 045d243e5..81aa986c3 100644 --- a/.github/workflows/release-final.yml +++ b/.github/workflows/release-final.yml @@ -84,7 +84,7 @@ jobs: RELEASE_VERSION: ${{ inputs.version }} RELEASE_PREVIOUS_VERSION: ${{ inputs.previous_version }} DRY_RUN: ${{ inputs.dry_run }} - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ secrets.RELEASE_PAT }} GITHUB_RUN_ID: ${{ github.run_id }} OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} run: npm run release-final diff --git a/extra/generate-changelog.mjs b/extra/generate-changelog.mjs index 1d7bb9e8f..d8955d1f6 100644 --- a/extra/generate-changelog.mjs +++ b/extra/generate-changelog.mjs @@ -27,11 +27,7 @@ const outputFormat = JSON.stringify({ others: [192, 21], }); -const prompt = `Input Data: -\`\`\`json -{{ input }} -\`\`\` - +const prompt = `Input Data: {{ input }} LLM Task: - Output a one-line JSON object in the following format: {{ outputFormat }} @@ -190,6 +186,75 @@ export async function generateChangelog(previousVersion, categorizedMap) { return content; } +/** + * Generate Changelog using AI + * @param {string} previousVersion Previous Version Tag + * @returns {Promise} 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 {boolean} removeAuthor Whether to strip the author field from the returned PR list diff --git a/extra/release/create-release.mjs b/extra/release/create-release.mjs new file mode 100644 index 000000000..46770b8c8 --- /dev/null +++ b/extra/release/create-release.mjs @@ -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, ""); diff --git a/extra/release/finish.mjs b/extra/release/finish.mjs index 71a362f14..f93c7b033 100644 --- a/extra/release/finish.mjs +++ b/extra/release/finish.mjs @@ -1,7 +1,8 @@ import "dotenv/config"; import * as childProcess from "child_process"; 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 previousVersion = process.env.RELEASE_PREVIOUS_VERSION; @@ -41,96 +42,12 @@ if (!fs.existsSync(prNumberFile)) { } 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..."); -const changelog = await generateChangelog(previousVersion, categorizedMap); +const changelog = await generateChangelogAI(previousVersion); 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.`); +await createRelease(version, changelog, isBeta, distTarGz); diff --git a/extra/release/lib.mjs b/extra/release/lib.mjs index 2b5b4a946..59dfcfec0 100644 --- a/extra/release/lib.mjs +++ b/extra/release/lib.mjs @@ -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"); 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} + */ +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.`); +} diff --git a/package.json b/package.json index fa0646946..67e66f931 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,8 @@ "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", "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": { "@grpc/grpc-js": "~1.8.22",