mirror of
https://github.com/different-ai/openwork
synced 2026-04-25 17:15:34 +02:00
Ships the last-missing piece of the Electron migration. After this PR
merges there are three one-shot scripts under scripts/migration/ that
carry the whole lifecycle from "cut v0.12.0" through "delete src-tauri":
01-cut-migration-release.mjs # bumps versions, writes the release env
# fragment, tags, pushes.
02-validate-migration.mjs # guided smoke test against a cut release.
03-post-migration-cleanup.mjs # deletes src-tauri, flips defaults,
# scrubs Tauri docs (dry-run by default).
Code landing in the same PR (dormant until a release sets
VITE_OPENWORK_MIGRATION_RELEASE=1):
- apps/desktop/src-tauri/src/commands/migration.rs gains
migrate_to_electron() — downloads the matching Electron .zip, verifies
the Apple signature, swaps the .app bundle in place via a detached
shell script, relaunches, and exits. Windows + Linux branches are
stubbed with clear TODOs for the follow-up.
- apps/app/src/app/lib/migration.ts grows migrateToElectron() + a
"later" defer helper.
- apps/app/src/react-app/shell/migration-prompt.tsx adds the one-time
"OpenWork is moving to a new engine" modal. Mounted from
providers.tsx. Gated on isTauriRuntime() AND the build-time flag, so
Electron users and all non-migration-release builds never render it.
- apps/app/vite.config.ts loads apps/app/.env.migration-release when
present so the prompt gets the release-specific download URLs.
- .gitignore allows the migration-release fragment to be committed only
on the tagged migration-release commit (removed in cleanup step).
Release workflow:
- .github/workflows/release-macos-aarch64.yml gains a publish-electron
job alongside the Tauri jobs. Gated on RELEASE_PUBLISH_ELECTRON repo
var OR the new publish_electron workflow_dispatch input (default
false). Uses the existing Apple Dev ID secrets — no new credential
story. Produces latest-mac.yml alongside Tauri's latest.json so a
v0.12.0 release serves both updaters.
Verified:
pnpm --filter @openwork/app typecheck ✓
cargo check --manifest-path apps/desktop/src-tauri/Cargo.toml ✓
node --check on all mjs scripts ✓
python yaml parse of release-macos-aarch64.yml ✓
Not verified (needs a real release cycle):
end-to-end migration from a signed Tauri .app to a signed Electron
.app through the detached-script install swap.
Co-authored-by: Benjamin Shafii <benjamin@openworklabs.com>
179 lines
5.9 KiB
JavaScript
Executable File
179 lines
5.9 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' \
|
|
// --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-sha256") out.macSha256 = argv[++i];
|
|
else if (arg === "--win-url") out.winUrl = argv[++i];
|
|
else if (arg === "--linux-url") out.linuxUrl = 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",
|
|
"Optional: --mac-sha256, --win-url, --linux-url, --dry-run",
|
|
].join("\n"),
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (!args.version) die("--version is required (e.g. --version 0.12.0)");
|
|
if (!args.macUrl) die("--mac-url is 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 });
|
|
|
|
// 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.macSha256 ? `VITE_OPENWORK_MIGRATION_MAC_SHA256=${args.macSha256}` : "",
|
|
args.winUrl ? `VITE_OPENWORK_MIGRATION_WINDOWS_URL=${args.winUrl}` : "",
|
|
args.linuxUrl ? `VITE_OPENWORK_MIGRATION_LINUX_URL=${args.linuxUrl}` : "",
|
|
"",
|
|
]
|
|
.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);
|
|
});
|