Skip to content

Commit 10e43de

Browse files
committed
Feature: Test, fix, and document updates
- Tested manually - Added more builds that the app actually needs - Wrote feature docs
1 parent 2ec3f7e commit 10e43de

6 files changed

Lines changed: 202 additions & 34 deletions

File tree

.github/workflows/release.yml

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -34,18 +34,6 @@ jobs:
3434
- name: Install x86_64 target for universal binary
3535
run: rustup target add x86_64-apple-darwin
3636

37-
- name: Build sidecar binary for both architectures
38-
working-directory: ./apps/desktop/src-tauri
39-
run: |
40-
cargo build --release --bin cmdr-mcp-stdio --target aarch64-apple-darwin
41-
cargo build --release --bin cmdr-mcp-stdio --target x86_64-apple-darwin
42-
# Create universal binary
43-
mkdir -p target/universal-apple-darwin/release
44-
lipo -create \
45-
target/aarch64-apple-darwin/release/cmdr-mcp-stdio \
46-
target/x86_64-apple-darwin/release/cmdr-mcp-stdio \
47-
-output target/universal-apple-darwin/release/cmdr-mcp-stdio
48-
4937
- name: Build and release
5038
uses: tauri-apps/tauri-action@v0
5139
env:
@@ -107,6 +95,7 @@ jobs:
10795
NOTES=$(cat /tmp/notes.txt | jq -Rs .)
10896
10997
# Write to /tmp first (will copy to main branch later)
98+
# All platforms point to the same universal binary
11099
cat > /tmp/latest.json << EOF
111100
{
112101
"version": "$VERSION",
@@ -116,6 +105,14 @@ jobs:
116105
"darwin-universal": {
117106
"signature": "${{ steps.artifacts.outputs.signature }}",
118107
"url": "https://github.com/vdavid/cmdr/releases/download/${{ github.ref_name }}/${{ steps.artifacts.outputs.filename }}"
108+
},
109+
"darwin-aarch64": {
110+
"signature": "${{ steps.artifacts.outputs.signature }}",
111+
"url": "https://github.com/vdavid/cmdr/releases/download/${{ github.ref_name }}/${{ steps.artifacts.outputs.filename }}"
112+
},
113+
"darwin-x86_64": {
114+
"signature": "${{ steps.artifacts.outputs.signature }}",
115+
"url": "https://github.com/vdavid/cmdr/releases/download/${{ github.ref_name }}/${{ steps.artifacts.outputs.filename }}"
119116
}
120117
}
121118
}

apps/desktop/scripts/tauri-wrapper.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,23 @@ import { spawn } from 'child_process'
55
// Get arguments passed to the script
66
const args = process.argv.slice(2)
77

8-
// Check if the command is 'dev'
8+
// Check if the command is 'dev' or 'build'
99
const isDev = args.includes('dev')
10+
const isBuild = args.includes('build')
1011

1112
// If dev, inject the dev configuration
1213
if (isDev) {
1314
// Add -c src-tauri/tauri.dev.json to merge config
1415
args.push('-c', 'src-tauri/tauri.dev.json')
1516
}
1617

