feat(desktop,website): wire updater endpoint + release tooling

- tauri.conf.json: endpoints now points at the website-hosted
  manifest (https://<azure-swa>/updater/latest.json) so the
  already-built UpdateChecker actually has somewhere to check.
- website/public/updater/latest.json: placeholder manifest with
  empty platforms map (clients will see "up to date" until a real
  release ships).
- scripts/build-updater-manifest.mjs: helper that ingests signed
  Tauri bundle artifacts and emits the manifest, so future releases
  are one node command instead of hand-edited JSON.
- scripts/UPDATER.md: step-by-step for every release — what to set,
  what to upload, where to commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-12 16:18:51 +08:00
co-authored by Claude Opus 4.7
parent c17be9ef3b
commit 49be79fb9a
4 changed files with 172 additions and 1 deletions
+62
View File
@@ -0,0 +1,62 @@
# Desktop 自动更新发版手册
> 客户端 UI(`UpdateChecker.tsx` + `updateStore.ts`)已经做好。本文只讲**每次发版**怎么把 manifest 喂进去。
## 一次性准备
1. **拿到 minisign 私钥**。`tauri.conf.json` 里的 `pubkey` 对应一把私钥,发版机必须有。如果丢了,重新生成一对并同步更新 `pubkey`,所有已经装在用户机上的旧版本将无法验证新版本(必须手工换包)。
2. **设置签名环境变量**(每次 build 都要):
```pwsh
$env:TAURI_SIGNING_PRIVATE_KEY = "<your minisign private key contents>"
$env:TAURI_SIGNING_PRIVATE_KEY_PASSWORD = "<password if any>"
```
3. **决定 artifact 托管位置**。当前 `endpoints` 指向:
```
https://ashy-dune-0e22d7b00.7.azurestaticapps.net/updater/latest.json
```
manifest 自己住在 website 仓库的 `website/public/updater/latest.json`。
**MSI/DMG 本体不能塞进静态站**(太大 + Azure SWA 有大小限制)—— 需要单独的 CDN:Azure Blob、阿里 OSS、GitHub Release 都行。本文档以 `https://heicode-release.blob.core.windows.net/desktop/<version>/` 占位。
## 每次发版
```pwsh
# 1. 在 cc-haha/desktop 目录打包,签名私钥已经在环境变量里
cd cc-haha/desktop
bun run tauri build
# 产出:
# src-tauri/target/release/bundle/nsis/heicode_X.Y.Z_x64-setup.exe
# src-tauri/target/release/bundle/nsis/heicode_X.Y.Z_x64-setup.exe.sig
# (mac 上).../bundle/macos/Heicode.app.tar.gz + .sig
# 2. 把 .exe/.app.tar.gz 传到 CDN
# 例如 azcopy copy <local> <blob-url>
# 3. 生成 manifest,写到 website 仓库
node scripts/build-updater-manifest.mjs `
--version 0.1.1 `
--notes "本次更新内容..." `
--base-url https://heicode-release.blob.core.windows.net/desktop/0.1.1 `
--windows-x86_64 src-tauri/target/release/bundle/nsis/heicode_0.1.1_x64-setup.exe `
--darwin-aarch64 src-tauri/target/release/bundle/macos/Heicode_aarch64.app.tar.gz `
--out ../../website/public/updater/latest.json
# 4. 提交 website 仓库(manifest 改动)并触发部署
cd ../../website
git add public/updater/latest.json
git commit -m "release: desktop 0.1.1"
git push # Azure SWA 自动重新部署
```
部署完后:旧版本客户端在启动 5 秒后会自动 `silent` 检查;命中新版本会弹出右上角更新框。
## 兜底 / 调试
- 手动触发检查:Settings 页面里有"检查更新"按钮(`UpdateChecker` + `updateStore.checkForUpdates`)。
- 用户主动忽略某个版本:右上框点"稍后",写 localStorage `cc-haha-dismissed-update-version`。下一个版本号会再次弹。
- 验证 manifest:浏览器直接访问 `https://<host>/updater/latest.json` 应能下载 JSON。
## 当前状态
- ✅ 客户端 UI:完整
- ✅ `endpoints`:已指向 Azure SWA
- ⏳ `latest.json` 当前只有占位 `platforms: {}`,意味着任何客户端 check 都会返回"已是最新"。第一次走完上面 4 步后才开始真实推送更新。
@@ -0,0 +1,101 @@
#!/usr/bin/env node
/**
* Build the updater manifest (latest.json) consumed by tauri-plugin-updater.
*
* Usage:
* node scripts/build-updater-manifest.mjs \
* --version 0.1.1 \
* --notes "Bugfix release" \
* --base-url https://your-cdn.example.com/releases/0.1.1 \
* [--windows-x86_64 ./bundle/heicode_0.1.1_x64-setup.exe] \
* [--darwin-aarch64 ./bundle/heicode_0.1.1_aarch64.app.tar.gz] \
* [--darwin-x86_64 ./bundle/heicode_0.1.1_x64.app.tar.gz] \
* [--linux-x86_64 ./bundle/heicode_0.1.1_amd64.AppImage] \
* --out ../../../website/public/updater/latest.json
*
* For each platform path supplied, the script reads the sibling `.sig` file
* that `tauri build` produced (because `createUpdaterArtifacts: true` is
* set in tauri.conf.json) and inlines its contents into the manifest. The
* `url` field is built from `--base-url` + basename of the artifact, so the
* uploader is free to put artifacts wherever — release CDN, Azure Blob,
* GitHub Release assets — as long as `--base-url` matches.
*/
import { readFileSync, writeFileSync, mkdirSync, statSync } from 'node:fs'
import { dirname, basename, resolve } from 'node:path'
import { argv } from 'node:process'
function parseArgs(rawArgs) {
const out = {}
for (let i = 0; i < rawArgs.length; i++) {
const key = rawArgs[i]
if (!key?.startsWith('--')) continue
const k = key.slice(2)
const next = rawArgs[i + 1]
if (!next || next.startsWith('--')) {
out[k] = true
} else {
out[k] = next
i++
}
}
return out
}
const PLATFORM_KEYS = [
'windows-x86_64',
'darwin-aarch64',
'darwin-x86_64',
'linux-x86_64',
]
const args = parseArgs(argv.slice(2))
if (!args.version || !args['base-url'] || !args.out) {
console.error('Required: --version, --base-url, --out')
process.exit(2)
}
const platforms = {}
for (const key of PLATFORM_KEYS) {
const path = args[key]
if (!path || path === true) continue
const resolvedPath = resolve(path)
try {
statSync(resolvedPath)
} catch {
console.error(`Artifact not found for ${key}: ${resolvedPath}`)
process.exit(3)
}
const sigPath = `${resolvedPath}.sig`
let signature
try {
signature = readFileSync(sigPath, 'utf8').trim()
} catch {
console.error(`Signature file missing: ${sigPath}`)
console.error('Run `bun run tauri build` with TAURI_SIGNING_PRIVATE_KEY set.')
process.exit(4)
}
platforms[key] = {
signature,
url: `${args['base-url'].replace(/\/$/, '')}/${basename(resolvedPath)}`,
}
}
if (Object.keys(platforms).length === 0) {
console.error('No platform artifacts supplied — manifest would be empty.')
process.exit(5)
}
const manifest = {
version: args.version,
notes: args.notes || `Heicode ${args.version}`,
pub_date: new Date().toISOString(),
platforms,
}
const outPath = resolve(args.out)
mkdirSync(dirname(outPath), { recursive: true })
writeFileSync(outPath, `${JSON.stringify(manifest, null, 2)}\n`)
console.log(`Wrote ${outPath}`)
console.log(` version = ${manifest.version}`)
console.log(` pub_date = ${manifest.pub_date}`)
console.log(` platforms = ${Object.keys(platforms).join(', ')}`)
+3 -1
View File
@@ -32,7 +32,9 @@
"plugins": {
"updater": {
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDlCOUIwRDExQTc5RTFGMzYKUldRMkg1Nm5FUTJibTJ2cGlHY0pkL0dGemxXMUlzc01pVTVMM1U3WGpmWUtrUC8wK2ErSXhLKzEK",
"endpoints": [],
"endpoints": [
"https://ashy-dune-0e22d7b00.7.azurestaticapps.net/updater/latest.json"
],
"windows": {
"installMode": "passive"
}