Files
openwork/scripts/migration/01-cut-migration-release.mjs

197 lines
7.2 KiB
JavaScript
Executable File

#!/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);
});