# Tauri → Electron auto-update cutover > Self-contained playbook. The branch and conversation where this plan was > designed will not be around when the cutover happens — read this file top to > bottom and execute; do not assume prior context. > Current status: the release workflow cutover is implemented. Desktop releases > now build Electron and repackage that Electron `.app` into the old Tauri > updater format for existing Tauri installs. The next safe engineering step is > [Step 5 — Remove Tauri-specific code](#step-5--remove-tauri-specific-code), > but only after the transition release has shipped and lived for at least 2 > weeks with no rollback. ## What this is OpenChamber historically shipped as a Tauri app. A parallel Electron shell was added on branch `electron-app` (merged to `main` as part of a larger migration). Since then, both desktop shells have been released in the same GitHub release and each has its own auto-update channel: | Shell | Manifest | Update format | Secret used to sign | |----------|-------------------|---------------------|---------------------| | Tauri | `latest.json` | `.tar.gz` + `.sig` | `TAURI_SIGNING_PRIVATE_KEY` (Tauri signer / minisign format) | | Electron | `latest-mac.yml` | `.zip` + `blockmap` | Developer ID codesign (APPLE_* secrets) | Existing Tauri installs keep their own auto-update path (`latest.json`). Electron installs auto-update through `latest-mac.yml`. They coexist without conflict because filenames and manifests differ. At some point the user wants to **stop maintaining the Tauri build** and make the Tauri installs migrate themselves into Electron via auto-update. This document describes how to do that in a single "transition release". ## The core trick Tauri's updater downloads whatever `.tar.gz` the `latest.json` points at, verifies the minisign signature, unpacks the contents **over** the existing `.app` directory, and restarts. It does **not** introspect the payload — it just replaces files. So: produce a `.tar.gz` of the Electron `.app`, sign it with the existing Tauri minisign key, point `latest.json` at it. Tauri users receive the update, their `OpenChamber.app` becomes the Electron bundle in-place, and next launch starts Electron. Subsequent updates go through `latest-mac.yml` (electron-updater). One-way migration, one-shot workflow change. ## Prerequisites before running the cutover Check all of these before making any release: 1. **Electron has shipped stable through its own `latest-mac.yml` path for at least 2 releases.** Verify: ``` gh release list --repo btriapitsyn/openchamber gh release view vX.Y.Z --repo btriapitsyn/openchamber \ | grep -E 'OpenChamber-.*\.zip|latest-mac\.yml' ``` A user on Electron should have successfully auto-updated at least once. If not, pause and stabilise that path first — don't stack risk. 2. **`~/.config/openchamber/settings.json` is still the shared state path.** Tauri `src-tauri/src/main.rs:settings_file_path` and Electron `packages/electron/main.mjs:settingsFilePath` must both resolve to `$HOME/.config/openchamber/settings.json`. If either has moved, data parity breaks and this migration loses user data. Audit both paths, update the non-migrated shell to match before proceeding. 3. **Electron `appId` is `dev.openchamber.desktop`** (check `packages/electron/package.json` `build.appId`). Tauri identifier is `ai.opencode.openchamber`. These differ intentionally — it means macOS LaunchServices will re-register after the in-place replace. That's fine but see "Risks" below. 4. **All GitHub secrets still valid:** `APPLE_CERTIFICATE`, `APPLE_CERTIFICATE_PASSWORD`, `APPLE_ID`, `APPLE_PASSWORD`, `APPLE_TEAM_ID`, `TAURI_SIGNING_PRIVATE_KEY`, `TAURI_SIGNING_PRIVATE_KEY_PASSWORD`. A workflow_dispatch dry-run should succeed before the real tag. 5. **`minisign` CLI is available on the macOS runner** (or installable via brew). Used to sign the Electron tarball with the Tauri key. ## Current release workflow The release workflow no longer builds a Tauri desktop app. It now does this: ```text create-release ├── build-desktop-electron-macos (Electron .dmg/.zip/blockmap/latest-mac.yml) ├── repackage-electron-as-tauri-update (Electron .app -> Tauri .app.tar.gz/.sig) ├── publish-npm ├── combine-manifests (Tauri latest.json for migration only) ├── combine-electron-manifests (Electron latest-mac.yml) └── finalize-release ``` The transition works like this: 1. `build-desktop-electron-macos` builds, signs, and notarizes the Electron app. 2. It wraps the signed `OpenChamber.app` in a tarball and uploads that tarball as a short-lived Actions artifact. 3. `repackage-electron-as-tauri-update` downloads that Electron `.app`. 4. It packs it into `OpenChamber--darwin-*.app.tar.gz`. 5. It signs that tarball with `tauri signer sign` and the existing Tauri signing key. 6. It uploads the tarball and `.sig` to the GitHub release. 7. It generates Tauri-compatible manifests and `combine-manifests` merges them into `latest.json`. So old Tauri installs still see the update contract they expect: ```text latest.json -> .app.tar.gz -> .sig ``` But the payload inside the `.app.tar.gz` is Electron, not Tauri. Tauri's updater only verifies the signature and extracts the bundle over the existing `/Applications/OpenChamber.app`. After restart, the app is Electron and future updates use `latest-mac.yml` through `electron-updater`. Note: do not upload the `.app` directory directly with `actions/upload-artifact`. That action can flatten the app to its inner `Contents/` folder and can also normalize file modes. Losing the executable bit on `Contents/MacOS/*` makes the updated app fail to launch with a permissions/package error. The workflow wraps the `.app` in a tarball before upload so permissions survive the handoff between jobs, then verifies the app executable is still executable before creating the Tauri updater tarball. ## Historical release workflow changes The file edited for the cutover was `.github/workflows/release.yml`. Before the cutover it had these jobs (simplified): ``` create-release ├── build-desktop-macos (Tauri .dmg/.tar.gz/.tar.gz.sig) ├── build-desktop-electron-macos (Electron .dmg/.zip/blockmap/latest-mac.yml) ├── publish-npm ├── combine-manifests (merges Tauri per-arch JSONs → latest.json) ├── combine-electron-manifests (merges Electron per-arch YMLs → latest-mac.yml) └── finalize-release ``` ### Step 1 — Remove the Tauri build Status: done. Delete these jobs entirely: - `build-desktop-macos` - `combine-manifests` They are replaced by the repackage job (below). `finalize-release` `needs:` list must be updated to drop both. ### Step 2 — Add a repackage job Status: done. Insert after `build-desktop-electron-macos`: ```yaml repackage-electron-as-tauri-update: needs: [create-release, build-desktop-electron-macos] runs-on: macos-26 strategy: fail-fast: false matrix: include: - arch: arm64 platform: darwin-aarch64 - arch: x64 platform: darwin-x86_64 steps: - uses: actions/checkout@v4 - uses: oven-sh/setup-bun@v2 - uses: actions/setup-node@v4 with: node-version: '20' # Pull the signed+notarized Electron .app that build-desktop-electron-macos # already produced. Either re-download the dmg and mount+copy the .app, or # (cleaner) modify build-desktop-electron-macos to upload the .app itself # as an artifact so this job can download it. Prefer the latter — adds one # `actions/upload-artifact@v4` step uploading `packages/electron/dist/mac-/OpenChamber.app`. - name: Download signed Electron .app uses: actions/download-artifact@v4 with: name: electron-app-${{ matrix.arch }} path: staged - name: Tar and sign Electron .app as Tauri update payload env: TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} VERSION: ${{ needs.create-release.outputs.version }} run: | set -euo pipefail cd staged # The tarball name convention Tauri's updater expects. Must end in # `.app.tar.gz`. Name stays stable — Tauri updater does not care about # the inner .app name. TARBALL="OpenChamber.app.tar.gz" tar -czf "$TARBALL" OpenChamber.app # Use Tauri's signer instead of minisign directly. The CI secret is in # the format consumed by TAURI_SIGNING_PRIVATE_KEY. bun run --cwd ../packages/desktop tauri signer sign "$PWD/$TARBALL" # Rename per platform so the release has distinct names for arm64/x64. mv "$TARBALL" "OpenChamber-${VERSION}-${{ matrix.platform }}.app.tar.gz" mv "${TARBALL}.sig" "OpenChamber-${VERSION}-${{ matrix.platform }}.app.tar.gz.sig" - name: Generate Tauri latest-.json env: VERSION: ${{ needs.create-release.outputs.version }} REPO: ${{ github.repository }} run: | SIG=$(cat staged/OpenChamber-${VERSION}-${{ matrix.platform }}.app.tar.gz.sig) TAR=OpenChamber-${VERSION}-${{ matrix.platform }}.app.tar.gz cat > staged/latest-${{ matrix.platform }}.json <