diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..d1c0534 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,207 @@ +# https://github.com/tauri-apps/tauri-action +name: Release + +on: + push: + branches: + - main + +jobs: + preflight: + name: Preflight — check if release already exists + permissions: + contents: read + runs-on: ubuntu-latest + outputs: + skip: ${{ steps.check.outputs.skip }} + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Check for existing tag/release v__VERSION__ + id: check + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const fs = require('fs'); + const path = require('path'); + const cfgPath = path.join(process.cwd(), 'src-tauri', 'tauri.conf.json'); + let version = ''; + try { + const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8')); + version = cfg.version || ''; + } catch (e) { + core.info(`Failed reading ${cfgPath}: ${e.message}`); + } + if (!version) { + try { + const pkg = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'package.json'), 'utf8')); + version = pkg.version || ''; + } catch (e) { + core.info(`Failed reading package.json: ${e.message}`); + } + } + if (!version) { + core.setFailed('Unable to determine version from src-tauri/tauri.conf.json or package.json'); + return; + } + const tagName = `v${version}`; + core.info(`Checking if tag or release exists for ${tagName} ...`); + + try { + await github.rest.git.getRef({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: `tags/${tagName}`, + }); + core.info(`Tag ${tagName} already exists. Skipping release.`); + core.setOutput('skip', 'true'); + return; + } catch (e) { + if (e.status !== 404) throw e; + } + const releases = await github.rest.repos.listReleases({ + owner: context.repo.owner, + repo: context.repo.repo, + per_page: 100, + }); + const found = releases.data.find(r => r.tag_name === tagName); + if (found) { + core.info(`Release with tag ${tagName} already exists. Skipping release.`); + core.setOutput('skip', 'true'); + return; + } + core.info(`OK: ${tagName} does not exist. Proceeding with release.`); + core.setOutput('skip', 'false'); + + publish-tauri: + needs: preflight + if: needs.preflight.outputs.skip != 'true' + permissions: + contents: write + strategy: + fail-fast: false + matrix: + include: + - platform: 'macos-latest' + args: '--target aarch64-apple-darwin' + - platform: 'macos-latest' + args: '--target x86_64-apple-darwin' + - platform: 'windows-latest' + args: '--bundles nsis' + + runs-on: ${{ matrix.platform }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + + - name: Install Rust (stable) + uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }} + + - name: Cache Rust + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + src-tauri/target + key: ${{ runner.os }}-cargo-${{ hashFiles('src-tauri/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo- + + - name: Install frontend dependencies + run: npm ci + + - name: Build and publish with Tauri + uses: tauri-apps/tauri-action@v0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + with: + tagName: v__VERSION__ + releaseName: 'v__VERSION__' + releaseBody: 'See the assets to download this version and install.' + releaseDraft: false + prerelease: false + generateReleaseNotes: true + includeUpdaterJson: true + updaterJsonPreferNsis: true + args: ${{ matrix.args }} + + upload-stable-assets: + name: Upload fixed-name assets for README links + needs: publish-tauri + permissions: + contents: write + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Upload fixed-name copies + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const fs = require('fs'); + const cfg = JSON.parse(fs.readFileSync('src-tauri/tauri.conf.json', 'utf8')); + const version = cfg.version; + const tag = `v${version}`; + + const { data: release } = await github.rest.repos.getReleaseByTag({ + owner: context.repo.owner, + repo: context.repo.repo, + tag, + }); + + // Mapping: versioned asset name pattern → fixed name + const renames = [ + { match: `_${version}_aarch64.dmg`, fixedName: 'Scripta_macOS_arm64.dmg' }, + { match: `_${version}_x64.dmg`, fixedName: 'Scripta_macOS_x64.dmg' }, + { match: `_${version}_x64-setup.exe`, fixedName: 'Scripta_Windows_x64-setup.exe' }, + ]; + + // Remove old fixed-name assets if they exist from a previous run + for (const { fixedName } of renames) { + const existing = release.assets.find(a => a.name === fixedName); + if (existing) { + await github.rest.repos.deleteReleaseAsset({ + owner: context.repo.owner, + repo: context.repo.repo, + asset_id: existing.id, + }); + core.info(`Deleted old asset: ${fixedName}`); + } + } + + for (const { match, fixedName } of renames) { + const asset = release.assets.find(a => a.name.endsWith(match)); + if (!asset) { + core.warning(`Asset matching *${match} not found, skipping ${fixedName}`); + continue; + } + // Download the versioned asset + const download = await github.rest.repos.getReleaseAsset({ + owner: context.repo.owner, + repo: context.repo.repo, + asset_id: asset.id, + headers: { accept: 'application/octet-stream' }, + }); + // Upload with fixed name + const uploadUrl = release.upload_url; + await github.rest.repos.uploadReleaseAsset({ + owner: context.repo.owner, + repo: context.repo.repo, + release_id: release.id, + name: fixedName, + data: Buffer.from(download.data), + }); + core.info(`Uploaded ${fixedName} (from ${asset.name})`); + } diff --git a/README.md b/README.md index 84e9b53..b28ba27 100644 --- a/README.md +++ b/README.md @@ -89,8 +89,11 @@ Scripta remembers which note you had open, where you were scrolled to, and where Download the latest version from the [Releases](https://github.com/j4rviscmd/Scripta/releases/latest) page. -- **macOS**: Download the `.dmg` file -- **Windows**: Download the `.msi` installer +| Platform | Download | +| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | +| **macOS (Apple Silicon)** | [Scripta_macOS_arm64.dmg](https://github.com/j4rviscmd/Scripta/releases/latest/download/Scripta_macOS_arm64.dmg) | +| **macOS (Intel)** | [Scripta_macOS_x64.dmg](https://github.com/j4rviscmd/Scripta/releases/latest/download/Scripta_macOS_x64.dmg) | +| **Windows** | [Scripta_Windows_x64-setup.exe](https://github.com/j4rviscmd/Scripta/releases/latest/download/Scripta_Windows_x64-setup.exe) | > [!NOTE] > macOS builds are not signed. On first launch, run: diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 1ba8b87..7e96b92 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -59,6 +59,15 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + [[package]] name = "async-broadcast" version = "0.7.2" @@ -701,6 +710,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.117", +] + [[package]] name = "derive_more" version = "0.99.20" @@ -1001,6 +1021,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" @@ -2055,7 +2086,10 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" dependencies = [ + "bitflags 2.11.0", "libc", + "plain", + "redox_syscall 0.7.3", ] [[package]] @@ -2171,6 +2205,12 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minisign-verify" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f9645cb765ea72b8111f36c522475d2daa0d22c957a9826437e97534bc4e9e" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -2362,6 +2402,7 @@ checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ "bitflags 2.11.0", "block2", + "libc", "objc2", "objc2-core-foundation", ] @@ -2377,6 +2418,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" @@ -2433,6 +2486,12 @@ dependencies = [ "pathdiff", ] +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + [[package]] name = "option-ext" version = "0.2.0" @@ -2449,6 +2508,20 @@ dependencies = [ "pin-project-lite", ] +[[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" @@ -2498,7 +2571,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.18", "smallvec", "windows-link 0.2.1", ] @@ -2731,6 +2804,12 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + [[package]] name = "plist" version = "1.8.0" @@ -3089,6 +3168,15 @@ dependencies = [ "bitflags 2.11.0", ] +[[package]] +name = "redox_syscall" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" +dependencies = [ + "bitflags 2.11.0", +] + [[package]] name = "redox_users" version = "0.5.2" @@ -3201,15 +3289,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", @@ -3315,6 +3408,18 @@ dependencies = [ "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" @@ -3325,6 +3430,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", + "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.10" @@ -3357,6 +3489,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "schemars" version = "0.8.22" @@ -3445,9 +3586,33 @@ dependencies = [ "tauri-plugin-fs", "tauri-plugin-opener", "tauri-plugin-store", + "tauri-plugin-updater", "uuid", ] +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.11.0", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "selectors" version = "0.24.0" @@ -3775,7 +3940,7 @@ dependencies = [ "objc2-foundation", "objc2-quartz-core", "raw-window-handle", - "redox_syscall", + "redox_syscall 0.5.18", "tracing", "wasm-bindgen", "web-sys", @@ -3990,6 +4155,17 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "tar" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973" +dependencies = [ + "filetime", + "libc", + "xattr", +] + [[package]] name = "target-lexicon" version = "0.12.16" @@ -4206,6 +4382,39 @@ dependencies = [ "tracing", ] +[[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", + "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.1" @@ -5106,6 +5315,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 = "webpki-roots" version = "1.0.6" @@ -5781,6 +5999,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 = "yoke" version = "0.8.1" @@ -5945,6 +6173,18 @@ dependencies = [ "syn 2.0.117", ] +[[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 a0ab8ce..694edb5 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -25,3 +25,4 @@ chrono = { version = "0.4", features = ["serde"] } reqwest = { version = "0.12", features = ["rustls-tls"], default-features = false } scraper = "0.23" tauri-plugin-fs = "2" +tauri-plugin-updater = "2" diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 0fb4d95..909a36d 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -12,6 +12,7 @@ "store:default", "dialog:default", "fs:default", + "updater:default", { "identifier": "fs:allow-write-file", "allow": [{ "path": "$APPDATA/images" }, { "path": "$APPDATA/images/**" }] diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 589d76f..b695695 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -41,6 +41,7 @@ pub fn run() { .plugin(tauri_plugin_store::Builder::default().build()) .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_fs::init()) + .plugin(tauri_plugin_updater::Builder::default().build()) .setup(|app| { db::init_db(app.handle())?; app.manage(link_preview::LinkPreviewCache(Mutex::new(HashMap::new()))); diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index f5bc026..c4b2a6a 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -28,6 +28,15 @@ "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico" - ] + ], + "createUpdaterArtifacts": true + }, + "plugins": { + "updater": { + "endpoints": [ + "https://github.com/j4rviscmd/Scripta/releases/latest/download/latest.json" + ], + "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDcxOUJGMkIxRjAwOTcwQTIKUldTaWNBbndzZktiY1FWb3lCdFcxU3FDMGFoc1g0ckVMV3BDcWt4WkdaZnp5WnVVR1RMUDNZT2cK" + } } }