refactor: remove legacy Tauri desktop support

Electron updater now uses Electron release metadata only
Removed legacy Tauri package and migration workflow
Replaced Tauri shim usage with the desktop bridge
This commit is contained in:
Bohdan Triapitsyn
2026-06-03 02:42:00 +03:00
parent e80bd15dd9
commit c7bc026b4b
81 changed files with 269 additions and 16914 deletions
-414
View File
@@ -1,414 +0,0 @@
# 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-<version>-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-<arch>/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-<platform>.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 <<EOF
{
"version": "${VERSION}",
"notes": "OpenChamber has moved to Electron. This update replaces the Tauri shell with the Electron build. Subsequent updates will be delivered via the Electron auto-updater.",
"pub_date": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"platforms": {
"${{ matrix.platform }}": {
"signature": "${SIG}",
"url": "https://github.com/${REPO}/releases/download/v${VERSION}/${TAR}"
}
}
}
EOF
- name: Upload tarball + sig to release
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ needs.create-release.outputs.version }}
files: |
staged/*.app.tar.gz
staged/*.app.tar.gz.sig
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload per-platform manifest as artifact for merge
uses: actions/upload-artifact@v4
with:
name: tauri-manifest-${{ matrix.platform }}
path: staged/latest-${{ matrix.platform }}.json
retention-days: 1
```
### Step 3 — Re-add the `combine-manifests` job
Status: done.
Bring it back (it was deleted in Step 1) but sourcing artifacts from the
repackage job instead of the old Tauri build. The merging logic is identical
to what the old job did. Minimum job shape:
```yaml
combine-manifests:
needs: [create-release, repackage-electron-as-tauri-update]
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with:
pattern: tauri-manifest-*
path: artifacts
- name: Merge
run: |
# Copy the original merge logic from git history. It takes the two
# per-platform JSONs and produces a single `latest.json` with both
# platform entries. Upload as a release asset.
# Search git history: git log --all --diff-filter=D -- .github/workflows/release.yml
# Find the commit that deleted the old merge step and copy its shell block.
...
- uses: softprops/action-gh-release@v2
with:
tag_name: v${{ needs.create-release.outputs.version }}
files: artifacts/latest.json
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```
### Step 4 — Update `finalize-release.needs`
Status: done.
```yaml
finalize-release:
needs: [create-release, build-desktop-electron-macos, repackage-electron-as-tauri-update, publish-npm, combine-manifests, combine-electron-manifests]
```
### Step 5 — Remove Tauri-specific code
Status: next safe refactoring step, after the transition release ships and has
been out at least 2 weeks with no rollback.
Do not do this in the same release as the migration. Once the transition release
has proved stable, remove:
- `packages/desktop/` (entire package — Tauri Rust + UI glue)
- Any `isTauriShell()` branches that are now dead code in
`packages/ui/src/` (search for the symbol; most call sites already fall
through to the Electron path because our preload exposes a `__TAURI__` shim;
audit each before removing).
- This file (`docs/TAURI_TO_ELECTRON_CUTOVER.md`) — mission accomplished.
Do this in a separate PR. Keep the transition release workflow intact until the
cleanup lands; rolling the cleanup into the transition release itself makes
debugging much harder if the migration misbehaves for a user.
The manual arm64 macOS DMG workflow has already been changed to build Electron
only, so there should be no GitHub Actions path that accidentally produces a new
Tauri DMG.
When rerunning the same release version with `workflow_dispatch`, use
`dry_run=true` if npm and marketplace packages are already published. In that
mode the workflow still rebuilds release assets, but skips publishing to npm and
skips re-uploading the npm tarball asset.
## Validation before tagging the transition release
You must manually validate with a real Tauri install. Do NOT skip this.
1. Have the previous Tauri release installed locally
(`/Applications/OpenChamber.app` with `Contents/Info.plist` showing
`CFBundleIdentifier = ai.opencode.openchamber`).
2. Tag the transition release to a test tag
(e.g. `v2.0.0-migration-test`) and push.
3. Let the workflow complete. Do not merge cleanup PR yet.
4. In the running Tauri app, use the built-in "Check for updates".
5. Accept the update. The app should download, verify, extract, restart.
6. After restart, `Info.plist` under `/Applications/OpenChamber.app/` should
now show `CFBundleIdentifier = dev.openchamber.desktop`.
7. Settings should be intact: hosts list, default host, sessions history.
8. In the new Electron app, "Check for updates" should report no update
available (it's now at the transition version, which is the latest).
9. Produce a dummy v2.0.1 Electron-only release to prove the subsequent
Electron-path update works. Accept it. App relaunches into v2.0.1.
If any step fails:
- Delete the test tag and GitHub release.
- Do not delete yet-shipped artifacts from a real tag until rollback below.
## Rollback if the transition release misbehaves
If users report the Tauri → Electron update bricks their install:
1. **Immediately** delete the latest release asset
`OpenChamber-*.app.tar.gz` and `latest.json` from the GitHub release
(keep the DMGs so manual download still works).
2. Re-upload the previous version's `latest.json` as the current latest so
Tauri updaters see "up to date" instead of a broken update on next check.
3. Post a support note: users who already applied the broken update can
download a fresh Electron `.dmg` manually and drag-replace. Their
`~/.config/openchamber/settings.json` survives.
4. Investigate, fix the workflow, retry with a new version number.
## Risks & edge cases
### Different `CFBundleIdentifier` at same path
macOS LaunchServices caches identifier ↔ path mappings. When we replace
`ai.opencode.openchamber` with `dev.openchamber.desktop` at the same `.app`
path, LaunchServices will rebuild on next launch (automatic). Usually fine.
If a user's system is in a weird state, a `killall Dock` or logout/login
fixes it. Worth noting in the release notes.
### macOS notification permissions
Notification permission is per-bundle-id. After migration, the app has a new
bundle-id, so the first notification will re-prompt the user. Unavoidable.
Mention in release notes.
### Deep-link protocol registration
The `openchamber://` protocol was registered for `ai.opencode.openchamber`.
After migration, `dev.openchamber.desktop` registers itself on first launch.
LaunchServices updates the handler. Usually seamless. Test with
`open 'openchamber://session/test'` post-migration.
### Gatekeeper "damaged app" dialog
Rare. Triggered if the replaced `.app` fails a mid-extract codesign check.
Can happen if Tauri's extractor corrupts xattrs. Mitigation: test on a
pristine macOS install before tagging production.
### Users on unsupported old Tauri versions
If a user is on a very old Tauri build that doesn't know how to do the
fetch-verify-extract flow, they're stuck. Expected: negligibly few users;
they'll just stay on their old version forever until they manually download.
Acceptable.
### Rollback-after-migration-accepted is impossible per-user
Once a user is on Electron, the Tauri updater is gone. If they want to go
back to a Tauri build, they must manually download. We don't support this.
## Relevant files to understand before making changes
- `.github/workflows/release.yml` — the release workflow.
- `packages/electron/package.json` — electron-builder config (appId,
mac/dmg, publish, artifactName).
- `packages/electron/main.mjs` — autoUpdater setup (`setupAutoUpdater`,
`desktop_check_for_updates`, `desktop_download_and_install_update`,
`desktop_restart`). Understand this flow before touching the CI.
- `packages/electron/scripts/finalize-latest-yml.mjs` — per-arch
`latest-mac.yml` merger. Already wired in `combine-electron-manifests`.
- `packages/desktop/src-tauri/tauri.conf.json` — legacy Tauri identifier,
minisign pubkey embedded for updater verification. Don't modify; just
reference for context.
## Working protocol
Default to a dry-run (test tag like `vX.Y.Z-migration-test` on a workflow_dispatch
run) before the real tag. Surface only business-level decisions —
"cutover this release, or hold one more cycle?" — and make technical calls
(minisign invocation flags, YAML layout, job dependency order) yourself,
documenting each one in the PR description.