Compare commits

..

5 Commits

Author SHA1 Message Date
byGalax 6301ebb392 chore(desktop): release v0.10.2 2026-04-22 19:24:14 +02:00
byGalax 31d21dd2c2 fix(release): tauri v2 ships .exe + .exe.sig, not .nsis.zip
v1 used to wrap the installer in a .nsis.zip and sign that wrapper.
v2 signs the .exe directly, so the updater url points at the .exe and
the .sig file sits next to it. Script was still looking for the
legacy .nsis.zip path and aborting after a successful build.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 19:22:17 +02:00
byGalax 9add0a4d61 fix(release): call tauri binary directly, not via desktop build script
Node execSync runs via cmd.exe on Windows, which preserves the `--`
separator pnpm injects between the script name and forwarded args.
Tauri CLI then forwards that `--` to cargo, which rejects `--bundles`
with "unexpected argument". Invoking `pnpm exec tauri build --bundles nsis`
skips the script indirection so no `--` is emitted.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 19:19:42 +02:00
byGalax 500f1c4bc2 chore(release): self-hosted updater on update.netralax.cloud
Switches the Tauri updater endpoint from GitHub Releases to a static
host. New Ed25519 pubkey (old private key was lost); existing 0.10.x
installs need one manual reinstall to pick up the new updater identity.

