diff --git a/.env.release.example b/.env.release.example new file mode 100644 index 0000000..59c4251 --- /dev/null +++ b/.env.release.example @@ -0,0 +1,21 @@ +# Copy to .env.release (gitignored) and fill in. +# Consumed by scripts/release.mjs. + +# Absolute path to the private key file produced by `tauri signer generate`. +TAURI_SIGNING_PRIVATE_KEY_PATH=C:/Users/denni/.tauri/chatapp.key + +# Password set when generating the key. Leave empty if none. +TAURI_SIGNING_PRIVATE_KEY_PASSWORD= + +# Host serving latest.json + installer artifacts over HTTPS. +UPDATE_HOST=update.netralax.cloud + +# SSH user on UPDATE_HOST with write access to UPDATE_REMOTE_PATH. +UPDATE_SSH_USER=chatapp-deploy + +# Optional: path to the SSH private key. Omit to fall back on ssh-agent or the +# default id_rsa. +UPDATE_SSH_KEY= + +# Absolute path on the server where windows/ artifacts + latest.json live. +UPDATE_REMOTE_PATH=/var/www/updates/windows diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c92b942..92084e3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,35 +1,27 @@ -name: Release desktop app +name: Release desktop app (manual backup) -# Tag a version to trigger a release: -# git tag v0.1.0 && git push --tags -# -# Produces signed Tauri bundles for macOS (arm + intel), Windows, and Linux, -# uploads them to a GitHub Release, and publishes `latest.json` for the -# updater plugin to discover. +# Self-hosted updates run from scripts/release.mjs on Dennis's Windows box. +# This workflow is kept as a manual backup — trigger it from the Actions tab +# if the local build host is unavailable. on: - push: - tags: - - "v*" + workflow_dispatch: + inputs: + tag: + description: "Tag to build (e.g. v0.10.2) — must already exist" + required: true permissions: contents: write jobs: build: - strategy: - fail-fast: false - matrix: - include: - - platform: macos-14 # universal binary covers both Intel + Apple Silicon - args: "--target universal-apple-darwin --bundles app,updater" - - platform: windows-latest - args: "" - - runs-on: ${{ matrix.platform }} + runs-on: windows-latest steps: - uses: actions/checkout@v4 + with: + ref: ${{ inputs.tag }} - name: Install pnpm uses: pnpm/action-setup@v4 @@ -42,8 +34,6 @@ jobs: - name: Setup Rust uses: dtolnay/rust-toolchain@stable - with: - targets: ${{ matrix.platform == 'macos-14' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }} - name: Install JS deps run: pnpm install --frozen-lockfile @@ -54,18 +44,16 @@ jobs: 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 }} - # Client-side env vars baked into the bundle — paste your prod values - # into the repo's Actions → Secrets so releases point at prod. VITE_SUPABASE_URL: ${{ secrets.VITE_SUPABASE_URL }} VITE_SUPABASE_ANON_KEY: ${{ secrets.VITE_SUPABASE_ANON_KEY }} VITE_AUTH_REDIRECT_URL: ${{ secrets.VITE_AUTH_REDIRECT_URL }} VITE_LIVEKIT_URL: ${{ secrets.VITE_LIVEKIT_URL }} with: projectPath: apps/desktop - tagName: ${{ github.ref_name }} - releaseName: "ChatApp ${{ github.ref_name }}" - releaseBody: "See the assets below to download this version." + tagName: ${{ inputs.tag }} + releaseName: "ChatApp ${{ inputs.tag }}" + releaseBody: "Manual build — copy .nsis.zip/.sig/latest.json to the update host." releaseDraft: true prerelease: false tauriScript: pnpm exec tauri - args: ${{ matrix.args }} + args: "--bundles nsis" diff --git a/.gitignore b/.gitignore index b19390a..8cd7a74 100644 --- a/.gitignore +++ b/.gitignore @@ -15,7 +15,9 @@ out/ .env .env.local .env.*.local +.env.release !.env.example +!.env.release.example # Expo .expo/ diff --git a/apps/desktop/src-tauri/tauri.conf.json b/apps/desktop/src-tauri/tauri.conf.json index 8de286a..8413892 100644 --- a/apps/desktop/src-tauri/tauri.conf.json +++ b/apps/desktop/src-tauri/tauri.conf.json @@ -42,8 +42,8 @@ }, "plugins": { "updater": { - "endpoints": ["https://github.com/byGalax/chat-app/releases/latest/download/latest.json"], - "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDQ5N0U0RDcxOTU2OEQ0QUUKUldTdTFHaVZjVTErU1ZuMk1lWXBUbEcyS1RHYzJQN3k4VDdiUGRvRnVJYVJKR3BxWG1xcENpdlYK", + "endpoints": ["https://update.netralax.cloud/windows/latest.json"], + "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEI1Mzc0QjVBQUZEQTA3RUIKUldUckI5cXZXa3MzdGM3QkE4WWFPd3NnVzRZeXdpcUM0eUtjRDlGN09ySEdzNXhLNlo3azBPajYK", "windows": { "installMode": "passive" } diff --git a/package.json b/package.json index 0641bbb..fe060aa 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "desktop": "pnpm --filter @chat-app/desktop", "desktop:dev": "pnpm --filter @chat-app/desktop dev", "desktop:build": "pnpm --filter @chat-app/desktop build", + "release": "node scripts/release.mjs", "db:types": "supabase gen types typescript --local > packages/db-types/src/index.ts", "prod:migrate": "./scripts/prod/push-migrations.sh", "prod:deploy-fn": "./scripts/prod/push-edge-function.sh", diff --git a/scripts/release.mjs b/scripts/release.mjs new file mode 100644 index 0000000..19d356f --- /dev/null +++ b/scripts/release.mjs @@ -0,0 +1,156 @@ +#!/usr/bin/env node +// Release script — builds the Windows desktop installer, signs it with the +// Tauri updater key, and uploads the artifacts + latest.json to the update +// host over scp. Reads credentials from .env.release (not committed). +// +// Usage: +// pnpm release 0.10.2 "Ringtone cap auf 8 MB, bugfixes" + +import { execSync } from 'node:child_process'; +import { existsSync, readFileSync, writeFileSync } from 'node:fs'; +import { join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const ROOT = resolve(fileURLToPath(new URL('.', import.meta.url)), '..'); + +const [, , versionArg, ...notesParts] = process.argv; +if (!versionArg || !/^\d+\.\d+\.\d+$/.test(versionArg)) { + console.error('Usage: pnpm release "release notes"'); + process.exit(1); +} +const notes = notesParts.join(' ').trim() || `Release ${versionArg}`; + +const envPath = join(ROOT, '.env.release'); +if (!existsSync(envPath)) { + console.error('.env.release missing. Copy .env.release.example and fill it in.'); + process.exit(1); +} +const env = Object.fromEntries( + readFileSync(envPath, 'utf8') + .split('\n') + .map((l) => l.trim()) + .filter((l) => l && !l.startsWith('#')) + .map((l) => { + const i = l.indexOf('='); + return [l.slice(0, i).trim(), l.slice(i + 1).trim()]; + }), +); + +const required = [ + 'TAURI_SIGNING_PRIVATE_KEY_PATH', + 'UPDATE_HOST', + 'UPDATE_SSH_USER', + 'UPDATE_REMOTE_PATH', +]; +for (const key of required) { + if (!env[key]) { + console.error(`Missing ${key} in .env.release`); + process.exit(1); + } +} +if (!existsSync(env.TAURI_SIGNING_PRIVATE_KEY_PATH)) { + console.error(`Signing key not found at ${env.TAURI_SIGNING_PRIVATE_KEY_PATH}`); + process.exit(1); +} + +const gitStatus = execSync('git status --porcelain', { cwd: ROOT, encoding: 'utf8' }); +if (gitStatus.trim()) { + console.error('Working tree not clean. Commit or stash first.'); + console.error(gitStatus); + process.exit(1); +} + +const pkgJsonPath = join(ROOT, 'apps/desktop/package.json'); +const tauriConfPath = join(ROOT, 'apps/desktop/src-tauri/tauri.conf.json'); +const cargoTomlPath = join(ROOT, 'apps/desktop/src-tauri/Cargo.toml'); + +function bumpJson(path, version) { + const obj = JSON.parse(readFileSync(path, 'utf8')); + obj.version = version; + writeFileSync(path, JSON.stringify(obj, null, 2) + '\n', 'utf8'); +} +function bumpCargo(path, version) { + const text = readFileSync(path, 'utf8'); + const next = text.replace(/^version = "[^"]+"$/m, `version = "${version}"`); + if (next === text) throw new Error(`No version line found in ${path}`); + writeFileSync(path, next, 'utf8'); +} + +bumpJson(pkgJsonPath, versionArg); +bumpJson(tauriConfPath, versionArg); +bumpCargo(cargoTomlPath, versionArg); + +console.log(`Version -> ${versionArg}. Building NSIS bundle…`); + +const signingKey = readFileSync(env.TAURI_SIGNING_PRIVATE_KEY_PATH, 'utf8').trim(); +const buildEnv = { + ...process.env, + TAURI_SIGNING_PRIVATE_KEY: signingKey, + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: env.TAURI_SIGNING_PRIVATE_KEY_PASSWORD ?? '', +}; +execSync('pnpm --filter @chat-app/desktop build -- --bundles nsis', { + cwd: ROOT, + env: buildEnv, + stdio: 'inherit', +}); + +const bundleDir = join(ROOT, 'apps/desktop/src-tauri/target/release/bundle/nsis'); +const zipName = `ChatApp_${versionArg}_x64-setup.nsis.zip`; +const exeName = `ChatApp_${versionArg}_x64-setup.exe`; +const sigName = `${zipName}.sig`; +const zipPath = join(bundleDir, zipName); +const exePath = join(bundleDir, exeName); +const sigPath = join(bundleDir, sigName); +for (const p of [zipPath, sigPath]) { + if (!existsSync(p)) { + console.error(`Missing build artifact: ${p}`); + process.exit(1); + } +} + +const signature = readFileSync(sigPath, 'utf8').trim(); +const latest = { + version: versionArg, + notes, + pub_date: new Date().toISOString(), + platforms: { + 'windows-x86_64': { + signature, + url: `https://${env.UPDATE_HOST}/windows/${zipName}`, + }, + }, +}; +const latestJsonPath = join(bundleDir, 'latest.json'); +writeFileSync(latestJsonPath, JSON.stringify(latest, null, 2), 'utf8'); + +const sshTarget = `${env.UPDATE_SSH_USER}@${env.UPDATE_HOST}`; +const keyFlag = env.UPDATE_SSH_KEY ? ` -i "${env.UPDATE_SSH_KEY}"` : ''; +function scp(localPath) { + execSync(`scp${keyFlag} "${localPath}" ${sshTarget}:${env.UPDATE_REMOTE_PATH}/`, { + cwd: ROOT, + stdio: 'inherit', + }); +} + +console.log('Uploading artifacts (JSON last so clients never see a stale ref)…'); +scp(zipPath); +scp(sigPath); +// Also ship the bare installer so friends can download it directly via browser +// without going through the updater — useful for the very first install. +if (existsSync(exePath)) scp(exePath); +scp(latestJsonPath); + +execSync( + `git add apps/desktop/package.json apps/desktop/src-tauri/tauri.conf.json apps/desktop/src-tauri/Cargo.toml`, + { cwd: ROOT, stdio: 'inherit' }, +); +execSync(`git commit -m "chore(desktop): release v${versionArg}"`, { + cwd: ROOT, + stdio: 'inherit', +}); +execSync(`git tag v${versionArg}`, { cwd: ROOT, stdio: 'inherit' }); + +console.log(`\nReleased v${versionArg}`); +console.log(` Manifest: https://${env.UPDATE_HOST}/windows/latest.json`); +console.log(` Installer: https://${env.UPDATE_HOST}/windows/${exeName}`); +console.log(` Run 'git push && git push --tags' to sync to remote.`);