Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6301ebb392 | |||
| 31d21dd2c2 | |||
| 9add0a4d61 | |||
| 500f1c4bc2 | |||
| a38e2f96c0 | |||
| 5aa39b40ff | |||
| eb452bf57e |
@@ -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
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@chat-app/desktop",
|
"name": "@chat-app/desktop",
|
||||||
"version": "0.10.0",
|
"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",
|
||||||
|
|||||||
Generated
+1
-1
@@ -726,7 +726,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chat-app-desktop"
|
name = "chat-app-desktop"
|
||||||
version = "0.10.0"
|
version = "0.10.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"dryoc",
|
"dryoc",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "chat-app-desktop"
|
name = "chat-app-desktop"
|
||||||
version = "0.10.0"
|
version = "0.10.2"
|
||||||
description = "ChatApp desktop client"
|
description = "ChatApp desktop client"
|
||||||
authors = ["Dennis"]
|
authors = ["Dennis"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|||||||
@@ -22,6 +22,33 @@ struct TrayUnreadPayload {
|
|||||||
count: u32,
|
count: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Red-dot overlay icon for the Windows taskbar. Drawn as raw RGBA instead of
|
||||||
|
// shipping a PNG so we don't add another resource to the bundle. Kept small
|
||||||
|
// (32x32) since Windows scales the overlay down anyway.
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
fn unread_overlay_rgba() -> Vec<u8> {
|
||||||
|
const SIZE: u32 = 32;
|
||||||
|
let r = SIZE as f32 / 2.0;
|
||||||
|
let mut buf = Vec::with_capacity((SIZE * SIZE * 4) as usize);
|
||||||
|
for y in 0..SIZE {
|
||||||
|
for x in 0..SIZE {
|
||||||
|
let dx = x as f32 - r + 0.5;
|
||||||
|
let dy = y as f32 - r + 0.5;
|
||||||
|
let d = (dx * dx + dy * dy).sqrt();
|
||||||
|
let edge = r - 1.0;
|
||||||
|
if d <= edge {
|
||||||
|
buf.extend_from_slice(&[0xDC, 0x26, 0x26, 0xFF]);
|
||||||
|
} else if d <= r {
|
||||||
|
let alpha = (255.0 * (r - d)).clamp(0.0, 255.0) as u8;
|
||||||
|
buf.extend_from_slice(&[0xDC, 0x26, 0x26, alpha]);
|
||||||
|
} else {
|
||||||
|
buf.extend_from_slice(&[0, 0, 0, 0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buf
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
fn show_main_window(app: &AppHandle) {
|
fn show_main_window(app: &AppHandle) {
|
||||||
if let Some(win) = app.get_webview_window("main") {
|
if let Some(win) = app.get_webview_window("main") {
|
||||||
@@ -171,8 +198,10 @@ pub fn run() {
|
|||||||
format!("ChatApp · {} neu", payload.count)
|
format!("ChatApp · {} neu", payload.count)
|
||||||
};
|
};
|
||||||
let _ = tray_handle.set_tooltip(Some(tooltip));
|
let _ = tray_handle.set_tooltip(Some(tooltip));
|
||||||
// macOS dock badge. `set_badge_label` is macOS-only but the
|
// Dock/taskbar badge. macOS uses a numeric label; Windows uses
|
||||||
// call is a no-op on other platforms so we don't need a cfg.
|
// an overlay icon (red dot = unread). Linux has no cross-DE
|
||||||
|
// badge API — skip.
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
if let Some(win) = badge_window.as_ref() {
|
if let Some(win) = badge_window.as_ref() {
|
||||||
let badge = if payload.count == 0 {
|
let badge = if payload.count == 0 {
|
||||||
None
|
None
|
||||||
@@ -181,6 +210,18 @@ pub fn run() {
|
|||||||
};
|
};
|
||||||
let _ = win.set_badge_label(badge);
|
let _ = win.set_badge_label(badge);
|
||||||
}
|
}
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
if let Some(win) = badge_window.as_ref() {
|
||||||
|
if payload.count == 0 {
|
||||||
|
let _ = win.set_overlay_icon(None);
|
||||||
|
} else {
|
||||||
|
let rgba = unread_overlay_rgba();
|
||||||
|
let img = tauri::image::Image::new_owned(rgba, 32, 32);
|
||||||
|
let _ = win.set_overlay_icon(Some(img));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
|
||||||
|
let _ = &badge_window;
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -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.0",
|
"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>
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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.",
|
||||||
|
|||||||
@@ -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.",
|
||||||
|
|||||||
@@ -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.`);
|
||||||
Reference in New Issue
Block a user