Release flow is now pnpm release <version> <notes> which bumps,
builds + signs locally, scps artifacts to the server, commits, tags.
GitHub workflow stays as workflow_dispatch backup (Windows only).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 19:17:58 +02:00
byGalax a38e2f96c0 feat(desktop): raise ringtone cap to 8 MB
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 19:17:48 +02:00
12 changed files with 210 additions and 38 deletions
+21
View File
@@ -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
+16 -28
View File
@@ -1,35 +1,27 @@
name: Release desktop app name: Release desktop app (manual backup)
# Tag a version to trigger a release: # Self-hosted updates run from scripts/release.mjs on Dennis's Windows box.
# git tag v0.1.0 && git push --tags # This workflow is kept as a manual backup — trigger it from the Actions tab
# # if the local build host is unavailable.
# 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.
on: on:
push: workflow_dispatch:
tags: inputs:
- "v*" tag:
description: "Tag to build (e.g. v0.10.2) — must already exist"
required: true
permissions: permissions:
contents: write contents: write
jobs: jobs:
build: build:
strategy: runs-on: windows-latest
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 }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with:
ref: ${{ inputs.tag }}
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
@@ -42,8 +34,6 @@ jobs:
- name: Setup Rust - name: Setup Rust
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.platform == 'macos-14' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
- name: Install JS deps - name: Install JS deps
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
@@ -54,18 +44,16 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} 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_URL: ${{ secrets.VITE_SUPABASE_URL }}
VITE_SUPABASE_ANON_KEY: ${{ secrets.VITE_SUPABASE_ANON_KEY }} VITE_SUPABASE_ANON_KEY: ${{ secrets.VITE_SUPABASE_ANON_KEY }}
VITE_AUTH_REDIRECT_URL: ${{ secrets.VITE_AUTH_REDIRECT_URL }} VITE_AUTH_REDIRECT_URL: ${{ secrets.VITE_AUTH_REDIRECT_URL }}
VITE_LIVEKIT_URL: ${{ secrets.VITE_LIVEKIT_URL }} VITE_LIVEKIT_URL: ${{ secrets.VITE_LIVEKIT_URL }}
with: with:
projectPath: apps/desktop projectPath: apps/desktop
tagName: ${{ github.ref_name }} tagName: ${{ inputs.tag }}
releaseName: "ChatApp ${{ github.ref_name }}" releaseName: "ChatApp ${{ inputs.tag }}"
releaseBody: "See the assets below to download this version." releaseBody: "Manual build — copy .nsis.zip/.sig/latest.json to the update host."
releaseDraft: true releaseDraft: true
prerelease: false prerelease: false
tauriScript: pnpm exec tauri tauriScript: pnpm exec tauri
args: ${{ matrix.args }} args: "--bundles nsis"
+2
View File
@@ -15,7 +15,9 @@ out/
.env .env
.env.local .env.local
.env.*.local .env.*.local
.env.release
!.env.example !.env.example
!.env.release.example
# Expo # Expo
.expo/ .expo/
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@chat-app/desktop", "name": "@chat-app/desktop",
"version": "0.10.1", "version": "0.10.2",
"private": true, "private": true,
"description": "Tauri v2 desktop client (Windows / macOS / Linux)", "description": "Tauri v2 desktop client (Windows / macOS / Linux)",
"type": "module", "type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "chat-app-desktop" name = "chat-app-desktop"
version = "0.10.1" version = "0.10.2"
description = "ChatApp desktop client" description = "ChatApp desktop client"
authors = ["Dennis"] authors = ["Dennis"]
edition = "2021" edition = "2021"
+5 -3
View File
@@ -1,7 +1,7 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "ChatApp", "productName": "ChatApp",
"version": "0.10.1", "version": "0.10.2",
"identifier": "com.meinname.chatapp", "identifier": "com.meinname.chatapp",
"build": { "build": {
"beforeDevCommand": "pnpm vite:dev", "beforeDevCommand": "pnpm vite:dev",
@@ -42,8 +42,10 @@
}, },
"plugins": { "plugins": {
"updater": { "updater": {
"endpoints": ["https://github.com/byGalax/chat-app/releases/latest/download/latest.json"], "endpoints": [
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDQ5N0U0RDcxOTU2OEQ0QUUKUldTdTFHaVZjVTErU1ZuMk1lWXBUbEcyS1RHYzJQN3k4VDdiUGRvRnVJYVJKR3BxWG1xcENpdlYK", "https://update.netralax.cloud/windows/latest.json"
],
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEI1Mzc0QjVBQUZEQTA3RUIKUldUckI5cXZXa3MzdGM3QkE4WWFPd3NnVzRZeXdpcUM0eUtjRDlGN09ySEdzNXhLNlo3azBPajYK",
"windows": { "windows": {
"installMode": "passive" "installMode": "passive"
} }
@@ -82,7 +82,7 @@ export function RingtoneSettings({ disabled = false }: Props) {
if (code === 'ringtone_too_large') { if (code === 'ringtone_too_large') {
setError( setError(
t('app:settings.ringtone_error_too_large', { t('app:settings.ringtone_error_too_large', {
defaultValue: 'Datei zu groß (max 2 MB).', defaultValue: 'Datei zu groß (max {{max}} MB).',
max: MAX_RINGTONE_BYTES / BYTES_PER_MB, max: MAX_RINGTONE_BYTES / BYTES_PER_MB,
}), }),
); );
@@ -236,7 +236,7 @@ export function RingtoneSettings({ disabled = false }: Props) {
<p className="text-[11px] text-fg-muted"> <p className="text-[11px] text-fg-muted">
{t('app:settings.ringtone_hint', { {t('app:settings.ringtone_hint', {
defaultValue: defaultValue:
'MP3, WAV, OGG oder M4A bis 2 MB. Nur für eingehende Anrufe — ausgehend bleibt der Standard.', 'MP3, WAV, OGG oder M4A bis 8 MB. Nur für eingehende Anrufe — ausgehend bleibt der Standard.',
})} })}
</p> </p>
</div> </div>
+1 -1
View File
@@ -16,7 +16,7 @@ export interface StoredRingtone {
updatedAt: number; updatedAt: number;
} }
export const MAX_RINGTONE_BYTES = 2 * 1024 * 1024; // 2 MB cap export const MAX_RINGTONE_BYTES = 8 * 1024 * 1024; // 8 MB cap
export const SUPPORTED_RINGTONE_MIMES = [ export const SUPPORTED_RINGTONE_MIMES = [
'audio/mpeg', 'audio/mpeg',
+1
View File
@@ -26,6 +26,7 @@
"desktop": "pnpm --filter @chat-app/desktop", "desktop": "pnpm --filter @chat-app/desktop",
"desktop:dev": "pnpm --filter @chat-app/desktop dev", "desktop:dev": "pnpm --filter @chat-app/desktop dev",
"desktop:build": "pnpm --filter @chat-app/desktop build", "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", "db:types": "supabase gen types typescript --local > packages/db-types/src/index.ts",
"prod:migrate": "./scripts/prod/push-migrations.sh", "prod:migrate": "./scripts/prod/push-migrations.sh",
"prod:deploy-fn": "./scripts/prod/push-edge-function.sh", "prod:deploy-fn": "./scripts/prod/push-edge-function.sh",
+1 -1
View File
@@ -205,7 +205,7 @@
"ringtone_preview": "Vorhören", "ringtone_preview": "Vorhören",
"ringtone_stop": "Stop", "ringtone_stop": "Stop",
"ringtone_reset": "Zurücksetzen", "ringtone_reset": "Zurücksetzen",
"ringtone_hint": "MP3, WAV, OGG oder M4A bis 2 MB. Nur für eingehende Anrufe — ausgehend bleibt der Standard.", "ringtone_hint": "MP3, WAV, OGG oder M4A bis 8 MB. Nur für eingehende Anrufe — ausgehend bleibt der Standard.",
"ringtone_error_too_large": "Datei zu groß (max {{max}} MB).", "ringtone_error_too_large": "Datei zu groß (max {{max}} MB).",
"ringtone_error_not_audio": "Nur Audio-Dateien werden unterstützt.", "ringtone_error_not_audio": "Nur Audio-Dateien werden unterstützt.",
"ringtone_error_generic": "Ringtone konnte nicht gespeichert werden.", "ringtone_error_generic": "Ringtone konnte nicht gespeichert werden.",
+1 -1
View File
@@ -205,7 +205,7 @@
"ringtone_preview": "Preview", "ringtone_preview": "Preview",
"ringtone_stop": "Stop", "ringtone_stop": "Stop",
"ringtone_reset": "Reset", "ringtone_reset": "Reset",
"ringtone_hint": "MP3, WAV, OGG or M4A up to 2 MB. Incoming calls only — outgoing keeps the default.", "ringtone_hint": "MP3, WAV, OGG or M4A up to 8 MB. Incoming calls only — outgoing keeps the default.",
"ringtone_error_too_large": "File too large (max {{max}} MB).", "ringtone_error_too_large": "File too large (max {{max}} MB).",
"ringtone_error_not_audio": "Only audio files are supported.", "ringtone_error_not_audio": "Only audio files are supported.",
"ringtone_error_generic": "Could not save the ringtone.", "ringtone_error_generic": "Could not save the ringtone.",
+158
View File
@@ -0,0 +1,158 @@
#!/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 <x.y.z> "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 ?? '',
};
// Call the tauri binary directly (not via the desktop `build` script) so the
// `--` separator pnpm normally injects doesn't get forwarded to cargo. When
// run from cmd.exe (Node's default execSync shell on Windows) pnpm preserves
// the `--`, which cargo then rejects with "unexpected argument '--bundles'".
execSync('pnpm --filter @chat-app/desktop exec tauri build --bundles nsis', {
cwd: ROOT,
env: buildEnv,
stdio: 'inherit',
});
// Tauri v2 ships a single .exe + .exe.sig for NSIS updates — no .nsis.zip
// wrapper like v1. The updater downloads the .exe directly, verifies the
// minisign signature, then launches it in passive mode.
const bundleDir = join(ROOT, 'apps/desktop/src-tauri/target/release/bundle/nsis');
const exeName = `ChatApp_${versionArg}_x64-setup.exe`;
const sigName = `${exeName}.sig`;
const exePath = join(bundleDir, exeName);
const sigPath = join(bundleDir, sigName);
for (const p of [exePath, 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/${exeName}`,
},
},
};
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(exePath);
scp(sigPath);
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.`);