源码快照 @ 2026-09-02 (source-only, 剔除非源码素材)
This commit is contained in:
+196
@@ -0,0 +1,196 @@
|
||||
#!/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);
|
||||
});
|
||||
Executable
+205
@@ -0,0 +1,205 @@
|
||||
#!/usr/bin/env node
|
||||
// Guided validation for a migration release. Mix of automated probes and
|
||||
// checklist prompts you acknowledge in-terminal. Designed to be run in a
|
||||
// release-review pair session.
|
||||
//
|
||||
// Usage:
|
||||
// node scripts/migration/02-validate-migration.mjs --tag v0.12.0
|
||||
// node scripts/migration/02-validate-migration.mjs --tag v0.12.0 --skip-manual
|
||||
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { createReadStream, existsSync } from "node:fs";
|
||||
import { mkdir, readFile, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { createInterface } from "node:readline/promises";
|
||||
import { stdin as input, stdout as output } from "node:process";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = resolve(__dirname, "../..");
|
||||
|
||||
function parseArgs(argv) {
|
||||
const out = { skipManual: false };
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const arg = argv[i];
|
||||
if (arg === "--skip-manual") out.skipManual = true;
|
||||
else if (arg === "--tag") out.tag = argv[++i];
|
||||
else if (arg === "--help" || arg === "-h") out.help = true;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function die(msg) {
|
||||
console.error(`[validate] ${msg}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
async function sha256(path) {
|
||||
const hash = createHash("sha256");
|
||||
return await new Promise((resolvePromise, rejectPromise) => {
|
||||
createReadStream(path)
|
||||
.on("data", (chunk) => hash.update(chunk))
|
||||
.on("end", () => resolvePromise(hash.digest("hex")))
|
||||
.on("error", rejectPromise);
|
||||
});
|
||||
}
|
||||
|
||||
async function confirm(prompt) {
|
||||
const rl = createInterface({ input, output });
|
||||
try {
|
||||
const answer = (await rl.question(`${prompt} [y/N] `)).trim().toLowerCase();
|
||||
return answer === "y" || answer === "yes";
|
||||
} finally {
|
||||
rl.close();
|
||||
}
|
||||
}
|
||||
|
||||
async function ghReleaseAssets(tag) {
|
||||
const result = spawnSync(
|
||||
"gh",
|
||||
[
|
||||
"release",
|
||||
"view",
|
||||
tag,
|
||||
"--repo",
|
||||
"different-ai/openwork",
|
||||
"--json",
|
||||
"assets",
|
||||
],
|
||||
{ encoding: "utf8" },
|
||||
);
|
||||
if (result.status !== 0) die(`gh release view ${tag} failed`);
|
||||
const parsed = JSON.parse(result.stdout);
|
||||
return parsed.assets ?? [];
|
||||
}
|
||||
|
||||
async function ghDownload(tag, assetName, targetDir) {
|
||||
await mkdir(targetDir, { recursive: true });
|
||||
const target = join(targetDir, assetName);
|
||||
const result = spawnSync(
|
||||
"gh",
|
||||
[
|
||||
"release",
|
||||
"download",
|
||||
tag,
|
||||
"--repo",
|
||||
"different-ai/openwork",
|
||||
"--pattern",
|
||||
assetName,
|
||||
"--dir",
|
||||
targetDir,
|
||||
"--clobber",
|
||||
],
|
||||
{ stdio: "inherit" },
|
||||
);
|
||||
if (result.status !== 0) die(`gh release download failed for ${assetName}`);
|
||||
return target;
|
||||
}
|
||||
|
||||
function codesignVerify(appPath) {
|
||||
const result = spawnSync(
|
||||
"codesign",
|
||||
["--verify", "--deep", "--strict", "--verbose=2", appPath],
|
||||
{ encoding: "utf8" },
|
||||
);
|
||||
return {
|
||||
ok: result.status === 0,
|
||||
message: (result.stderr ?? "").trim() || (result.stdout ?? "").trim(),
|
||||
};
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
if (args.help || !args.tag) {
|
||||
console.log("Usage: 02-validate-migration.mjs --tag vX.Y.Z [--skip-manual]");
|
||||
process.exit(args.help ? 0 : 2);
|
||||
}
|
||||
|
||||
const workDir = join(tmpdir(), `openwork-migrate-validate-${args.tag}`);
|
||||
await rm(workDir, { recursive: true, force: true });
|
||||
await mkdir(workDir, { recursive: true });
|
||||
|
||||
console.log(`[validate] scratch dir: ${workDir}`);
|
||||
|
||||
// 1. List assets on the release.
|
||||
const assets = await ghReleaseAssets(args.tag);
|
||||
const names = assets.map((a) => a.name);
|
||||
console.log(`[validate] release ${args.tag} has ${assets.length} assets`);
|
||||
names.forEach((n) => console.log(` - ${n}`));
|
||||
|
||||
const electronZip = names.find(
|
||||
(n) => /electron|\.zip$/i.test(n) && n.includes("mac"),
|
||||
) ?? names.find((n) => n.endsWith(".zip"));
|
||||
const electronYml = names.find((n) => n === "latest-mac.yml");
|
||||
const tauriLatestJson = names.find((n) => n === "latest.json");
|
||||
|
||||
const presenceChecks = [
|
||||
["electron macOS zip", electronZip],
|
||||
["electron-updater manifest (latest-mac.yml)", electronYml],
|
||||
["tauri minisign feed (latest.json)", tauriLatestJson],
|
||||
];
|
||||
console.log("");
|
||||
console.log("[validate] required release assets:");
|
||||
presenceChecks.forEach(([label, name]) => {
|
||||
console.log(` ${name ? "✓" : "✗"} ${label}${name ? ` — ${name}` : ""}`);
|
||||
});
|
||||
if (!electronZip) die("no Electron zip asset on the release.");
|
||||
|
||||
// 2. Download + Apple sig check on the Electron zip.
|
||||
console.log("");
|
||||
console.log("[validate] downloading Electron zip for signature check…");
|
||||
const zipPath = await ghDownload(args.tag, electronZip, workDir);
|
||||
console.log(`[validate] got ${zipPath} (${(await readFile(zipPath)).byteLength} bytes)`);
|
||||
const zipHash = await sha256(zipPath);
|
||||
console.log(`[validate] sha256 = ${zipHash}`);
|
||||
|
||||
const unzipDir = join(workDir, "unzipped");
|
||||
await mkdir(unzipDir, { recursive: true });
|
||||
const unzipResult = spawnSync("unzip", ["-q", "-o", zipPath, "-d", unzipDir], {
|
||||
stdio: "inherit",
|
||||
});
|
||||
if (unzipResult.status !== 0) die("unzip failed");
|
||||
const appPath = join(unzipDir, "OpenWork.app");
|
||||
if (!existsSync(appPath)) {
|
||||
console.log(`[validate] ✗ OpenWork.app not found inside zip (got: ${unzipDir})`);
|
||||
die("zip layout unexpected");
|
||||
}
|
||||
const sig = codesignVerify(appPath);
|
||||
console.log(`[validate] ${sig.ok ? "✓" : "✗"} codesign verify on ${appPath}`);
|
||||
if (!sig.ok) console.log(sig.message);
|
||||
|
||||
// 3. Manual steps.
|
||||
if (args.skipManual) {
|
||||
console.log("[validate] --skip-manual: stopping here.");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("");
|
||||
console.log("[validate] manual checklist — acknowledge each step.");
|
||||
const steps = [
|
||||
"Install a fresh Tauri v0.11.x build on a test machine (or VM).",
|
||||
`Launch it, "Check for updates" → migrates to ${args.tag}.`,
|
||||
"Restart, see the 'OpenWork is moving' modal, click 'Install now'.",
|
||||
"Confirm Electron app launches automatically after ~30s.",
|
||||
"Confirm sidebar shows every workspace that was visible in Tauri.",
|
||||
"Open one workspace; confirm last session is preselected.",
|
||||
"Settings → Advanced: both runtimes show 'Connected'.",
|
||||
"Reopen: confirm the migration modal does NOT reappear.",
|
||||
];
|
||||
for (const step of steps) {
|
||||
const ok = await confirm(` ☐ ${step}`);
|
||||
if (!ok) die(`manual step declined: ${step}`);
|
||||
}
|
||||
|
||||
console.log("");
|
||||
console.log("[validate] all checks passed. Migration release is healthy.");
|
||||
console.log("[validate] watch v0.12.0 telemetry for ~1-2 weeks before running");
|
||||
console.log(" node scripts/migration/03-post-migration-cleanup.mjs");
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
+225
@@ -0,0 +1,225 @@
|
||||
#!/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. Once Tauri is gone, make the sidecar helper's default output match the
|
||||
// Electron resource layout. Before cleanup, Tauri still uses the old
|
||||
// default and Electron passes --outdir explicitly.
|
||||
await replaceInFile(
|
||||
resolve(repoRoot, "apps/desktop/scripts/prepare-sidecar.mjs"),
|
||||
[
|
||||
[
|
||||
/join\(__dirname, "\.\.", "src-tauri", "sidecars"\)/,
|
||||
'join(__dirname, "..", "resources", "sidecars")',
|
||||
],
|
||||
],
|
||||
{ dryRun: args.dryRun },
|
||||
);
|
||||
|
||||
// 8. 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);
|
||||
});
|
||||
@@ -0,0 +1,146 @@
|
||||
# Tauri → Electron migration runbook
|
||||
|
||||
Three scripts. Run in order. Each one is idempotent and prints what it
|
||||
will do before it does it.
|
||||
|
||||
```
|
||||
scripts/migration/01-cut-migration-release.mjs # cut v0.12.0, the last Tauri release
|
||||
scripts/migration/02-validate-migration.mjs # guided post-release smoke test
|
||||
scripts/migration/03-post-migration-cleanup.mjs # delete src-tauri, flip defaults (run after users stabilize)
|
||||
```
|
||||
|
||||
## When to run what
|
||||
|
||||
| Step | When | What user-visible effect |
|
||||
| ---- | ---- | ------------------------ |
|
||||
| 01 | Ready to ship the migration | Tauri users see "OpenWork is moving" prompt on next update |
|
||||
| 02 | Right after the workflow finishes | Dogfood validation — no user effect |
|
||||
| 03 | After 1-2 weeks of stable Electron telemetry | Dev repo is Electron-only; no user effect |
|
||||
|
||||
## Current safe-prep rollout
|
||||
|
||||
The next release is intentionally **non-destructive**:
|
||||
|
||||
- Tauri remains the main/stable desktop build and keeps using the existing
|
||||
Tauri updater feed (`latest.json`).
|
||||
- The alpha macOS arm64 release now carries both notarized Tauri artifacts
|
||||
(`latest.json`) and notarized Electron artifacts (`latest-mac.yml`) on the
|
||||
rolling `alpha-macos-latest` release.
|
||||
- Electron is built as a preview artifact on every push by
|
||||
`.github/workflows/build-electron-desktop.yml`.
|
||||
- Pushes to `dev` or `main` refresh the rolling prerelease bucket at
|
||||
<https://github.com/different-ai/openwork/releases/tag/electron-preview-latest>.
|
||||
- The Debug settings migration controls are Tauri-only and developer-mode only.
|
||||
The default action, **Prepare migration data**, only writes
|
||||
`migration-snapshot.v1.json`; it does not quit, replace, or delete Tauri.
|
||||
- The install handoff requires a pasted Electron artifact URL plus two explicit
|
||||
confirmations. On macOS the native handoff keeps the previous bundle at
|
||||
`OpenWork.app.migrate-bak` for rollback.
|
||||
|
||||
Use this safe-prep release to test migration data capture and preview downloads
|
||||
before enabling any user-facing migration prompt.
|
||||
|
||||
### Sharing Electron preview downloads
|
||||
|
||||
1. Wait for `Build Electron Desktop Preview` to finish on the target commit.
|
||||
2. Share the rolling preview release page:
|
||||
<https://github.com/different-ai/openwork/releases/tag/electron-preview-latest>
|
||||
3. Ask testers to download the matching platform artifact:
|
||||
- macOS Apple Silicon: `openwork-mac-arm64-*.dmg` or `.zip`
|
||||
- macOS Intel: `openwork-mac-x64-*.dmg` or `.zip`
|
||||
- Windows: `openwork-win-x64-*.exe`
|
||||
- Linux: `openwork-linux-x64-*.AppImage` or `.tar.gz`
|
||||
|
||||
Do not point stable Tauri users at these preview assets as an automatic update
|
||||
until the explicit migration release is cut and validated.
|
||||
|
||||
## 01 — cut-migration-release.mjs
|
||||
|
||||
```bash
|
||||
node scripts/migration/01-cut-migration-release.mjs --version 0.12.0 \
|
||||
--mac-url 'https://github.com/different-ai/openwork/releases/download/v0.12.0/OpenWork-darwin-arm64-0.12.0-mac.zip' \
|
||||
--dry-run # inspect planned changes first
|
||||
```
|
||||
|
||||
What it does:
|
||||
|
||||
1. Refuses to run on a dirty working tree.
|
||||
2. Runs `pnpm bump:set -- <version>` (bumps all 5 sync files per
|
||||
AGENTS.md release runbook).
|
||||
3. Creates a release-config fragment at
|
||||
`apps/app/.env.migration-release` setting
|
||||
`VITE_OPENWORK_MIGRATION_RELEASE=1` and the per-platform download
|
||||
URLs. The `Release App` workflow copies this into the build env so
|
||||
the migration prompt is dormant on every other build but live for
|
||||
this release.
|
||||
4. Commits the version bump.
|
||||
5. Creates and pushes the `vX.Y.Z` tag.
|
||||
6. Prints `gh run watch` command so you can follow the workflow.
|
||||
|
||||
Drop `--dry-run` to actually execute.
|
||||
|
||||
## 02 — validate-migration.mjs
|
||||
|
||||
```bash
|
||||
node scripts/migration/02-validate-migration.mjs --tag v0.12.0
|
||||
```
|
||||
|
||||
Interactive guided check. Prints steps and confirms each one before
|
||||
moving on:
|
||||
|
||||
1. Downloads the Tauri DMG for the tag and verifies its minisign
|
||||
signature (reuses the existing updater public key).
|
||||
2. Downloads the Electron .zip for the tag and verifies the Apple
|
||||
Developer ID signature.
|
||||
3. Prompts you to install the Tauri DMG on a fresh machine / VM and
|
||||
confirm the migration prompt appears.
|
||||
4. Drops a canary key into localStorage on the Tauri side, triggers
|
||||
"Install now", and checks that the canary survives into the Electron
|
||||
launch via `readMigrationSnapshot` + localStorage inspection.
|
||||
5. Reports pass/fail for each step.
|
||||
|
||||
Needs `--skip-manual` to run the automated parts only. Useful inside a
|
||||
release-review meeting as a shared checklist.
|
||||
|
||||
## 03 — post-migration-cleanup.mjs
|
||||
|
||||
```bash
|
||||
node scripts/migration/03-post-migration-cleanup.mjs --dry-run
|
||||
```
|
||||
|
||||
Once v0.12.x has been stable for 1-2 weeks:
|
||||
|
||||
1. Flips `apps/desktop/package.json` defaults:
|
||||
- `dev` → `node ./scripts/electron-dev.mjs`
|
||||
- `build` → `node ./scripts/electron-build.mjs`
|
||||
- `package` → `pnpm run build && pnpm exec electron-builder …`
|
||||
2. Removes `apps/desktop/src-tauri/` entirely. Electron icons and generated
|
||||
sidecars already live under `apps/desktop/resources/`, so this step no
|
||||
longer removes anything the Electron packager needs.
|
||||
3. Strips `@tauri-apps/*` from `apps/app/package.json` and
|
||||
`apps/story-book/package.json`.
|
||||
4. Collapses `apps/app/src/app/lib/desktop-tauri.ts` into
|
||||
`desktop.ts` (direct Electron implementation).
|
||||
5. Updates `AGENTS.md`, `ARCHITECTURE.md`, `README.md`, translated
|
||||
READMEs to drop Tauri references.
|
||||
6. Updates the release runbook in `AGENTS.md` to remove the
|
||||
`Cargo.toml` and `tauri.conf.json` version-bump entries.
|
||||
7. Creates a commit with the combined cleanup.
|
||||
|
||||
Drop `--dry-run` to actually perform the changes.
|
||||
|
||||
## Emergency rollback
|
||||
|
||||
If the v0.12.0 migration release is bad:
|
||||
|
||||
- Users on Electron already: ship v0.12.1 via electron-updater. Same
|
||||
mechanism as any other update.
|
||||
- Users still on Tauri: the migrate prompt is gated on
|
||||
`VITE_OPENWORK_MIGRATION_RELEASE=1` at build time. Re-cut the
|
||||
release with that flag unset, minisign-sign a replacement
|
||||
`latest.json`, and users who haven't clicked "Install now" yet will
|
||||
fall back to the non-migrating release.
|
||||
- Users mid-migration: the Rust `migrate_to_electron` command keeps
|
||||
the previous `.app` at `OpenWork.app.migrate-bak`. Instruct users
|
||||
to `mv OpenWork.app.migrate-bak OpenWork.app` if the Electron
|
||||
launch fails.
|
||||
Reference in New Issue
Block a user