From 190b7d007575887ee42dd56ac30ccfd364f63538 Mon Sep 17 00:00:00 2001 From: lucaronin Date: Sun, 22 Feb 2026 12:10:24 +0100 Subject: [PATCH 1/2] feat: add OTA updater plugin with startup update check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Install @tauri-apps/plugin-updater + plugin-process (npm + cargo) - Configure tauri.conf.json: updater endpoint pointing to GitHub Releases, signing pubkey, createUpdaterArtifacts enabled - Register updater + process plugins in Rust (desktop-only, #[cfg(desktop)]) - Add updater:default + process:default capabilities - Add useUpdater hook: checks for updates 3s after startup, shows native confirm dialog, downloads+installs+relaunches on accept - Silently catches errors so update failures never block the app - Add comprehensive tests for the useUpdater hook (5 test cases) Product decision: Using window.confirm() for the update dialog rather than a custom React modal. This keeps the implementation simple and the native dialog is appropriate for a system-level action like app restart. Can be upgraded to a custom UI later if desired. Private key generated at ~/.tauri/laputa.key — NOT committed. For CI: set TAURI_SIGNING_PRIVATE_KEY + TAURI_SIGNING_PRIVATE_KEY_PASSWORD as GitHub Secrets. Co-Authored-By: Claude Opus 4.6 --- package.json | 2 + pnpm-lock.yaml | 20 +++ src-tauri/Cargo.lock | 209 +++++++++++++++++++++++++++- src-tauri/Cargo.toml | 2 + src-tauri/capabilities/default.json | 4 +- src-tauri/src/lib.rs | 6 + src-tauri/tauri.conf.json | 9 ++ src/App.tsx | 3 + src/hooks/useUpdater.test.ts | 107 ++++++++++++++ src/hooks/useUpdater.ts | 40 ++++++ 10 files changed, 399 insertions(+), 3 deletions(-) create mode 100644 src/hooks/useUpdater.test.ts create mode 100644 src/hooks/useUpdater.ts diff --git a/package.json b/package.json index 60451401..26a95fd5 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,8 @@ "@radix-ui/react-tooltip": "^1.2.8", "@tailwindcss/vite": "^4.1.18", "@tauri-apps/api": "^2.10.1", + "@tauri-apps/plugin-process": "^2.3.1", + "@tauri-apps/plugin-updater": "^2.10.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "katex": "^0.16.28", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 949346a2..15428e3d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -62,6 +62,12 @@ importers: '@tauri-apps/api': specifier: ^2.10.1 version: 2.10.1 + '@tauri-apps/plugin-process': + specifier: ^2.3.1 + version: 2.3.1 + '@tauri-apps/plugin-updater': + specifier: ^2.10.0 + version: 2.10.0 class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -1708,6 +1714,12 @@ packages: engines: {node: '>= 10'} hasBin: true + '@tauri-apps/plugin-process@2.3.1': + resolution: {integrity: sha512-nCa4fGVaDL/B9ai03VyPOjfAHRHSBz5v6F/ObsB73r/dA3MHHhZtldaDMIc0V/pnUw9ehzr2iEG+XkSEyC0JJA==} + + '@tauri-apps/plugin-updater@2.10.0': + resolution: {integrity: sha512-ljN8jPlnT0aSn8ecYhuBib84alxfMx6Hc8vJSKMJyzGbTPFZAC44T2I1QNFZssgWKrAlofvJqCC6Rr472JWfkQ==} + '@testing-library/dom@10.4.1': resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} engines: {node: '>=18'} @@ -5075,6 +5087,14 @@ snapshots: '@tauri-apps/cli-win32-ia32-msvc': 2.10.0 '@tauri-apps/cli-win32-x64-msvc': 2.10.0 + '@tauri-apps/plugin-process@2.3.1': + dependencies: + '@tauri-apps/api': 2.10.1 + + '@tauri-apps/plugin-updater@2.10.0': + dependencies: + '@tauri-apps/api': 2.10.1 + '@testing-library/dom@10.4.1': dependencies: '@babel/code-frame': 7.29.0 diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 4fb24cc8..1dae2a53 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -93,6 +93,15 @@ version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + [[package]] name = "arraydeque" version = "0.5.1" @@ -633,6 +642,17 @@ dependencies = [ "serde_core", ] +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.115", +] + [[package]] name = "derive_more" version = "0.99.20" @@ -884,6 +904,17 @@ dependencies = [ "rustc_version", ] +[[package]] +name = "filetime" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" +dependencies = [ + "cfg-if", + "libc", + "libredox", +] + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -1910,6 +1941,8 @@ dependencies = [ "tauri", "tauri-build", "tauri-plugin-log", + "tauri-plugin-process", + "tauri-plugin-updater", "tempfile", "tokio", "walkdir", @@ -1975,6 +2008,7 @@ checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" dependencies = [ "bitflags 2.11.0", "libc", + "redox_syscall 0.7.1", ] [[package]] @@ -2065,6 +2099,12 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minisign-verify" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e856fdd13623a2f5f2f54676a4ee49502a96a80ef4a62bcedd23d52427c44d43" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -2373,6 +2413,18 @@ dependencies = [ "objc2-core-foundation", ] +[[package]] +name = "objc2-osa-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f112d1746737b0da274ef79a23aac283376f335f4095a083a267a082f21db0c0" +dependencies = [ + "bitflags 2.11.0", + "objc2", + "objc2-app-kit", + "objc2-foundation", +] + [[package]] name = "objc2-quartz-core" version = "0.3.2" @@ -2480,6 +2532,20 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "osakit" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b" +dependencies = [ + "objc2", + "objc2-foundation", + "objc2-osa-kit", + "serde", + "serde_json", + "thiserror 2.0.18", +] + [[package]] name = "pango" version = "0.18.3" @@ -2523,7 +2589,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.18", "smallvec", "windows-link 0.2.1", ] @@ -2966,6 +3032,15 @@ dependencies = [ "bitflags 2.11.0", ] +[[package]] +name = "redox_syscall" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35985aa610addc02e24fc232012c86fd11f14111180f902b67e2d5331f8ebf2b" +dependencies = [ + "bitflags 2.11.0", +] + [[package]] name = "redox_users" version = "0.4.6" @@ -3103,15 +3178,20 @@ dependencies = [ "http-body", "http-body-util", "hyper", + "hyper-rustls", "hyper-util", "js-sys", "log", "percent-encoding", "pin-project-lite", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", "serde", "serde_json", "sync_wrapper", "tokio", + "tokio-rustls", "tokio-util", "tower", "tower-http", @@ -3211,12 +3291,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" dependencies = [ "once_cell", + "ring", "rustls-pki-types", "rustls-webpki", "subtle", "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + [[package]] name = "rustls-pki-types" version = "1.14.0" @@ -3226,6 +3319,33 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-platform-verifier" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + [[package]] name = "rustls-webpki" version = "0.103.9" @@ -3629,7 +3749,7 @@ dependencies = [ "objc2-foundation", "objc2-quartz-core", "raw-window-handle", - "redox_syscall", + "redox_syscall 0.5.18", "tracing", "wasm-bindgen", "web-sys", @@ -3849,6 +3969,17 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +[[package]] +name = "tar" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" +dependencies = [ + "filetime", + "libc", + "xattr", +] + [[package]] name = "target-lexicon" version = "0.12.16" @@ -4009,6 +4140,49 @@ dependencies = [ "time", ] +[[package]] +name = "tauri-plugin-process" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d55511a7bf6cd70c8767b02c97bf8134fa434daf3926cfc1be0a0f94132d165a" +dependencies = [ + "tauri", + "tauri-plugin", +] + +[[package]] +name = "tauri-plugin-updater" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fe8e9bebd88fc222938ffdfbdcfa0307081423bd01e3252fc337d8bde81fc61" +dependencies = [ + "base64 0.22.1", + "dirs 6.0.0", + "flate2", + "futures-util", + "http", + "infer", + "log", + "minisign-verify", + "osakit", + "percent-encoding", + "reqwest 0.13.2", + "rustls", + "semver", + "serde", + "serde_json", + "tar", + "tauri", + "tauri-plugin", + "tempfile", + "thiserror 2.0.18", + "time", + "tokio", + "url", + "windows-sys 0.60.2", + "zip", +] + [[package]] name = "tauri-runtime" version = "2.10.0" @@ -4886,6 +5060,15 @@ dependencies = [ "system-deps", ] +[[package]] +name = "webpki-root-certs" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "webview2-com" version = "0.38.2" @@ -5630,6 +5813,16 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + [[package]] name = "yaml-rust2" version = "0.8.1" @@ -5744,6 +5937,18 @@ dependencies = [ "syn 2.0.115", ] +[[package]] +name = "zip" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1" +dependencies = [ + "arbitrary", + "crc32fast", + "indexmap 2.13.0", + "memchr", +] + [[package]] name = "zmij" version = "1.0.21" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index efcb16e1..6732c488 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -29,6 +29,8 @@ tokio = { version = "1", features = ["rt-multi-thread", "macros"] } futures-util = "0.3" base64 = "0.22" dirs = "5" +tauri-plugin-updater = "2.10.0" +tauri-plugin-process = "2.3.1" [dev-dependencies] tempfile = "3" diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index c135d7f1..5e95bf9c 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -6,6 +6,8 @@ "main" ], "permissions": [ - "core:default" + "core:default", + "updater:default", + "process:default" ] } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index acaaee6d..1141ecfe 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -85,6 +85,12 @@ pub fn run() { )?; } + #[cfg(desktop)] + { + app.handle().plugin(tauri_plugin_updater::Builder::new().build())?; + app.handle().plugin(tauri_plugin_process::init())?; + } + // Purge trashed files older than 30 days on startup let vault_path = dirs::home_dir() .map(|h| h.join("Laputa")) diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 04e74f15..0a22c0a5 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -33,6 +33,7 @@ "bundle": { "active": true, "targets": "all", + "createUpdaterArtifacts": true, "icon": [ "icons/32x32.png", "icons/128x128.png", @@ -40,5 +41,13 @@ "icons/icon.icns", "icons/icon.ico" ] + }, + "plugins": { + "updater": { + "endpoints": [ + "https://github.com/refactoringhq/laputa-app/releases/latest/download/latest.json" + ], + "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IERFNzVEOTA0Q0U2QUE5ODIKUldTQ3FXck9CTmwxM2x2eUZyOFYwUno0ZzVXVStCeG9xcUcvK0pIWnRWS0xMbmEwVE84NUdXejAK" + } } } diff --git a/src/App.tsx b/src/App.tsx index 254f1671..15552a2d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -14,6 +14,7 @@ import { useAppKeyboard } from './hooks/useAppKeyboard' import { useEntryActions } from './hooks/useEntryActions' import { isTauri } from './mock-tauri' import { useKeyboardNavigation } from './hooks/useKeyboardNavigation' +import { useUpdater } from './hooks/useUpdater' import type { SidebarSelection, GitCommit } from './types' import './App.css' @@ -106,6 +107,8 @@ function App() { handleCloseTabRef: notes.handleCloseTabRef, }) + useUpdater() + useKeyboardNavigation({ tabs: notes.tabs, activeTabPath: notes.activeTabPath, diff --git a/src/hooks/useUpdater.test.ts b/src/hooks/useUpdater.test.ts new file mode 100644 index 00000000..d63d83df --- /dev/null +++ b/src/hooks/useUpdater.test.ts @@ -0,0 +1,107 @@ +import { renderHook } from '@testing-library/react' +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' +import { useUpdater } from './useUpdater' + +// Mock isTauri +vi.mock('../mock-tauri', () => ({ + isTauri: vi.fn(() => false), +})) + +// Mock the dynamic imports +const mockCheck = vi.fn() +const mockRelaunch = vi.fn() + +vi.mock('@tauri-apps/plugin-updater', () => ({ + check: (...args: unknown[]) => mockCheck(...args), +})) + +vi.mock('@tauri-apps/plugin-process', () => ({ + relaunch: (...args: unknown[]) => mockRelaunch(...args), +})) + +import { isTauri } from '../mock-tauri' + +describe('useUpdater', () => { + beforeEach(() => { + vi.useFakeTimers() + vi.clearAllMocks() + vi.spyOn(window, 'confirm').mockReturnValue(false) + vi.spyOn(console, 'warn').mockImplementation(() => {}) + }) + + afterEach(() => { + vi.useRealTimers() + vi.restoreAllMocks() + }) + + it('does nothing when not in Tauri', () => { + vi.mocked(isTauri).mockReturnValue(false) + renderHook(() => useUpdater()) + vi.advanceTimersByTime(5000) + expect(mockCheck).not.toHaveBeenCalled() + }) + + it('checks for updates after delay when in Tauri', async () => { + vi.mocked(isTauri).mockReturnValue(true) + mockCheck.mockResolvedValue(null) // no update + + renderHook(() => useUpdater()) + expect(mockCheck).not.toHaveBeenCalled() + + // Advance past the 3s delay, then flush microtasks for dynamic imports + await vi.advanceTimersByTimeAsync(3500) + // Dynamic imports are resolved by the mock, but need microtask flush + await vi.waitFor(() => { + expect(mockCheck).toHaveBeenCalledOnce() + }) + }) + + it('shows confirm dialog when update is available', async () => { + vi.mocked(isTauri).mockReturnValue(true) + mockCheck.mockResolvedValue({ + version: '1.2.0', + body: 'Bug fixes and improvements', + downloadAndInstall: vi.fn().mockResolvedValue(undefined), + }) + vi.spyOn(window, 'confirm').mockReturnValue(false) + + renderHook(() => useUpdater()) + await vi.advanceTimersByTimeAsync(3500) + + expect(window.confirm).toHaveBeenCalledWith( + expect.stringContaining('1.2.0') + ) + expect(mockRelaunch).not.toHaveBeenCalled() + }) + + it('downloads, installs, and relaunches when user accepts', async () => { + vi.mocked(isTauri).mockReturnValue(true) + const mockDownloadAndInstall = vi.fn().mockResolvedValue(undefined) + mockCheck.mockResolvedValue({ + version: '1.2.0', + body: '', + downloadAndInstall: mockDownloadAndInstall, + }) + vi.spyOn(window, 'confirm').mockReturnValue(true) + mockRelaunch.mockResolvedValue(undefined) + + renderHook(() => useUpdater()) + await vi.advanceTimersByTimeAsync(3500) + + expect(mockDownloadAndInstall).toHaveBeenCalled() + expect(mockRelaunch).toHaveBeenCalled() + }) + + it('logs warning on check failure without crashing', async () => { + vi.mocked(isTauri).mockReturnValue(true) + mockCheck.mockRejectedValue(new Error('Network error')) + + renderHook(() => useUpdater()) + await vi.advanceTimersByTimeAsync(3500) + + expect(console.warn).toHaveBeenCalledWith( + '[updater] Failed to check for updates:', + expect.any(Error) + ) + }) +}) diff --git a/src/hooks/useUpdater.ts b/src/hooks/useUpdater.ts new file mode 100644 index 00000000..4f63f4b4 --- /dev/null +++ b/src/hooks/useUpdater.ts @@ -0,0 +1,40 @@ +import { useEffect } from 'react' +import { isTauri } from '../mock-tauri' + +/** + * Checks for OTA updates on app startup (Tauri only). + * If an update is available, shows a native confirm dialog and + * downloads + installs + relaunches if the user accepts. + */ +export function useUpdater() { + useEffect(() => { + if (!isTauri()) return + + const checkForUpdates = async () => { + try { + const { check } = await import('@tauri-apps/plugin-updater') + const { relaunch } = await import('@tauri-apps/plugin-process') + + const update = await check() + if (!update) return + + const yes = window.confirm( + `A new version (${update.version}) is available.\n\n` + + (update.body ? `${update.body}\n\n` : '') + + 'Do you want to update and restart now?' + ) + if (!yes) return + + await update.downloadAndInstall() + await relaunch() + } catch (err) { + // Silently log — update check failures should never block the app + console.warn('[updater] Failed to check for updates:', err) + } + } + + // Delay slightly so the app can render first + const timer = setTimeout(checkForUpdates, 3000) + return () => clearTimeout(timer) + }, []) +} From d39b8176255a23c3c1cad74884e54290edef7734 Mon Sep 17 00:00:00 2001 From: lucaronin Date: Sun, 22 Feb 2026 12:10:33 +0100 Subject: [PATCH 2/2] ci: add release workflow for macOS OTA updates via GitHub Releases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Triggered on v* tags. Builds universal macOS binaries (aarch64 + x86_64) using tauri-apps/tauri-action@v0, signs updater artifacts with TAURI_SIGNING_PRIVATE_KEY secret, and publishes to GitHub Releases. The updater endpoint in tauri.conf.json points to the latest.json artifact from these releases, completing the OTA update pipeline. No Apple code signing for now — macOS Gatekeeper will show unsigned-app warnings until we add notarization in a future task. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/release.yml | 57 +++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..21d540e1 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,57 @@ +name: Release + +on: + push: + tags: + - 'v*' + +jobs: + release: + name: Build & Release (macOS) + permissions: + contents: write + strategy: + matrix: + include: + - target: aarch64-apple-darwin + runner: macos-latest + - target: x86_64-apple-darwin + runner: macos-latest + + runs-on: ${{ matrix.runner }} + + steps: + - uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 10 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + cache: 'pnpm' + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build and release + uses: tauri-apps/tauri-action@v0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} + with: + tagName: ${{ github.ref_name }} + releaseName: 'Laputa v__VERSION__' + releaseBody: 'See the assets below to download and install this version.' + releaseDraft: false + prerelease: false + args: --target ${{ matrix.target }}