Files
heicode-win/cc-haha/desktop/scripts/release-desktop.mjs
T
chenchenandClaude Opus 4.7 739354e786 feat(desktop): rotate updater key + concrete release pipeline
- pubkey: rotated to the keypair stored at
  C:\Users\陈晨\.heicode-updater\heicode_updater.key (private side
  is the user's; only the pubkey ships in tauri.conf.json).
- scripts/release-desktop.mjs: one-shot release helper —
  uploads the signed bundle artifacts to Azure Blob (account
  heicodeblob, container msi, public-blob-read) and rewrites
  website/public/updater/latest.json to point at the new URLs.
- UPDATER.md: rewritten with the concrete URLs, container, key
  paths, and step-by-step commands. No more generic placeholders.

Azure Blob setup (done out-of-band, not in this commit):
- Storage account heicodeblob set allowBlobPublicAccess=true
- Container msi set to public-blob read
- Smoke-tested: https://heicodeblob.blob.core.windows.net/msi/<x>
  returns 200 anonymously.

The actual Azure connection string + private-key path live in
scripts/.env.release, which is gitignored under .env.* and was
verified excluded before this commit.

NOTE: pubkey was rotated. Any MSI already in the wild signed by
the *previous* key cannot self-update to this signing chain —
those users need a fresh manual install. This is acceptable for
pre-GA where no public release exists yet.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 16:50:14 +08:00

144 lines
4.6 KiB
JavaScript

#!/usr/bin/env node
/**
* End-to-end desktop release helper.
*
* 1. Reads the version from src-tauri/tauri.conf.json (you bumped it
* and rebuilt before running this).
* 2. Uploads every supplied bundle artifact + its `.sig` to the
* Azure Blob container `msi` (account `heicodeblob`). Container
* is configured public-blob-read, so the URLs are durable.
* 3. Generates `website/public/updater/latest.json` pointing at
* those URLs and inlining the signatures.
*
* After this script runs you still need to `git commit && git push`
* the website manifest so the static site redeploys.
*
* Usage (PowerShell example):
*
* $env:AZURE_STORAGE_CONNECTION_STRING = "<conn-str>"
* node scripts/release-desktop.mjs `
* --notes "0.1.1 — bug fixes" `
* --windows-x86_64 src-tauri/target/release/bundle/nsis/heicode_0.1.1_x64-setup.exe
*
* Tip: the connection string lives in scripts/.release-credentials
* locally (gitignored). `source` it before running on Mac/Linux, or
* use `Get-Content | ForEach-Object {...}` on Windows.
*/
import {
readFileSync, statSync, mkdirSync, writeFileSync, existsSync,
} from 'node:fs'
import { dirname, basename, resolve, join } from 'node:path'
import { argv, env, exit } from 'node:process'
import { execSync, spawnSync } from 'node:child_process'
const REPO_ROOT = resolve(import.meta.dirname, '..', '..', '..')
const CONTAINER = 'msi'
const ACCOUNT = 'heicodeblob'
const PUBLIC_HOST = `https://${ACCOUNT}.blob.core.windows.net/${CONTAINER}`
const MANIFEST_REL = 'website/public/updater/latest.json'
const TAURI_CONF = resolve(import.meta.dirname, '..', 'src-tauri', 'tauri.conf.json')
const PLATFORM_KEYS = [
'windows-x86_64',
'darwin-aarch64',
'darwin-x86_64',
'linux-x86_64',
]
function parseArgs(rawArgs) {
const out = {}
for (let i = 0; i < rawArgs.length; i++) {
const k = rawArgs[i]
if (!k?.startsWith('--')) continue
const next = rawArgs[i + 1]
if (!next || next.startsWith('--')) {
out[k.slice(2)] = true
} else {
out[k.slice(2)] = next
i++
}
}
return out
}
function die(msg, code = 1) {
console.error(`error: ${msg}`)
exit(code)
}
function getTauriVersion() {
const json = JSON.parse(readFileSync(TAURI_CONF, 'utf8'))
if (!json.version) die('tauri.conf.json has no version')
return json.version
}
function uploadBlob(localPath, remoteName) {
if (!env.AZURE_STORAGE_CONNECTION_STRING) {
die('AZURE_STORAGE_CONNECTION_STRING is not set (see scripts/.release-credentials)')
}
const result = spawnSync(
'az',
[
'storage', 'blob', 'upload',
'--container-name', CONTAINER,
'--name', remoteName,
'--file', localPath,
'--overwrite',
'--no-progress',
],
{ stdio: ['ignore', 'pipe', 'pipe'], env, encoding: 'utf8' },
)
if (result.status !== 0) {
console.error(result.stderr)
die(`upload failed for ${remoteName}`)
}
return `${PUBLIC_HOST}/${remoteName}`
}
const args = parseArgs(argv.slice(2))
const version = getTauriVersion()
const versionPrefix = `desktop/${version}`
const platforms = {}
for (const key of PLATFORM_KEYS) {
const path = args[key]
if (!path || path === true) continue
const local = resolve(path)
try { statSync(local) } catch { die(`${key} artifact missing: ${local}`) }
const sigLocal = `${local}.sig`
let signature
try { signature = readFileSync(sigLocal, 'utf8').trim() }
catch { die(`signature missing: ${sigLocal} (rebuild with TAURI_SIGNING_PRIVATE_KEY set)`) }
const remoteArtifact = `${versionPrefix}/${basename(local)}`
const remoteSig = `${versionPrefix}/${basename(sigLocal)}`
console.log(`Uploading ${key}: ${remoteArtifact}`)
const url = uploadBlob(local, remoteArtifact)
uploadBlob(sigLocal, remoteSig)
platforms[key] = { signature, url }
}
if (Object.keys(platforms).length === 0) {
die('no platform artifacts supplied — pass at least one --<target> <path>')
}
const manifest = {
version,
notes: args.notes || `Heicode ${version}`,
pub_date: new Date().toISOString(),
platforms,
}
const manifestPath = resolve(REPO_ROOT, MANIFEST_REL)
if (!existsSync(dirname(manifestPath))) mkdirSync(dirname(manifestPath), { recursive: true })
writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`)
console.log(`\nWrote ${manifestPath}`)
console.log(`Version : ${version}`)
console.log(`Platforms: ${Object.keys(platforms).join(', ')}`)
console.log('\nNext: commit + push website to redeploy Azure SWA.')
console.log(` cd ${join(REPO_ROOT, 'website')}`)
console.log(` git add public/updater/latest.json`)
console.log(` git commit -m "release: desktop ${version}"`)
console.log(' git push')