diff --git a/.github/workflows/build-macos-arm64-dmg.yml b/.github/workflows/build-macos-arm64-dmg.yml
index 71aeaa04..6018093d 100644
--- a/.github/workflows/build-macos-arm64-dmg.yml
+++ b/.github/workflows/build-macos-arm64-dmg.yml
@@ -1,4 +1,4 @@
-name: Build macOS DMG (arm64)
+name: Build Electron macOS DMG (arm64)
on:
workflow_dispatch:
@@ -16,114 +16,7 @@ on:
required: false
default: ""
-env:
- CARGO_INCREMENTAL: 0
- RUST_BACKTRACE: short
-
jobs:
- build-macos-dmg-arm64:
- name: Build DMG (arm64, ${{ inputs.macos_version }})
- runs-on: ${{ inputs.macos_version }}
- steps:
- - name: Checkout
- uses: actions/checkout@v4
- with:
- ref: ${{ inputs.ref || github.ref }}
-
- - name: Setup bun
- uses: oven-sh/setup-bun@v2
-
- - name: Setup Node.js
- uses: actions/setup-node@v4
- with:
- node-version: "20"
-
- - name: Install Rust stable
- uses: dtolnay/rust-toolchain@stable
- with:
- targets: aarch64-apple-darwin
-
- - name: Rust cache
- uses: swatinem/rust-cache@v2
- with:
- workspaces: packages/desktop/src-tauri
- key: aarch64-apple-darwin
-
- - name: Install dependencies
- run: bun install --frozen-lockfile
-
- - name: Install Apple Certificate
- env:
- APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
- APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
- run: |
- KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
- KEYCHAIN_PASSWORD=$(openssl rand -base64 32)
-
- security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
- security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
- security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
-
- echo "$APPLE_CERTIFICATE" | base64 --decode > $RUNNER_TEMP/certificate.p12
- security import $RUNNER_TEMP/certificate.p12 \
- -P "$APPLE_CERTIFICATE_PASSWORD" \
- -A -t cert -f pkcs12 \
- -k "$KEYCHAIN_PATH"
-
- security list-keychain -d user -s "$KEYCHAIN_PATH"
- security set-key-partition-list -S apple-tool:,apple:,codesign: \
- -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
-
- - name: Set up notarization credentials
- env:
- APPLE_ID: ${{ secrets.APPLE_ID }}
- APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
- APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
- run: |
- if [ -z "$APPLE_ID" ] || [ -z "$APPLE_TEAM_ID" ] || [ -z "$APPLE_PASSWORD" ]; then
- echo "Error: Missing Apple notarization credentials"
- exit 1
- fi
-
- xcrun notarytool store-credentials "openchamber-notarize" \
- --apple-id "$APPLE_ID" \
- --team-id "$APPLE_TEAM_ID" \
- --password "$APPLE_PASSWORD"
-
- - name: Build UI package
- run: bun run --cwd packages/ui build
-
- - name: Build Desktop app (arm64)
- run: bun run --cwd packages/desktop build && bun run --cwd packages/desktop tauri build --target aarch64-apple-darwin
- env:
- TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
- TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
- APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
- APPLE_ID: ${{ secrets.APPLE_ID }}
- APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
- APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
-
- - name: Prepare DMG artifact
- run: |
- set -euo pipefail
- mkdir -p artifacts
- DMG_PATH="packages/desktop/src-tauri/target/aarch64-apple-darwin/release/bundle/dmg/*.dmg"
- if ls $DMG_PATH 1> /dev/null 2>&1; then
- DMG_FILE=$(ls $DMG_PATH | head -n 1)
- DMG_NAME="OpenChamber_${{ inputs.macos_version }}_arm64.dmg"
- cp "$DMG_FILE" "artifacts/$DMG_NAME"
- else
- echo "Error: DMG file not found at $DMG_PATH"
- exit 1
- fi
-
- - name: Upload DMG artifact
- uses: actions/upload-artifact@v4
- with:
- name: dmg-${{ inputs.macos_version }}-arm64
- path: artifacts/*.dmg
- retention-days: 7
-
build-macos-dmg-arm64-electron:
name: Build Electron DMG (arm64, ${{ inputs.macos_version }})
runs-on: ${{ inputs.macos_version }}
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index b1865b18..3b7b8cd4 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -78,299 +78,6 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- build-desktop-macos:
- needs: create-release
- runs-on: macos-26
- strategy:
- fail-fast: false
- matrix:
- target: [aarch64-apple-darwin, x86_64-apple-darwin]
- include:
- - target: aarch64-apple-darwin
- arch: aarch64
- platform: darwin-aarch64
- - target: x86_64-apple-darwin
- arch: x86_64
- platform: darwin-x86_64
- outputs:
- version: ${{ needs.create-release.outputs.version }}
- steps:
- - uses: actions/checkout@v4
-
- - name: Setup bun
- uses: oven-sh/setup-bun@v2
-
- - name: Setup Node.js
- uses: actions/setup-node@v4
- with:
- node-version: '20'
-
- - name: Install Rust stable
- uses: dtolnay/rust-toolchain@stable
- with:
- targets: ${{ matrix.target }}
-
- - name: Rust cache
- uses: swatinem/rust-cache@v2
- with:
- workspaces: packages/desktop/src-tauri
- key: ${{ matrix.target }}
-
- - name: Install dependencies
- run: bun install --frozen-lockfile
-
- - name: Install Apple Certificate
- env:
- APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
- APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
- run: |
- # Create temporary keychain
- KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
- KEYCHAIN_PASSWORD=$(openssl rand -base64 32)
-
- security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
- security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
- security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
-
- # Import certificate
- echo "$APPLE_CERTIFICATE" | base64 --decode > $RUNNER_TEMP/certificate.p12
- security import $RUNNER_TEMP/certificate.p12 \
- -P "$APPLE_CERTIFICATE_PASSWORD" \
- -A -t cert -f pkcs12 \
- -k "$KEYCHAIN_PATH"
-
- security list-keychain -d user -s "$KEYCHAIN_PATH"
- security set-key-partition-list -S apple-tool:,apple:,codesign: \
- -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
-
- - name: Set up notarization credentials
- env:
- APPLE_ID: ${{ secrets.APPLE_ID }}
- APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
- APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
- run: |
- # Validate secrets are set
- if [ -z "$APPLE_ID" ] || [ -z "$APPLE_TEAM_ID" ] || [ -z "$APPLE_PASSWORD" ]; then
- echo "Error: Missing Apple notarization credentials"
- exit 1
- fi
-
- xcrun notarytool store-credentials "openchamber-notarize" \
- --apple-id "$APPLE_ID" \
- --team-id "$APPLE_TEAM_ID" \
- --password "$APPLE_PASSWORD"
-
- - name: Build UI package
- run: bun run --cwd packages/ui build
-
- - name: Build Desktop app
- # Note: We use inline commands instead of desktop:build to pass architecture-specific --target flag
- # This enables cross-compilation for both arm64 and x86_64 from the same runner
- run: |
- export TAURI_ENV_TARGET_TRIPLE=${{ matrix.target }}
- bun run --cwd packages/desktop build
- bun run --cwd packages/desktop tauri build --target ${{ matrix.target }}
- env:
- TAURI_ENV_TARGET_TRIPLE: ${{ matrix.target }}
- TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
- TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
- APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
- APPLE_ID: ${{ secrets.APPLE_ID }}
- APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
- APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
-
- - name: Verify binary architectures
- run: |
- set -euo pipefail
-
- BUNDLE_DIR="packages/desktop/src-tauri/target/${{ matrix.target }}/release/bundle/macos"
-
- if [ ! -d "$BUNDLE_DIR" ]; then
- echo "❌ Error: bundle directory not found: $BUNDLE_DIR"
- exit 1
- fi
-
- APP_PATH=$(find "$BUNDLE_DIR" -maxdepth 2 -name "*.app" -print -quit)
- if [ -z "$APP_PATH" ]; then
- echo "❌ Error: .app bundle not found under $BUNDLE_DIR"
- exit 1
- fi
-
- echo "🔍 Verifying binary architectures in $APP_PATH"
-
- # Extract raw architecture names (macOS file command reports ARM as "arm64")
- MAIN_ARCH_RAW=$(file "$APP_PATH/Contents/MacOS/openchamber-desktop" | grep -oE 'arm64|x86_64|aarch64' | head -1)
- SIDEARCH_ARCH_RAW=$(file "$APP_PATH/Contents/MacOS/openchamber-server" | grep -oE 'arm64|x86_64|aarch64' | head -1)
-
- # Normalize architecture names (arm64 -> aarch64 for consistency with Rust/Tauri)
- normalize_arch() {
- case "$1" in
- arm64) echo "aarch64" ;;
- aarch64|x86_64) echo "$1" ;;
- *) echo "unknown" ;;
- esac
- }
-
- MAIN_ARCH=$(normalize_arch "$MAIN_ARCH_RAW")
- SIDEARCH_ARCH=$(normalize_arch "$SIDEARCH_ARCH_RAW")
- EXPECTED_ARCH=$(echo "${{ matrix.target }}" | grep -oE 'aarch64|x86_64' | head -1)
-
- echo " Main: $MAIN_ARCH_RAW → $MAIN_ARCH"
- echo " Sidecar: $SIDEARCH_ARCH_RAW → $SIDEARCH_ARCH"
- echo " Expected: $EXPECTED_ARCH"
-
- if [ "$MAIN_ARCH" != "$EXPECTED_ARCH" ]; then
- echo "❌ ERROR: Main binary architecture mismatch!"
- echo " Expected: $EXPECTED_ARCH"
- echo " Got: $MAIN_ARCH (raw: $MAIN_ARCH_RAW)"
- exit 1
- fi
-
- if [ "$SIDEARCH_ARCH" != "$EXPECTED_ARCH" ]; then
- echo "❌ ERROR: Sidecar binary architecture mismatch!"
- echo " Expected: $EXPECTED_ARCH"
- echo " Got: $SIDEARCH_ARCH (raw: $SIDEARCH_ARCH_RAW)"
- exit 1
- fi
-
- echo "✅ Architecture verification passed: both binaries match $EXPECTED_ARCH"
-
- - name: Verify macOS entitlements
- run: |
- set -euo pipefail
-
- BUNDLE_DIR="packages/desktop/src-tauri/target/${{ matrix.target }}/release/bundle/macos"
-
- if [ ! -d "$BUNDLE_DIR" ]; then
- echo "Error: bundle directory not found: $BUNDLE_DIR"
- exit 1
- fi
-
- APP_PATH=$(find "$BUNDLE_DIR" -maxdepth 2 -name "*.app" -print -quit)
- if [ -z "$APP_PATH" ]; then
- echo "Error: .app bundle not found under $BUNDLE_DIR"
- echo "Contents:"; ls -la "$BUNDLE_DIR"
- exit 1
- fi
-
- echo "Verifying app bundle: $APP_PATH"
- codesign -vv "$APP_PATH"
-
- ENTITLEMENTS=$(codesign -d --entitlements :- "$APP_PATH" 2>&1 || true)
- echo "$ENTITLEMENTS"
-
- if echo "$ENTITLEMENTS" | grep -q "com.apple.security.app-sandbox"; then
- echo "Error: app sandbox entitlement is present"
- exit 1
- fi
-
- for key in \
- com.apple.security.cs.allow-jit \
- com.apple.security.cs.allow-unsigned-executable-memory \
- com.apple.security.cs.disable-executable-page-protection \
- com.apple.security.cs.disable-library-validation
- do
- if ! echo "$ENTITLEMENTS" | grep -q "$key"; then
- echo "Error: required entitlement missing: $key"
- exit 1
- fi
- done
-
- - name: Prepare release artifacts
- run: |
- mkdir -p artifacts
- VERSION="${{ needs.create-release.outputs.version }}"
-
- # Copy DMG (Tauri names it with the target triple in the path)
- DMG_PATH="packages/desktop/src-tauri/target/${{ matrix.target }}/release/bundle/dmg/*.dmg"
- if ls $DMG_PATH 1> /dev/null 2>&1; then
- DMG_FILE=$(ls $DMG_PATH | head -n 1)
- DMG_NAME="OpenChamber_${VERSION}_${{ matrix.platform }}.dmg"
- cp "$DMG_FILE" "artifacts/$DMG_NAME"
- else
- echo "Error: DMG file not found at $DMG_PATH"
- exit 1
- fi
-
- # Copy tar.gz and signature for updater
- TAR_PATH="packages/desktop/src-tauri/target/${{ matrix.target }}/release/bundle/macos/*.tar.gz"
- SIG_PATH="packages/desktop/src-tauri/target/${{ matrix.target }}/release/bundle/macos/*.tar.gz.sig"
-
- if ls $TAR_PATH 1> /dev/null 2>&1; then
- TAR_FILE=$(ls $TAR_PATH | head -n 1)
- TAR_BASE=$(basename "$TAR_FILE")
- TAR_NAME="${TAR_BASE%.tar.gz}-${{ matrix.platform }}.tar.gz"
- cp "$TAR_FILE" "artifacts/$TAR_NAME"
- else
- echo "Error: tar.gz file not found at $TAR_PATH"
- exit 1
- fi
-
- if ls $SIG_PATH 1> /dev/null 2>&1; then
- SIG_FILE=$(ls $SIG_PATH | head -n 1)
- SIG_BASE=$(basename "$SIG_FILE")
- SIG_NAME="${SIG_BASE%.tar.gz.sig}-${{ matrix.platform }}.tar.gz.sig"
- cp "$SIG_FILE" "artifacts/$SIG_NAME"
- else
- echo "Error: signature file not found at $SIG_PATH"
- exit 1
- fi
-
- echo "Successfully prepared artifacts:"
- ls -lh artifacts/
-
- - name: Generate update manifest
- run: |
- VERSION="${{ needs.create-release.outputs.version }}"
-
- # Find the signature file for this platform
- SIG_FILE=$(find artifacts -name "*-${{ matrix.platform }}.tar.gz.sig" | head -1)
- if [ -f "$SIG_FILE" ]; then
- SIGNATURE=$(cat "$SIG_FILE")
- else
- SIGNATURE=""
- fi
-
- # Find the tar.gz file name for this platform
- TAR_FILE=$(find artifacts -name "*-${{ matrix.platform }}.tar.gz" ! -name "*.sig" | head -1)
- TAR_NAME=$(basename "$TAR_FILE" 2>/dev/null || echo "OpenChamber-${{ matrix.platform }}.app.tar.gz")
-
- cat > artifacts/latest-${{ matrix.platform }}.json << EOF
- {
- "version": "${VERSION}",
- "notes": "See release notes at https://github.com/${{ github.repository }}/releases/tag/v${VERSION}",
- "pub_date": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
- "platforms": {
- "${{ matrix.platform }}": {
- "signature": "${SIGNATURE}",
- "url": "https://github.com/${{ github.repository }}/releases/download/v${VERSION}/${TAR_NAME}"
- }
- }
- }
- EOF
-
- echo "Generated latest-${{ matrix.platform }}.json:"
- cat artifacts/latest-${{ matrix.platform }}.json
-
- - name: Upload release assets
- uses: softprops/action-gh-release@v2
- with:
- tag_name: v${{ needs.create-release.outputs.version }}
- files: |
- artifacts/*.dmg
- artifacts/*.tar.gz
- artifacts/*.tar.gz.sig
- artifacts/latest-${{ matrix.platform }}.json
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-
- - name: Upload manifest as artifact
- uses: actions/upload-artifact@v4
- with:
- name: manifest-${{ matrix.platform }}
- path: artifacts/latest-${{ matrix.platform }}.json
- retention-days: 1
-
publish-npm:
needs: create-release
runs-on: ubuntu-latest
@@ -411,89 +118,6 @@ jobs:
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- combine-manifests:
- needs: [create-release, build-desktop-macos]
- runs-on: ubuntu-latest
- steps:
- - name: Download aarch64 manifest
- uses: actions/download-artifact@v4
- with:
- name: manifest-darwin-aarch64
- path: artifacts
-
- - name: Download x86_64 manifest
- uses: actions/download-artifact@v4
- with:
- name: manifest-darwin-x86_64
- path: artifacts
-
- - name: Combine manifests
- run: |
- VERSION="${{ needs.create-release.outputs.version }}"
- PUB_DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
- REPO="${{ github.repository }}"
-
- # Validate that both manifest files exist and are valid JSON
- if [ ! -f artifacts/latest-darwin-aarch64.json ]; then
- echo "Error: aarch64 manifest not found"
- exit 1
- fi
-
- if [ ! -f artifacts/latest-darwin-x86_64.json ]; then
- echo "Error: x86_64 manifest not found"
- exit 1
- fi
-
- # Validate JSON structure
- if ! jq empty artifacts/latest-darwin-aarch64.json 2>/dev/null; then
- echo "Error: aarch64 manifest is not valid JSON"
- exit 1
- fi
-
- if ! jq empty artifacts/latest-darwin-x86_64.json 2>/dev/null; then
- echo "Error: x86_64 manifest is not valid JSON"
- exit 1
- fi
-
- # Validate platform data exists in manifests
- if ! jq -e '.platforms["darwin-aarch64"]' artifacts/latest-darwin-aarch64.json > /dev/null; then
- echo "Error: darwin-aarch64 platform data not found in manifest"
- exit 1
- fi
-
- if ! jq -e '.platforms["darwin-x86_64"]' artifacts/latest-darwin-x86_64.json > /dev/null; then
- echo "Error: darwin-x86_64 platform data not found in manifest"
- exit 1
- fi
-
- # Use jq to properly combine the manifests
- jq -n \
- --arg version "$VERSION" \
- --arg notes "See release notes at https://github.com/${REPO}/releases/tag/v${VERSION}" \
- --arg pub_date "$PUB_DATE" \
- --slurpfile aarch64 artifacts/latest-darwin-aarch64.json \
- --slurpfile x86_64 artifacts/latest-darwin-x86_64.json \
- '{
- version: $version,
- notes: $notes,
- pub_date: $pub_date,
- platforms: {
- "darwin-aarch64": $aarch64[0].platforms["darwin-aarch64"],
- "darwin-x86_64": $x86_64[0].platforms["darwin-x86_64"]
- }
- }' > artifacts/latest.json
-
- echo "Generated combined latest.json:"
- cat artifacts/latest.json
-
- - name: Upload combined manifest
- 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 }}
-
build-desktop-electron-macos:
needs: create-release
runs-on: macos-26
@@ -617,6 +241,31 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ - name: Stage signed Electron app for Tauri updater repackage
+ run: |
+ set -euo pipefail
+
+ APP_DIR="packages/electron/dist/mac"
+ [ -d "packages/electron/dist/mac-arm64" ] && APP_DIR="packages/electron/dist/mac-arm64"
+
+ APP_PATH=$(find "$APP_DIR" -maxdepth 2 -name "*.app" -print -quit)
+ if [ -z "$APP_PATH" ]; then
+ echo "Error: .app not found under packages/electron/dist/mac*"
+ ls -la packages/electron/dist/
+ exit 1
+ fi
+
+ rm -rf electron-app-artifact
+ mkdir -p electron-app-artifact
+ cp -R "$APP_PATH" electron-app-artifact/OpenChamber.app
+
+ - name: Upload signed Electron app for Tauri updater repackage
+ uses: actions/upload-artifact@v4
+ with:
+ name: electron-app-${{ matrix.arch }}
+ path: electron-app-artifact/OpenChamber.app
+ retention-days: 1
+
- name: Upload per-arch latest-mac.yml for merge
uses: actions/upload-artifact@v4
with:
@@ -624,6 +273,163 @@ jobs:
path: packages/electron/dist/latest-mac.yml
retention-days: 1
+ 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
+
+ - name: Download signed Electron app
+ uses: actions/download-artifact@v4
+ with:
+ name: electron-app-${{ matrix.arch }}
+ path: staged
+
+ - name: Install minisign
+ run: brew install minisign
+
+ - name: Tar and sign Electron app as Tauri update payload
+ env:
+ TAURI_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
+ TAURI_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
+ VERSION: ${{ needs.create-release.outputs.version }}
+ run: |
+ set -euo pipefail
+
+ if [ ! -d staged/OpenChamber.app ]; then
+ echo "Error: staged/OpenChamber.app not found"
+ ls -la staged
+ exit 1
+ fi
+
+ cd staged
+ TARBALL="OpenChamber.app.tar.gz"
+ tar -czf "$TARBALL" OpenChamber.app
+
+ printf '%s\n' "$TAURI_KEY" > ../tauri-signing.key
+ printf '%s\n' "$TAURI_KEY_PASSWORD" | minisign -S -s ../tauri-signing.key -m "$TARBALL" -W
+
+ mv "$TARBALL" "OpenChamber-${VERSION}-${{ matrix.platform }}.app.tar.gz"
+ mv "${TARBALL}.minisig" "OpenChamber-${VERSION}-${{ matrix.platform }}.app.tar.gz.sig"
+
+ - name: Generate Tauri latest platform manifest
+ env:
+ VERSION: ${{ needs.create-release.outputs.version }}
+ REPO: ${{ github.repository }}
+ run: |
+ set -euo pipefail
+
+ SIG=$(cat staged/OpenChamber-${VERSION}-${{ matrix.platform }}.app.tar.gz.sig)
+ TAR="OpenChamber-${VERSION}-${{ matrix.platform }}.app.tar.gz"
+ jq -n \
+ --arg version "$VERSION" \
+ --arg 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." \
+ --arg pub_date "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
+ --arg platform "${{ matrix.platform }}" \
+ --arg signature "$SIG" \
+ --arg url "https://github.com/${REPO}/releases/download/v${VERSION}/${TAR}" \
+ '{ version: $version, notes: $notes, pub_date: $pub_date, platforms: { ($platform): { signature: $signature, url: $url } } }' \
+ > staged/latest-${{ matrix.platform }}.json
+
+ - name: Upload tarball and signature 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 Tauri manifest for merge
+ uses: actions/upload-artifact@v4
+ with:
+ name: tauri-manifest-${{ matrix.platform }}
+ path: staged/latest-${{ matrix.platform }}.json
+ retention-days: 1
+
+ combine-manifests:
+ needs: [create-release, repackage-electron-as-tauri-update]
+ runs-on: ubuntu-latest
+ steps:
+ - name: Download Tauri updater manifests
+ uses: actions/download-artifact@v4
+ with:
+ pattern: tauri-manifest-*
+ path: artifacts
+ merge-multiple: true
+
+ - name: Combine manifests
+ run: |
+ set -euo pipefail
+
+ VERSION="${{ needs.create-release.outputs.version }}"
+ PUB_DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
+
+ if [ ! -f artifacts/latest-darwin-aarch64.json ]; then
+ echo "Error: aarch64 manifest not found"
+ exit 1
+ fi
+
+ if [ ! -f artifacts/latest-darwin-x86_64.json ]; then
+ echo "Error: x86_64 manifest not found"
+ exit 1
+ fi
+
+ if ! jq empty artifacts/latest-darwin-aarch64.json 2>/dev/null; then
+ echo "Error: aarch64 manifest is not valid JSON"
+ exit 1
+ fi
+
+ if ! jq empty artifacts/latest-darwin-x86_64.json 2>/dev/null; then
+ echo "Error: x86_64 manifest is not valid JSON"
+ exit 1
+ fi
+
+ if ! jq -e '.platforms["darwin-aarch64"]' artifacts/latest-darwin-aarch64.json > /dev/null; then
+ echo "Error: darwin-aarch64 platform data not found in manifest"
+ exit 1
+ fi
+
+ if ! jq -e '.platforms["darwin-x86_64"]' artifacts/latest-darwin-x86_64.json > /dev/null; then
+ echo "Error: darwin-x86_64 platform data not found in manifest"
+ exit 1
+ fi
+
+ jq -n \
+ --arg version "$VERSION" \
+ --arg 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." \
+ --arg pub_date "$PUB_DATE" \
+ --slurpfile aarch64 artifacts/latest-darwin-aarch64.json \
+ --slurpfile x86_64 artifacts/latest-darwin-x86_64.json \
+ '{
+ version: $version,
+ notes: $notes,
+ pub_date: $pub_date,
+ platforms: {
+ "darwin-aarch64": $aarch64[0].platforms["darwin-aarch64"],
+ "darwin-x86_64": $x86_64[0].platforms["darwin-x86_64"]
+ }
+ }' > artifacts/latest.json
+
+ cat artifacts/latest.json
+
+ - name: Upload combined Tauri updater manifest
+ 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 }}
+
combine-electron-manifests:
needs: [create-release, build-desktop-electron-macos]
runs-on: ubuntu-latest
@@ -657,7 +463,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
finalize-release:
- needs: [create-release, build-desktop-macos, build-desktop-electron-macos, publish-npm, combine-manifests, combine-electron-manifests]
+ needs: [create-release, build-desktop-electron-macos, repackage-electron-as-tauri-update, publish-npm, combine-manifests, combine-electron-manifests]
runs-on: ubuntu-latest
env:
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
diff --git a/docs/TAURI_TO_ELECTRON_CUTOVER.md b/docs/TAURI_TO_ELECTRON_CUTOVER.md
index 28a21b53..3dbbff13 100644
--- a/docs/TAURI_TO_ELECTRON_CUTOVER.md
+++ b/docs/TAURI_TO_ELECTRON_CUTOVER.md
@@ -4,6 +4,13 @@
> 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
@@ -72,11 +79,46 @@ Check all of these before making any release:
5. **`minisign` CLI is available on the macOS runner** (or installable via
brew). Used to sign the Electron tarball with the Tauri key.
-## Release workflow changes
+## Current release workflow
-The file to edit: `.github/workflows/release.yml`.
+The release workflow no longer builds a Tauri desktop app. It now does this:
-Today it has these jobs (simplified):
+```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 uploads the signed `OpenChamber.app` 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 the existing Tauri minisign private 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`.
+
+## 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
@@ -90,6 +132,8 @@ create-release
### Step 1 — Remove the Tauri build
+Status: done.
+
Delete these jobs entirely:
- `build-desktop-macos`
- `combine-manifests`
@@ -99,6 +143,8 @@ list must be updated to drop both.
### Step 2 — Add a repackage job
+Status: done.
+
Insert after `build-desktop-electron-macos`:
```yaml
@@ -201,6 +247,8 @@ repackage-electron-as-tauri-update:
### 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:
@@ -232,6 +280,8 @@ combine-manifests:
### 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]
@@ -239,8 +289,11 @@ finalize-release:
### Step 5 — Remove Tauri-specific code
-After the transition release ships and has been out at least 2 weeks with no
-rollback, remove:
+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
@@ -249,9 +302,13 @@ rollback, remove:
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.
+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.
## Validation before tagging the transition release