Files
ChatApp/scripts/release.mjs
T
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

159 lines
5.4 KiB
JavaScript

#!/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.`);