diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4af321b..72b94c7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -71,6 +71,7 @@ jobs: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_ORG: ${{ secrets.SENTRY_ORG }} SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }} + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} - name: Ad-hoc sign run: | @@ -78,6 +79,48 @@ jobs: echo "Signing: $APP" codesign --deep --force --sign - "$APP" + - name: Collect updater artifacts + run: | + BUNDLE=src-tauri/target/universal-apple-darwin/release/bundle/macos + TARGZ=$(find "$BUNDLE" -name "*.app.tar.gz" -maxdepth 1 | head -1) + SIG=$(find "$BUNDLE" -name "*.app.tar.gz.sig" -maxdepth 1 | head -1) + if [ -z "$TARGZ" ] || [ -z "$SIG" ]; then + echo "No signed updater artifacts — TAURI_SIGNING_PRIVATE_KEY may not be set." + echo "HAS_UPDATER=false" >> $GITHUB_ENV + else + TARGZ_NAME=$(basename "$TARGZ") + cp "$TARGZ" "$TARGZ_NAME" + cp "$SIG" "${TARGZ_NAME}.sig" + echo "TARGZ_NAME=$TARGZ_NAME" >> $GITHUB_ENV + echo "HAS_UPDATER=true" >> $GITHUB_ENV + fi + + - name: Generate latest.json + if: env.HAS_UPDATER == 'true' + run: | + VERSION="${{ steps.ref.outputs.tag }}" + VERSION_NUM="${VERSION#v}" + TARGZ_NAME="${{ env.TARGZ_NAME }}" + DOWNLOAD_URL="https://github.com/varkart/contextbar/releases/download/${VERSION}/${TARGZ_NAME}" + python3 - "$VERSION_NUM" "$DOWNLOAD_URL" "${TARGZ_NAME}.sig" <<'PYEOF' + import sys, json + from datetime import datetime, timezone + version, url, sig_path = sys.argv[1], sys.argv[2], sys.argv[3] + signature = open(sig_path).read() + manifest = { + "version": version, + "notes": f"See https://github.com/varkart/contextbar/releases/tag/v{version}", + "pub_date": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), + "platforms": { + "darwin-aarch64": {"signature": signature, "url": url}, + "darwin-x86_64": {"signature": signature, "url": url}, + } + } + with open("latest.json", "w") as f: + json.dump(manifest, f, indent=2) + print(json.dumps(manifest, indent=2)) + PYEOF + - name: Package DMG run: | VERSION="${{ steps.ref.outputs.tag }}" @@ -112,6 +155,19 @@ jobs: SHA256=$(shasum -a 256 "${{ env.DMG_NAME }}" | awk '{print $1}') echo "DMG_SHA256=$SHA256" >> $GITHUB_ENV + - name: Build release file list + run: | + { + echo "RELEASE_FILES<> $GITHUB_ENV + - name: Create GitHub Release uses: softprops/action-gh-release@v2 with: @@ -123,8 +179,9 @@ jobs: --- **Install (DMG):** Download the `.dmg` below. **Install (Homebrew):** `brew tap varkart/tap && brew install --cask contextbar` + **Auto-update:** Existing installs update automatically via the Settings panel. **macOS security prompt:** run `xattr -d com.apple.quarantine /Applications/Context\ Bar.app` - files: ${{ env.DMG_NAME }} + files: ${{ env.RELEASE_FILES }} draft: false prerelease: false diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 100836e..308b9ef 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1815,6 +1815,23 @@ async fn check_for_update(app: tauri::AppHandle) -> Result Result<(), String> { + use tauri_plugin_updater::UpdaterExt; + let update = app + .updater() + .map_err(|e| e.to_string())? + .check() + .await + .map_err(|e| e.to_string())? + .ok_or_else(|| "No update available".to_string())?; + update + .download_and_install(|_, _| {}, || {}) + .await + .map_err(|e| e.to_string())?; + app.restart(); +} + // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- @@ -1939,6 +1956,7 @@ pub fn run() { install_skill_from_github, read_text_file, check_for_update, + install_update, query_mcp_tools, remove_skill, set_skill_active, diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 9343b86..fb0c32c 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -17,7 +17,7 @@ }, "plugins": { "updater": { - "pubkey": "", + "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDg4ODlBMUMxQThDNDM5RUYKUldUdk9jU293YUdKaUxpNHhPWjZPNktUWG9kMTBCU1VBdTQzYnNEN2RwaGdsb3l0OXZRMWNaM2QK", "endpoints": ["https://github.com/varkart/contextbar/releases/latest/download/latest.json"] } }, diff --git a/src/components/Settings.tsx b/src/components/Settings.tsx index e9a2e67..40af682 100644 --- a/src/components/Settings.tsx +++ b/src/components/Settings.tsx @@ -202,6 +202,8 @@ export default function Settings({ updateInfo, theme, onThemeChange, onOpenLogs const [vibrancyLoading, setVibrancyLoading] = useState(true) const [version, setVersion] = useState('') const [accessibilityGranted, setAccessibilityGranted] = useState(null) + const [installing, setInstalling] = useState(false) + const [installError, setInstallError] = useState(null) useEffect(() => { Promise.all([ @@ -309,10 +311,27 @@ export default function Settings({ updateInfo, theme, onThemeChange, onOpenLogs {updateInfo && ( - - {updateInfo.latestVersion} available - +
+ + {installError && ( + {installError} + )} +
)} diff --git a/src/components/__tests__/Settings.test.tsx b/src/components/__tests__/Settings.test.tsx index 06fe92f..a6dc2d6 100644 --- a/src/components/__tests__/Settings.test.tsx +++ b/src/components/__tests__/Settings.test.tsx @@ -113,7 +113,7 @@ describe('Settings', () => { it('shows update info when provided', async () => { render() - await waitFor(() => expect(screen.getByText(/1\.0\.0 available/)).toBeInTheDocument()) + await waitFor(() => expect(screen.getByRole('button', { name: /Install 1\.0\.0/ })).toBeInTheDocument()) }) it('does not show update row when updateInfo is null', async () => {