From 77bca78dd314a08c1e452dded539902f9b2414e6 Mon Sep 17 00:00:00 2001 From: can1357 Date: Sun, 4 Jan 2026 16:16:15 +0100 Subject: [PATCH] chore: migrated release script to TypeScript --- package.json | 4 +- scripts/release.mjs | 145 ------------------------------------- scripts/release.ts | 172 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 173 insertions(+), 148 deletions(-) delete mode 100755 scripts/release.mjs create mode 100755 scripts/release.ts diff --git a/package.json b/package.json index 66574a4e9..15aa41d56 100644 --- a/package.json +++ b/package.json @@ -20,9 +20,7 @@ "prepublishOnly": "bun run build && bun run check", "publish": "bun run prepublishOnly && npm publish -ws --access public", "publish:dry": "bun run prepublishOnly && npm publish -ws --access public --dry-run", - "release:patch": "bun scripts/release.mjs patch", - "release:minor": "bun scripts/release.mjs minor", - "release:major": "bun scripts/release.mjs major" + "release": "bun scripts/release.ts" }, "devDependencies": { "@biomejs/biome": "2.3.10", diff --git a/scripts/release.mjs b/scripts/release.mjs deleted file mode 100755 index 12b33d045..000000000 --- a/scripts/release.mjs +++ /dev/null @@ -1,145 +0,0 @@ -#!/usr/bin/env node -/** - * Release script for pi-mono - * - * Usage: node scripts/release.mjs - * - * Steps: - * 1. Check for uncommitted changes - * 2. Bump version via npm run version:xxx - * 3. Update CHANGELOG.md files: [Unreleased] -> [version] - date - * 4. Commit and tag - * 5. Publish to npm - * 6. Add new [Unreleased] section to changelogs - * 7. Commit - */ - -import { execSync } from "child_process"; -import { readFileSync, writeFileSync, readdirSync, existsSync } from "fs"; -import { join } from "path"; - -const BUMP_TYPE = process.argv[2]; - -if (!["major", "minor", "patch"].includes(BUMP_TYPE)) { - console.error("Usage: node scripts/release.mjs "); - process.exit(1); -} - -function run(cmd, options = {}) { - console.log(`$ ${cmd}`); - try { - return execSync(cmd, { encoding: "utf-8", stdio: options.silent ? "pipe" : "inherit", ...options }); - } catch (e) { - if (!options.ignoreError) { - console.error(`Command failed: ${cmd}`); - process.exit(1); - } - return null; - } -} - -function getVersion() { - const pkg = JSON.parse(readFileSync("packages/ai/package.json", "utf-8")); - return pkg.version; -} - -function getChangelogs() { - const packagesDir = "packages"; - const packages = readdirSync(packagesDir); - return packages - .map((pkg) => join(packagesDir, pkg, "CHANGELOG.md")) - .filter((path) => existsSync(path)); -} - -function updateChangelogsForRelease(version) { - const date = new Date().toISOString().split("T")[0]; - const changelogs = getChangelogs(); - - for (const changelog of changelogs) { - const content = readFileSync(changelog, "utf-8"); - - if (!content.includes("## [Unreleased]")) { - console.log(` Skipping ${changelog}: no [Unreleased] section`); - continue; - } - - const updated = content.replace( - "## [Unreleased]", - `## [${version}] - ${date}` - ); - writeFileSync(changelog, updated); - console.log(` Updated ${changelog}`); - } -} - -function addUnreleasedSection() { - const changelogs = getChangelogs(); - const unreleasedSection = "## [Unreleased]\n\n"; - - for (const changelog of changelogs) { - const content = readFileSync(changelog, "utf-8"); - - // Insert after "# Changelog\n\n" - const updated = content.replace( - /^(# Changelog\n\n)/, - `$1${unreleasedSection}` - ); - writeFileSync(changelog, updated); - console.log(` Added [Unreleased] to ${changelog}`); - } -} - -// Main flow -console.log("\n=== Release Script ===\n"); - -// 1. Check for uncommitted changes -console.log("Checking for uncommitted changes..."); -const status = run("git status --porcelain", { silent: true }); -if (status && status.trim()) { - console.error("Error: Uncommitted changes detected. Commit or stash first."); - console.error(status); - process.exit(1); -} -console.log(" Working directory clean\n"); - -// 2. Bump version -console.log(`Bumping version (${BUMP_TYPE})...`); -run(`npm run version:${BUMP_TYPE}`); -const version = getVersion(); -console.log(` New version: ${version}\n`); - -// 3. Update changelogs -console.log("Updating CHANGELOG.md files..."); -updateChangelogsForRelease(version); -console.log(); - -// 4. Commit and tag -console.log("Committing and tagging..."); -run("git add ."); -run(`git commit -m "Release v${version}"`); -run(`git tag v${version}`); -console.log(); - -// 5. Publish -console.log("Publishing to npm..."); -run("npm run publish"); -console.log(); - -// 6. Add new [Unreleased] sections -console.log("Adding [Unreleased] sections for next cycle..."); -addUnreleasedSection(); -console.log(); - -// 7. Commit -console.log("Committing changelog updates..."); -run("git add ."); -run(`git commit -m "Add [Unreleased] section for next cycle"`); -console.log(); - -// 8. Push -console.log("Pushing to remote..."); -run("git push origin main"); -run(`git push origin v${version}`); -console.log(); - -console.log(`=== Released v${version} ===`); diff --git a/scripts/release.ts b/scripts/release.ts new file mode 100755 index 000000000..ce96d416b --- /dev/null +++ b/scripts/release.ts @@ -0,0 +1,172 @@ +#!/usr/bin/env bun +/** + * Release script for pi-mono + * + * Usage: bun scripts/release.ts + * + * Example: bun scripts/release.ts 3.10.0 + * + * Steps: + * 1. Pre-flight checks (clean working dir, on main branch) + * 2. Update package.json versions + * 3. Regenerate lockfile + * 4. Update CHANGELOGs ([Unreleased] -> [version] - date, add new [Unreleased]) + * 5. Commit and tag + * 6. Push + * 7. Watch CI + */ + +import type { Subprocess } from "bun"; +import { Glob } from "bun"; + +const VERSION = process.argv[2]; + +if (!VERSION || !/^\d+\.\d+\.\d+/.test(VERSION)) { + console.error("Usage: bun scripts/release.ts "); + console.error("Example: bun scripts/release.ts 3.10.0"); + process.exit(1); +} + +interface RunOptions { + silent?: boolean; + capture?: boolean; + ignoreError?: boolean; +} + +function run(cmd: string[], options: RunOptions = {}): string { + const cmdStr = cmd.join(" "); + if (!options.silent) console.log(`$ ${cmdStr}`); + + const result = Bun.spawnSync(cmd, { + stdout: options.capture ? "pipe" : "inherit", + stderr: "inherit", + }); + + if (result.exitCode !== 0 && !options.ignoreError) { + console.error(`Command failed: ${cmdStr}`); + process.exit(1); + } + + return result.stdout?.toString().trim() ?? ""; +} + +async function getChangelogs(): Promise { + const glob = new Glob("packages/*/CHANGELOG.md"); + const paths: string[] = []; + for await (const path of glob.scan(".")) { + paths.push(path); + } + return paths; +} + +async function getPackageJsonPaths(): Promise { + const glob = new Glob("packages/*/package.json"); + const paths: string[] = []; + for await (const path of glob.scan(".")) { + paths.push(path); + } + return paths; +} + +async function updateChangelogsForRelease(version: string): Promise { + const date = new Date().toISOString().split("T")[0]; + const changelogs = await getChangelogs(); + + for (const changelog of changelogs) { + let content = await Bun.file(changelog).text(); + + if (!content.includes("## [Unreleased]")) { + console.log(` Skipping ${changelog}: no [Unreleased] section`); + continue; + } + + // Replace [Unreleased] with version and date + content = content.replace("## [Unreleased]", `## [${version}] - ${date}`); + + // Add new [Unreleased] section after # Changelog header + content = content.replace(/^(# Changelog\n\n)/, `$1## [Unreleased]\n\n`); + + await Bun.write(changelog, content); + console.log(` Updated ${changelog}`); + } +} + +// === Main Flow === + +console.log("\n=== Release Script ===\n"); + +// 1. Pre-flight checks +console.log("Pre-flight checks..."); + +const branch = run(["git", "branch", "--show-current"], { capture: true, silent: true }); +if (branch !== "main") { + console.error(`Error: Must be on main branch (currently on '${branch}')`); + process.exit(1); +} +console.log(" On main branch"); + +const status = run(["git", "status", "--porcelain"], { capture: true, silent: true }); +if (status) { + console.error("Error: Uncommitted changes detected. Commit or stash first."); + console.error(status); + process.exit(1); +} +console.log(" Working directory clean\n"); + +// 2. Update package versions +console.log(`Updating package versions to ${VERSION}...`); +const pkgJsonPaths = await getPackageJsonPaths(); +run(["sd", '"version": "[^"]+"', `"version": "${VERSION}"`, ...pkgJsonPaths]); + +// Verify +console.log(" Verifying versions:"); +for (const pkgPath of pkgJsonPaths) { + const pkgJson = await Bun.file(pkgPath).json(); + console.log(` ${pkgJson.name}: ${pkgJson.version}`); +} +console.log(); + +// 3. Regenerate lockfile +console.log("Regenerating lockfile..."); +run(["rm", "-f", "bun.lock"]); +run(["bun", "install"]); +console.log(); + +// 4. Update changelogs +console.log("Updating CHANGELOGs..."); +await updateChangelogsForRelease(VERSION); +console.log(); + +// 5. Commit and tag +console.log("Committing and tagging..."); +run(["git", "add", "."]); +run(["git", "commit", "-m", `chore: bump version to ${VERSION}`]); +run(["git", "tag", `v${VERSION}`]); +console.log(); + +// 6. Push +console.log("Pushing to remote..."); +run(["git", "push", "origin", "main"]); +run(["git", "push", "origin", `v${VERSION}`]); +console.log(); + +// 7. Watch CI +console.log("Watching CI..."); +const runListOutput = run(["gh", "run", "list", "--limit", "1", "--json", "databaseId"], { capture: true, silent: true }); +const runs = JSON.parse(runListOutput); +if (runs.length > 0) { + const runId = runs[0].databaseId; + console.log(` Watching run ${runId}...`); + run(["gh", "run", "watch", String(runId), "--exit-status"], { ignoreError: true }); +} +console.log(); + +console.log(`=== Released v${VERSION} ===`); +console.log(); +console.log("If CI failed, fix the issue and run:"); +console.log(" git commit --amend --no-edit"); +console.log(" git push origin main --force"); +console.log(` git tag -f v${VERSION} && git push origin v${VERSION} --force`); +console.log(); +console.log("To publish to npm:"); +console.log(" npm run publish");