From 2585dd128b132f77a099d8f9ad88db68bb0cc52a Mon Sep 17 00:00:00 2001 From: Jokin Date: Tue, 14 Jul 2026 00:25:10 +0200 Subject: [PATCH] Multi-arch release script + Gitea workflow; retire legacy BunSCP name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit bun run release -- --target mac-arm64|mac-x64|win-x64: gen version/icons, build UI, electrobun build --env=stable, collect artifacts as release//miniSCP-Build-.*. mac-x64 builds on the arm64 Mac by running electrobun's x64 CLI under Rosetta 2 (verified: x86_64 launcher in the produced app). Windows also zips a portable build next to the Setup installer. Tag-triggered Gitea Actions matrix included, inert until runners exist. legacyNames emptied — the BunSCP migration has served its purpose. Co-Authored-By: Claude Fable 5 --- .gitea/workflows/release.yml | 46 +++++++++++++ README.md | 25 ++++++- scripts/release.ts | 128 +++++++++++++++++++++++++++++++++++ src/shared/branding.ts | 4 +- 4 files changed, 200 insertions(+), 3 deletions(-) create mode 100644 .gitea/workflows/release.yml create mode 100644 scripts/release.ts diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml new file mode 100644 index 0000000..ee9cf88 --- /dev/null +++ b/.gitea/workflows/release.yml @@ -0,0 +1,46 @@ +# Release matrix — INERT until act_runner instances with these labels are +# registered on git.teknes.es. Each OS builds its own artifacts (electrobun +# cannot cross-compile: packaging needs codesign/hdiutil on macOS and +# rcedit/PowerShell on Windows). +# +# Runner prerequisites: +# macos — arm64 Mac: bun, Xcode CLT, Rosetta 2 (softwareupdate +# --install-rosetta), network to github.com (electrobun downloads +# its per-arch core binaries on first build) +# windows — Windows 11 x64: bun, network to github.com; WebView2 runtime is +# preinstalled on Win 11 +# +# Trigger: push a tag like build-62 (git tag build-$(git rev-list --count HEAD)). +name: release +on: + push: + tags: + - "build-*" + +jobs: + mac: + runs-on: macos + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # full history — the build number is the commit count + - run: bun install --frozen-lockfile + - run: bun scripts/release.ts --target mac-arm64 + - run: bun scripts/release.ts --target mac-x64 + - uses: actions/upload-artifact@v3 + with: + name: mac + path: release/** + + windows: + runs-on: windows + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - run: bun install --frozen-lockfile + - run: bun scripts/release.ts --target win-x64 + - uses: actions/upload-artifact@v3 + with: + name: windows + path: release/** diff --git a/README.md b/README.md index 0e81e79..97484c1 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,29 @@ This starts the Angular dev server (hot reload for UI code) and launches the Electrobun app pointed at it. Edits under `ui/` reload instantly; edits to the Bun main process (`src/bun/**`, RPC schema) trigger an app relaunch. +## Releases + +Versioning is Sublime-style: **Build n**, where n is the commit count +(`git rev-list --count HEAD`), regenerated into the gitignored +`src/shared/version.gen.ts` by every build script (and `postinstall`). + +One artifact per invocation, on the matching host — electrobun cannot +cross-compile between OSes: + +```sh +bun run release -- --target mac-arm64 # on the Mac (native) +bun run release -- --target mac-x64 # on the same arm64 Mac, via Rosetta 2 +bun run release -- --target win-x64 # on a Windows 11 x64 machine +``` + +Artifacts land in `release//miniSCP-Build-.*` — a `.dmg` +per mac arch; on Windows a `-Setup.zip` installer **and** a `-portable.zip` +(unzip anywhere and run). The mac-x64 build fetches electrobun's x64 CLI once +into `~/.cache/miniscp/` and runs it under Rosetta so the whole pipeline +believes it is on an Intel Mac. CI: `.gitea/workflows/release.yml` runs the +same script on a macos + windows runner matrix once act_runners are +registered; trigger by pushing a `build-` tag. + ## Build a packaged app ```bash @@ -109,7 +132,7 @@ Everything user-facing reads from **[`src/shared/branding.ts`](src/shared/brandi export const BRANDING = { appName: "miniSCP", // window title, bundle name, app-data folder identifier: "es.teknes.miniscp", - legacyNames: ["BunSCP"], // previous names; their data dirs migrate on boot + legacyNames: [], // previous names; their data dirs migrate on boot version: "0.1.0", icon: "assets/icon.iconset", } as const; diff --git a/scripts/release.ts b/scripts/release.ts new file mode 100644 index 0000000..df9c650 --- /dev/null +++ b/scripts/release.ts @@ -0,0 +1,128 @@ +// Build one release artifact for one target, on the matching host: +// bun scripts/release.ts [--target mac-arm64|mac-x64|win-x64] +// Default target = the host. electrobun cannot cross-compile OSes (its +// packaging needs host tools: codesign/hdiutil on mac, rcedit/PowerShell on +// Windows), so win-x64 must run on a Windows machine. mac-x64 IS buildable on +// an arm64 Mac: we fetch electrobun's x64 CLI binary and run it under +// Rosetta 2 — it sees an x64 host, pulls the x64 core, and packages mac-x64. +// Artifacts land in release//miniSCP-Build-.. +import { cpSync, existsSync, mkdirSync, readdirSync } from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +const ROOT = path.join(import.meta.dir, ".."); +const TARGETS = ["mac-arm64", "mac-x64", "win-x64"] as const; +type Target = (typeof TARGETS)[number]; + +function hostTarget(): Target { + if (process.platform === "win32") return "win-x64"; + return process.arch === "arm64" ? "mac-arm64" : "mac-x64"; +} + +function fail(message: string): never { + console.error(`\n[release] ERROR: ${message}`); + process.exit(1); +} + +function run(cmd: string[], opts: { cwd?: string } = {}): void { + console.log(`[release] $ ${cmd.join(" ")}`); + const proc = Bun.spawnSync(cmd, { + cwd: opts.cwd ?? ROOT, + stdout: "inherit", + stderr: "inherit", + }); + if (!proc.success) fail(`command failed: ${cmd.join(" ")}`); +} + +// ---- target selection + host guards ---- + +const argIndex = process.argv.indexOf("--target"); +const requested = (argIndex >= 0 ? process.argv[argIndex + 1] : hostTarget()) as Target; +if (!TARGETS.includes(requested)) fail(`unknown target "${requested}" (${TARGETS.join(", ")})`); + +if (requested === "win-x64" && process.platform !== "win32") { + fail("win-x64 must be built on a Windows machine/runner — electrobun cannot cross-compile."); +} +if (requested.startsWith("mac") && process.platform !== "darwin") { + fail(`${requested} must be built on macOS.`); +} +const viaRosetta = requested === "mac-x64" && process.arch === "arm64"; + +// ---- prepare: version, icons, UI bundle ---- + +run(["bun", "scripts/gen-version.ts"]); +run(["bun", "scripts/gen-icons.ts"]); +run(["bun", "run", "--cwd", "ui", "build"]); + +const { BUILD } = await import("../src/shared/version.gen"); +const { BRANDING } = await import("../src/shared/branding"); +const buildName = `Build${BUILD.number}`; + +// ---- electrobun build (native, or x64 CLI under Rosetta) ---- + +async function x64Cli(): Promise { + const pkg = await Bun.file(path.join(ROOT, "node_modules", "electrobun", "package.json")).json(); + const dir = path.join(os.homedir(), ".cache", "miniscp", `electrobun-cli-x64-${pkg.version}`); + const bin = path.join(dir, "electrobun"); + if (existsSync(bin)) return bin; + console.log(`[release] downloading electrobun x64 CLI ${pkg.version} (one-time)…`); + mkdirSync(dir, { recursive: true }); + const url = `https://github.com/blackboardsh/electrobun/releases/download/v${pkg.version}/electrobun-cli-darwin-x64.tar.gz`; + const tarball = path.join(dir, "cli.tar.gz"); + run(["curl", "-fsSL", "-o", tarball, url]); + run(["tar", "-xzf", tarball, "-C", dir]); + if (!existsSync(bin)) fail(`x64 CLI tarball did not contain "electrobun" (${dir})`); + run(["chmod", "+x", bin]); + return bin; +} + +if (viaRosetta) { + // x64 Mach-O runs under Rosetta transparently; it sees process.arch === x64 + const rosetta = Bun.spawnSync(["arch", "-x86_64", "true"]); + if (!rosetta.success) { + fail("Rosetta 2 is not installed — run: softwareupdate --install-rosetta --agree-to-license"); + } + run([await x64Cli(), "build", "--env=stable"]); +} else { + run(["bunx", "electrobun", "build", "--env=stable"]); +} + +// ---- collect artifacts ---- + +const [osName, archName] = requested === "win-x64" ? ["win", "x64"] : ["macos", requested.slice(4)]; +const prefix = `stable-${osName}-${archName}-`; +const artifactsDir = path.join(ROOT, "artifacts"); +const outDir = path.join(ROOT, "release", requested); +mkdirSync(outDir, { recursive: true }); + +const produced = readdirSync(artifactsDir).filter( + (f) => f.startsWith(prefix) && f.includes(BRANDING.appName), +); +if (!produced.length) fail(`no ${prefix}* artifacts found in artifacts/ — did the build run?`); + +const out: string[] = []; +for (const file of produced) { + const ext = file.endsWith(".dmg") ? ".dmg" : file.endsWith("-Setup.zip") ? "-Setup.zip" : null; + if (!ext) continue; // skip the raw .app.tar.zst + update.json + const name = `${BRANDING.appName}-${buildName}-${requested}${ext}`; + cpSync(path.join(artifactsDir, file), path.join(outDir, name)); + out.push(name); +} + +// Windows: also produce a portable zip from the assembled app folder (the +// Setup.exe is an installer; portable = unzip anywhere and run). Best-effort: +// the folder layout is confirmed by the first real Windows build. +if (requested === "win-x64") { + const appDir = path.join(ROOT, "build", `stable-${osName}-${archName}`, BRANDING.appName); + if (existsSync(appDir)) { + const portable = path.join(outDir, `${BRANDING.appName}-${buildName}-win-x64-portable.zip`); + run(["powershell", "-Command", `Compress-Archive -Force -Path '${appDir}\\*' -DestinationPath '${portable}'`]); + out.push(path.basename(portable)); + } else { + console.warn(`[release] WARN: portable skipped — app folder not found at ${appDir}`); + } +} + +if (!out.length) fail("artifacts were found but none matched a known package extension."); +console.log(`\n[release] ${BRANDING.appName} ${BRANDING.buildLabel} (${BUILD.commit}) — ${requested}`); +for (const name of out) console.log(`[release] release/${requested}/${name}`); diff --git a/src/shared/branding.ts b/src/shared/branding.ts index 9be7137..bffe3e1 100644 --- a/src/shared/branding.ts +++ b/src/shared/branding.ts @@ -5,9 +5,9 @@ import { BUILD } from "./version.gen"; export const BRANDING = { appName: "miniSCP", identifier: "es.teknes.miniscp", - // Previous app names: their app-data folders are migrated on boot + // Previous app names whose app-data folders migrate on boot after a rename // (see migrateLegacyDataDir in src/bun/store/paths.ts). - legacyNames: ["BunSCP"], + legacyNames: [] as string[], // Sublime-style versioning: n = commit count (scripts/gen-version.ts). // `version` stays semver-shaped for electrobun's config/updater. version: `0.1.${BUILD.number}`,