#!/usr/bin/env node // Cut the Tauri → Electron migration release (v0.12.0 or whatever). // Bumps versions, writes the migration-release env fragment, commits, tags, // pushes. Idempotent safeties: refuses to run on a dirty tree. // // Usage: // node scripts/migration/01-cut-migration-release.mjs \ // --version 0.12.0 \ // --mac-url 'https://.../OpenWork-darwin-arm64-0.12.0-mac.zip' \ // --mac-arm64-url 'https://.../openwork-mac-arm64-0.12.0.zip' \ // --mac-x64-url 'https://.../openwork-mac-x64-0.12.0.zip' \ // --win-url 'https://.../OpenWork-Setup-0.12.0.exe' (optional) \ // --linux-url 'https://.../OpenWork-0.12.0.AppImage' (optional) \ // --dry-run import { spawnSync } from "node:child_process"; import { existsSync } from "node:fs"; import { writeFile } from "node:fs/promises"; import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; const __dirname = dirname(fileURLToPath(import.meta.url)); const repoRoot = resolve(__dirname, "../.."); function parseArgs(argv) { const out = { dryRun: false }; for (let i = 0; i < argv.length; i++) { const arg = argv[i]; if (arg === "--dry-run") out.dryRun = true; else if (arg === "--version") out.version = argv[++i]; else if (arg === "--mac-url") out.macUrl = argv[++i]; else if (arg === "--mac-arm64-url") out.macArm64Url = argv[++i]; else if (arg === "--mac-x64-url") out.macX64Url = argv[++i]; else if (arg === "--mac-sha256") out.macSha256 = argv[++i]; else if (arg === "--win-url") out.winUrl = argv[++i]; else if (arg === "--win-x64-url") out.winX64Url = argv[++i]; else if (arg === "--linux-url") out.linuxUrl = argv[++i]; else if (arg === "--linux-arm64-url") out.linuxArm64Url = argv[++i]; else if (arg === "--linux-x64-url") out.linuxX64Url = argv[++i]; else if (arg === "--help" || arg === "-h") out.help = true; else { console.error(`unknown arg: ${arg}`); process.exit(2); } } return out; } function die(msg) { console.error(`[cut-release] ${msg}`); process.exit(1); } function run(cmd, args, opts = {}) { console.log(`[cut-release] $ ${cmd} ${args.join(" ")}`); if (opts.dryRun) return ""; const result = spawnSync(cmd, args, { cwd: opts.cwd ?? repoRoot, stdio: opts.capture ? ["ignore", "pipe", "inherit"] : "inherit", encoding: "utf8", }); if (result.status !== 0) die(`command failed: ${cmd}`); return (result.stdout ?? "").trim(); } function gitStatusClean() { const result = spawnSync("git", ["status", "--porcelain"], { cwd: repoRoot, encoding: "utf8", }); return result.status === 0 && (result.stdout ?? "").trim().length === 0; } function currentBranch() { const result = spawnSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd: repoRoot, encoding: "utf8", }); return (result.stdout ?? "").trim(); } function tagExists(tag) { const result = spawnSync("git", ["rev-parse", "-q", "--verify", `refs/tags/${tag}`], { cwd: repoRoot, encoding: "utf8", }); return result.status === 0; } function remoteTagExists(tag) { const result = spawnSync("git", ["ls-remote", "--tags", "origin", tag], { cwd: repoRoot, encoding: "utf8", }); return result.status === 0 && (result.stdout ?? "").includes(`refs/tags/${tag}`); } async function main() { const args = parseArgs(process.argv.slice(2)); if (args.help) { console.log( [ "Cut the Tauri → Electron migration release.", "", "Required: --version, --mac-url (or --mac-arm64-url + --mac-x64-url)", "Optional: --mac-sha256, --win-url, --win-x64-url, --linux-url, --linux-arm64-url, --linux-x64-url, --dry-run", ].join("\n"), ); return; } if (!args.version) die("--version is required (e.g. --version 0.12.0)"); if (!args.macUrl && (!args.macArm64Url || !args.macX64Url)) { die("--mac-url or both --mac-arm64-url + --mac-x64-url are required"); } if (!/^\d+\.\d+\.\d+$/.test(args.version)) { die(`--version must look like X.Y.Z, got "${args.version}"`); } if (!gitStatusClean()) { die("working tree is dirty. commit or stash before cutting a release."); } const branch = currentBranch(); if (branch !== "dev" && branch !== "main") { console.warn( `[cut-release] WARNING: current branch is "${branch}". Releases are usually cut from main/dev.`, ); } const tag = `v${args.version}`; if (tagExists(tag)) die(`tag ${tag} already exists locally`); if (remoteTagExists(tag)) die(`tag ${tag} already exists on origin`); // 1. Bump all 5 sync files via the existing helper. run("pnpm", ["bump:set", "--", args.version], { dryRun: args.dryRun }); // Keep CI/release jobs using --frozen-lockfile green after bumping // workspace package versions referenced by pnpm-lock.yaml. run("pnpm", ["install", "--no-frozen-lockfile"], { dryRun: args.dryRun }); // 2. Write the migration-release env fragment. Gets picked up by the // app build step during `Release App` via --copy-config. const envFragment = [ "# Generated by scripts/migration/01-cut-migration-release.mjs.", "# Consumed by apps/app Vite build during the v0.12.0 release only.", "VITE_OPENWORK_MIGRATION_RELEASE=1", `VITE_OPENWORK_MIGRATION_VERSION=${args.version}`, args.macUrl ? `VITE_OPENWORK_MIGRATION_MAC_URL=${args.macUrl}` : "", args.macArm64Url ? `VITE_OPENWORK_MIGRATION_MAC_ARM64_URL=${args.macArm64Url}` : "", args.macX64Url ? `VITE_OPENWORK_MIGRATION_MAC_X64_URL=${args.macX64Url}` : "", args.macSha256 ? `VITE_OPENWORK_MIGRATION_MAC_SHA256=${args.macSha256}` : "", args.winUrl ? `VITE_OPENWORK_MIGRATION_WINDOWS_URL=${args.winUrl}` : "", args.winX64Url ? `VITE_OPENWORK_MIGRATION_WINDOWS_X64_URL=${args.winX64Url}` : "", args.linuxUrl ? `VITE_OPENWORK_MIGRATION_LINUX_URL=${args.linuxUrl}` : "", args.linuxArm64Url ? `VITE_OPENWORK_MIGRATION_LINUX_ARM64_URL=${args.linuxArm64Url}` : "", args.linuxX64Url ? `VITE_OPENWORK_MIGRATION_LINUX_X64_URL=${args.linuxX64Url}` : "", "", ] .filter(Boolean) .join("\n"); const envPath = resolve(repoRoot, "apps/app/.env.migration-release"); console.log(`[cut-release] writing ${envPath}`); if (!args.dryRun) { await writeFile(envPath, envFragment, "utf8"); } // 3. Commit version bump + env fragment. run("git", ["add", "-A"], { dryRun: args.dryRun }); run( "git", ["commit", "-m", `chore(release): cut v${args.version} (Tauri → Electron migration)`], { dryRun: args.dryRun }, ); // 4. Tag + push. run("git", ["tag", tag], { dryRun: args.dryRun }); run("git", ["push", "origin", branch], { dryRun: args.dryRun }); run("git", ["push", "origin", tag], { dryRun: args.dryRun }); console.log(""); console.log(`[cut-release] pushed ${tag}.`); console.log(`[cut-release] watch the workflow:`); console.log(` gh run list --repo different-ai/openwork --workflow "Release App" --limit 3`); console.log(` gh run watch --repo different-ai/openwork`); console.log(""); console.log(`[cut-release] once the workflow finishes, run:`); console.log(` node scripts/migration/02-validate-migration.mjs --tag ${tag}`); } main().catch((err) => { console.error(err); process.exit(1); });