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>
This commit is contained in:
@@ -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 <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 ?? '',
|
||||
};
|
||||
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.`);
|
||||
Reference in New Issue
Block a user