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:
@@ -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.
|
||||
Reference in New Issue
Block a user