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>
212 lines
6.8 KiB
JavaScript
Executable File
212 lines
6.8 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
// Post-migration cleanup. Run this ONLY after v0.12.x has been stable
|
|
// for ~1-2 weeks and telemetry confirms users have rolled over to the
|
|
// Electron build. Irreversible (well, revertible via git, but not
|
|
// trivial to redeploy the Tauri path after removing it).
|
|
//
|
|
// Usage:
|
|
// node scripts/migration/03-post-migration-cleanup.mjs --dry-run # default
|
|
// node scripts/migration/03-post-migration-cleanup.mjs --execute # actually do it
|
|
|
|
import { spawnSync } from "node:child_process";
|
|
import { existsSync } from "node:fs";
|
|
import { readFile, 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) {
|
|
// Default is dry-run for safety; --execute required to actually do it.
|
|
const out = { dryRun: true };
|
|
for (const arg of argv) {
|
|
if (arg === "--execute") out.dryRun = false;
|
|
else if (arg === "--dry-run") out.dryRun = true;
|
|
else if (arg === "--help" || arg === "-h") out.help = true;
|
|
else {
|
|
console.error(`unknown arg: ${arg}`);
|
|
process.exit(2);
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function log(msg) {
|
|
console.log(`[cleanup] ${msg}`);
|
|
}
|
|
|
|
function run(cmd, args, opts = {}) {
|
|
log(`$ ${cmd} ${args.join(" ")}`);
|
|
if (opts.dryRun) return "";
|
|
const result = spawnSync(cmd, args, {
|
|
cwd: opts.cwd ?? repoRoot,
|
|
stdio: "inherit",
|
|
});
|
|
if (result.status !== 0) {
|
|
console.error(`[cleanup] command failed: ${cmd}`);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
async function patchJson(path, updater, { dryRun }) {
|
|
const raw = await readFile(path, "utf8");
|
|
const parsed = JSON.parse(raw);
|
|
const next = updater(parsed);
|
|
if (JSON.stringify(parsed) === JSON.stringify(next)) {
|
|
log(`no change needed: ${path}`);
|
|
return;
|
|
}
|
|
log(`patching: ${path}`);
|
|
if (!dryRun) {
|
|
await writeFile(path, `${JSON.stringify(next, null, 2)}\n`, "utf8");
|
|
}
|
|
}
|
|
|
|
async function replaceInFile(path, replacements, { dryRun }) {
|
|
if (!existsSync(path)) return;
|
|
const original = await readFile(path, "utf8");
|
|
let next = original;
|
|
for (const [pattern, replacement] of replacements) {
|
|
next = next.replace(pattern, replacement);
|
|
}
|
|
if (next === original) return;
|
|
log(`rewriting: ${path}`);
|
|
if (!dryRun) {
|
|
await writeFile(path, next, "utf8");
|
|
}
|
|
}
|
|
|
|
async function main() {
|
|
const args = parseArgs(process.argv.slice(2));
|
|
if (args.help) {
|
|
console.log(
|
|
[
|
|
"Post-migration cleanup (Tauri → Electron).",
|
|
"",
|
|
"Runs in dry-run mode by default. Pass --execute to actually change files.",
|
|
].join("\n"),
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (args.dryRun) {
|
|
log("DRY RUN — no filesystem changes. Pass --execute to apply.");
|
|
}
|
|
|
|
// 1. Flip apps/desktop/package.json defaults to Electron.
|
|
const desktopPkgPath = resolve(repoRoot, "apps/desktop/package.json");
|
|
await patchJson(
|
|
desktopPkgPath,
|
|
(pkg) => {
|
|
const scripts = { ...(pkg.scripts ?? {}) };
|
|
scripts.dev = "node ./scripts/electron-dev.mjs";
|
|
scripts.build = "node ./scripts/electron-build.mjs";
|
|
scripts.package =
|
|
"pnpm run build && pnpm exec electron-builder --config electron-builder.yml";
|
|
scripts["dev:electron"] = undefined;
|
|
scripts["build:electron"] = undefined;
|
|
scripts["package:electron"] = undefined;
|
|
scripts["package:electron:dir"] =
|
|
"pnpm run build && pnpm exec electron-builder --config electron-builder.yml --dir";
|
|
scripts.electron = undefined;
|
|
scripts["dev:react-session"] = undefined;
|
|
scripts["dev:electron:react-session"] =
|
|
"VITE_OPENWORK_REACT_SESSION=1 node ./scripts/electron-dev.mjs";
|
|
// Remove Tauri-specific script entries.
|
|
scripts["build:debug:react-session"] = undefined;
|
|
scripts["dev:windows"] = undefined;
|
|
scripts["dev:windows:x64"] = undefined;
|
|
|
|
const filteredScripts = Object.fromEntries(
|
|
Object.entries(scripts).filter(([, v]) => v != null),
|
|
);
|
|
const devDeps = { ...(pkg.devDependencies ?? {}) };
|
|
delete devDeps["@tauri-apps/cli"];
|
|
return { ...pkg, scripts: filteredScripts, devDependencies: devDeps };
|
|
},
|
|
{ dryRun: args.dryRun },
|
|
);
|
|
|
|
// 2. Strip @tauri-apps/* from apps/app and apps/story-book package.json.
|
|
for (const pkgPath of [
|
|
resolve(repoRoot, "apps/app/package.json"),
|
|
resolve(repoRoot, "apps/story-book/package.json"),
|
|
]) {
|
|
if (!existsSync(pkgPath)) continue;
|
|
await patchJson(
|
|
pkgPath,
|
|
(pkg) => {
|
|
const deps = { ...(pkg.dependencies ?? {}) };
|
|
const devDeps = { ...(pkg.devDependencies ?? {}) };
|
|
for (const name of Object.keys(deps)) {
|
|
if (name.startsWith("@tauri-apps/")) delete deps[name];
|
|
}
|
|
for (const name of Object.keys(devDeps)) {
|
|
if (name.startsWith("@tauri-apps/")) delete devDeps[name];
|
|
}
|
|
return { ...pkg, dependencies: deps, devDependencies: devDeps };
|
|
},
|
|
{ dryRun: args.dryRun },
|
|
);
|
|
}
|
|
|
|
// 3. Delete src-tauri/ entirely.
|
|
run("git", ["rm", "-r", "-f", "apps/desktop/src-tauri"], { dryRun: args.dryRun });
|
|
|
|
// 4. Collapse desktop-tauri.ts into desktop.ts. We do a surgical rename
|
|
// for now and leave the collapse for a follow-up PR; deleting the
|
|
// proxy layer is a bigger refactor.
|
|
log(
|
|
"[reminder] apps/app/src/app/lib/desktop-tauri.ts still exists. After this script lands,",
|
|
);
|
|
log(
|
|
" open a follow-up PR that inlines desktop.ts's Electron path and removes the",
|
|
);
|
|
log(" proxy + re-export surface.");
|
|
|
|
// 5. Drop AGENTS.md / ARCHITECTURE.md / README.md Tauri references.
|
|
const docReplacements = [
|
|
[/Tauri 2\.x/g, "Electron"],
|
|
[/\| Desktop\/Mobile shell \| Tauri 2\.x\s+\|/g, "| Desktop/Mobile shell | Electron |"],
|
|
[/apps\/desktop\/src-tauri/g, "apps/desktop/electron"],
|
|
];
|
|
for (const path of [
|
|
resolve(repoRoot, "AGENTS.md"),
|
|
resolve(repoRoot, "ARCHITECTURE.md"),
|
|
resolve(repoRoot, "README.md"),
|
|
]) {
|
|
await replaceInFile(path, docReplacements, { dryRun: args.dryRun });
|
|
}
|
|
|
|
// 6. Remove the migration-release env fragment once it's no longer
|
|
// relevant.
|
|
run("git", ["rm", "-f", "apps/app/.env.migration-release"], {
|
|
dryRun: args.dryRun,
|
|
});
|
|
|
|
// 7. Stage + commit.
|
|
run("git", ["add", "-A"], { dryRun: args.dryRun });
|
|
run(
|
|
"git",
|
|
[
|
|
"commit",
|
|
"-m",
|
|
"chore(desktop): remove Tauri shell, make Electron the default\n\nRun after v0.12.x stabilized in the wild. See\nscripts/migration/README.md for the full runbook.",
|
|
],
|
|
{ dryRun: args.dryRun },
|
|
);
|
|
|
|
log("");
|
|
log(
|
|
args.dryRun
|
|
? "dry run complete. rerun with --execute to apply."
|
|
: "commit created. push + open PR for review.",
|
|
);
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error(err);
|
|
process.exit(1);
|
|
});
|