17-
// Spawn the tauri process via npx (avoids shell: true deprecation warning)
18-
const tauriProcess = spawn('npx', ['tauri', ...args], {
18+
// If build and no target specified, default to universal binary
19+
if (isBuild && !args.includes('--target') && !args.includes('-t')) {
20+
args.push('--target', 'universal-apple-darwin')
21+
}
22+
23+
// Spawn the tauri process via pnpm exec
24+
const tauriProcess = spawn('pnpm', ['exec', 'tauri', ...args], {
1925
stdio: 'inherit',
2026
})
2127

apps/desktop/src-tauri/tauri.dev.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,10 @@
22
"$schema": "https://schema.tauri.app/config/2",
33
"app": {
44
"withGlobalTauri": true
5+
},
6+
"plugins": {
7+
"updater": {
8+
"endpoints": ["http://localhost:4321/latest.json"]
9+
}
510
}
611
}

apps/desktop/src/lib/updater.svelte.ts

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { check, type Update } from '@tauri-apps/plugin-updater'
22
import { relaunch } from '@tauri-apps/plugin-process'
3+
import { getVersion } from '@tauri-apps/api/app'
4+
import { feLog } from './tauri-commands'
35

46
const checkIntervalMs = 60 * 60 * 1000 // 60 minutes
57

@@ -20,9 +22,6 @@ export function getUpdateState(): UpdateState {
2022
}
2123

2224
export async function checkForUpdates(): Promise<void> {
23-
// eslint-disable-next-line no-console
24-
console.log('[updater] checkForUpdates called, current status:', updateState.status)
25-
2625
if (updateState.status === 'downloading' || updateState.status === 'ready') {
2726
return // Don't interrupt ongoing download or ready state
2827
}
@@ -31,25 +30,25 @@ export async function checkForUpdates(): Promise<void> {
3130
updateState.error = null
3231

3332
try {
34-
// eslint-disable-next-line no-console
35-
console.log('[updater] Checking for updates...')
33+
const currentVersion = await getVersion()
34+
feLog(`[updater] Checking for updates (current: v${currentVersion})...`)
3635
const update = await check()
37-
// eslint-disable-next-line no-console
38-
console.log('[updater] Check result:', update)
3936

4037
if (update !== null) {
38+
feLog(`[updater] Update available: v${currentVersion} → v${update.version}`)
4139
updateState.status = 'downloading'
4240
await update.downloadAndInstall()
41+
feLog(`[updater] v${update.version} downloaded, restart to apply`)
4342
updateState.status = 'ready'
4443
updateState.update = update
4544
} else {
45+
feLog(`[updater] v${currentVersion} is up to date`)
4646
updateState.status = 'idle'
4747
}
4848
} catch (error) {
4949
updateState.status = 'idle'
5050
updateState.error = error instanceof Error ? error.message : String(error)
51-
// eslint-disable-next-line no-console
52-
console.error('Update check failed:', error)
51+
feLog(`[updater] Check failed: ${updateState.error}`)
5352
}
5453
}
5554

@@ -58,15 +57,8 @@ export async function restartToUpdate(): Promise<void> {
5857
}
5958

6059
export function startUpdateChecker(): () => void {
61-
// eslint-disable-next-line no-console
62-
console.log('[updater] startUpdateChecker called, DEV mode:', import.meta.env.DEV)
63-
64-
// Skip update checks in dev mode to avoid hitting real endpoint
65-
if (import.meta.env.DEV) {
66-
// eslint-disable-next-line no-console
67-
console.log('[updater] Skipping update check in dev mode')
68-
return () => {}
69-
}
60+
const endpoint = import.meta.env.DEV ? 'localhost:4321' : 'getcmdr.com'
61+
feLog(`[updater] Started (endpoint: ${endpoint})`)
7062

7163
// Check immediately on start
7264
void checkForUpdates()

apps/website/public/latest.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,15 @@
55
"platforms": {
66
"darwin-universal": {
77
"signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVRS2pCYTdOaDlnb3Y3ZHZteXQvK2ZITlVFbGY1SVdkQW9weVNsL2R3QWNTNW1zcmpvY2o5eUFQR2E0SE1NZm1Sd0NxSlRMR2RCeXFCUllJemlQejBaY01KcURiRzltMXdNPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzY4MzUyMDc5CWZpbGU6Q21kci5hcHAudGFyLmd6Ck0rbW9NVit0RjlPcWo3dnhhaTZTYVlwNVBrcXd1bnhZWmFiTTRYNDhWTUpzZXl0ejgxYUxSVSt6YmM4MDBTVW54NHptSjZkZGhLbnFwMDRBSUNmL0N3PT0K",
8-
"url": "https://github.com/vdavid/cmdr/releases/download/v0.3.1/Cmdr.app.tar.gz"
8+
"url": "https://github.com/vdavid/cmdr/releases/download/v0.3.1/Cmdr_universal.app.tar.gz"
9+
},
10+
"darwin-aarch64": {
11+
"signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVRS2pCYTdOaDlnb3Y3ZHZteXQvK2ZITlVFbGY1SVdkQW9weVNsL2R3QWNTNW1zcmpvY2o5eUFQR2E0SE1NZm1Sd0NxSlRMR2RCeXFCUllJemlQejBaY01KcURiRzltMXdNPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzY4MzUyMDc5CWZpbGU6Q21kci5hcHAudGFyLmd6Ck0rbW9NVit0RjlPcWo3dnhhaTZTYVlwNVBrcXd1bnhZWmFiTTRYNDhWTUpzZXl0ejgxYUxSVSt6YmM4MDBTVW54NHptSjZkZGhLbnFwMDRBSUNmL0N3PT0K",
12+
"url": "https://github.com/vdavid/cmdr/releases/download/v0.3.1/Cmdr_universal.app.tar.gz"
13+
},
14+
"darwin-x86_64": {
15+
"signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVRS2pCYTdOaDlnb3Y3ZHZteXQvK2ZITlVFbGY1SVdkQW9weVNsL2R3QWNTNW1zcmpvY2o5eUFQR2E0SE1NZm1Sd0NxSlRMR2RCeXFCUllJemlQejBaY01KcURiRzltMXdNPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzY4MzUyMDc5CWZpbGU6Q21kci5hcHAudGFyLmd6Ck0rbW9NVit0RjlPcWo3dnhhaTZTYVlwNVBrcXd1bnhZWmFiTTRYNDhWTUpzZXl0ejgxYUxSVSt6YmM4MDBTVW54NHptSjZkZGhLbnFwMDRBSUNmL0N3PT0K",
16+
"url": "https://github.com/vdavid/cmdr/releases/download/v0.3.1/Cmdr_universal.app.tar.gz"
917
}
1018
}
1119
}

docs/features/automated-updates.md

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
# Automated updates
2+
3+
This document describes how Cmdr checks for and installs updates automatically.
4+
5+
## Overview
6+
7+
Cmdr uses Tauri's built-in updater plugin to deliver updates:
8+
9+
1. App checks for updates on startup and every 60 minutes
10+
2. If an update is available, it downloads silently in the background
11+
3. User sees a "Restart to update" notification when ready
12+
4. Clicking restart applies the update and relaunches the app
13+
14+
Updates are signed with Ed25519 to ensure authenticity. The app won't install anything that doesn't match the embedded public key.
15+
16+
## Architecture
17+
18+
```
19+
┌─────────────────────────────────────────────────────────────────────┐
20+
│ Update flow │
21+
├─────────────────────────────────────────────────────────────────────┤
22+
│ │
23+
│ ┌─────────────────┐ ┌─────────────────┐ ┌───────────────┐ │
24+
│ │ GitHub Actions │────▶│ GitHub │────▶│ getcmdr.com │ │
25+
│ │ (build+sign) │ │ Releases │ │ /latest.json │ │
26+
│ └─────────────────┘ └─────────────────┘ └───────┬───────┘ │
27+
│ │ │
28+
│ ▼ │
29+
│ ┌───────────────────────────────────────────────────────────────┐ │
30+
│ │ Cmdr app │ │
31+
│ │ 1. Fetches latest.json │ │
32+
│ │ 2. Compares versions │ │
33+
│ │ 3. Downloads .tar.gz from GitHub Releases │ │
34+
│ │ 4. Verifies Ed25519 signature │ │
35+
│ │ 5. Shows "Restart to update" notification │ │
36+
│ └───────────────────────────────────────────────────────────────┘ │
37+
│ │
38+
└─────────────────────────────────────────────────────────────────────┘
39+
```
40+
41+
## Update manifest
42+
43+
The app fetches `https://getcmdr.com/latest.json` to check for updates:
44+
45+
```json
46+
{
47+
"version": "0.3.1",
48+
"notes": "### Added\n- New feature...",
49+
"pub_date": "2026-01-14T00:54:48Z",
50+
"platforms": {
51+
"darwin-universal": {
52+
"signature": "base64-encoded-ed25519-signature",
53+
"url": "https://github.com/vdavid/cmdr/releases/download/v0.3.1/Cmdr_universal.app.tar.gz"
54+
},
55+
"darwin-aarch64": { ... },
56+
"darwin-x86_64": { ... }
57+
}
58+
}
59+
```
60+
61+
All three macOS platforms point to the same universal binary. This ensures both Apple Silicon and Intel Macs find a matching update.
62+
63+
## Implementation
64+
65+
### Frontend (`apps/desktop/src/lib/updater.svelte.ts`)
66+
67+
The updater service manages the update lifecycle:
68+
69+
| Export | Description |
70+
|------------------------|-----------------------------------------------------------------------|
71+
| `startUpdateChecker()` | Starts checking on launch and every 60 min. Returns cleanup function. |
72+
| `checkForUpdates()` | Manually triggers an update check |
73+
| `getUpdateState()` | Returns current state: `idle`, `checking`, `downloading`, or `ready` |
74+
| `restartToUpdate()` | Relaunches the app to apply the downloaded update |
75+
76+
### UI (`apps/desktop/src/lib/UpdateNotification.svelte`)
77+
78+
A toast notification that appears in the bottom-right corner when an update is ready. Shows "Restart to update" with a button to trigger the restart.
79+
80+
### Configuration
81+
82+
**Production** (`tauri.conf.json`):
83+
```json
84+
"plugins": {
85+
"updater": {
86+
"endpoints": ["https://getcmdr.com/latest.json"],
87+
"pubkey": "base64-encoded-public-key"
88+
}
89+
}
90+
```
91+
92+
**Development** (`tauri.dev.json`):
93+
```json
94+
"plugins": {
95+
"updater": {
96+
"endpoints": ["http://localhost:4321/latest.json"]
97+
}
98+
}
99+
```
100+
101+
### Capabilities
102+
103+
The updater requires these permissions in `capabilities/default.json`:
104+
- `updater:default` — allows checking and downloading updates
105+
- `process:allow-restart` — allows relaunching the app
106+
107+
## Release workflow
108+
109+
When you push a version tag (for example, `v0.3.2`), the GitHub Actions release workflow:
110+
111+
1. Builds a universal macOS binary (aarch64 + x86_64)
112+
2. Signs the `.app.tar.gz` with Ed25519 using `TAURI_SIGNING_PRIVATE_KEY`
113+
3. Uploads artifacts to GitHub Releases
114+
4. Updates `apps/website/public/latest.json` with the new version and signature
115+
5. Triggers a website deploy so the manifest is live
116+
117+
See [Releasing guide](../guides/releasing.md) for step-by-step instructions.
118+
119+
## Logging
120+
121+
The updater logs to the backend via `feLog()`. Example output:
122+
123+
```
124+
[updater] Started (endpoint: getcmdr.com)
125+
[updater] Checking for updates (current: v0.3.0)...
126+
[updater] Update available: v0.3.0 → v0.3.1
127+
[updater] v0.3.1 downloaded, restart to apply
128+
```
129+
130+
On error:
131+
```
132+
[updater] Check failed: Download request failed with status: 404 Not Found
133+
```
134+
135+
## Local testing
136+
137+
To test updates locally without deploying:
138+
139+
1. Start the website dev server (serves `latest.json`):
140+
```bash
141+
cd apps/website && pnpm dev
142+
```
143+
144+
2. Edit `apps/website/public/latest.json` — set a version higher than your local build
145+
146+
3. Run the app in dev mode:
147+
```bash
148+
cd apps/desktop && pnpm tauri dev
149+
```
150+
151+
4. The app checks `localhost:4321/latest.json` and shows the update notification
152+
153+
Note: The actual download will fail locally since there's no signed artifact. This flow is useful for testing the detection and UI.
154+
155+
## Security
156+
157+
- **Signature verification**: Every update is verified against the embedded Ed25519 public key before installation
158+
- **HTTPS**: Production endpoint uses HTTPS
159+
- **No downgrade**: Tauri won't install older versions by default
160+
- **Signed releases**: Only CI can sign releases (private key is a GitHub secret)

0 commit comments

Comments
 (0)