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:
@@ -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(', ')}`)
|
||||||
@@ -32,7 +32,9 @@
|
|||||||
"plugins": {
|
"plugins": {
|
||||||
"updater": {
|
"updater": {
|
||||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDlCOUIwRDExQTc5RTFGMzYKUldRMkg1Nm5FUTJibTJ2cGlHY0pkL0dGemxXMUlzc01pVTVMM1U3WGpmWUtrUC8wK2ErSXhLKzEK",
|
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDlCOUIwRDExQTc5RTFGMzYKUldRMkg1Nm5FUTJibTJ2cGlHY0pkL0dGemxXMUlzc01pVTVMM1U3WGpmWUtrUC8wK2ErSXhLKzEK",
|
||||||
"endpoints": [],
|
"endpoints": [
|
||||||
|
"https://ashy-dune-0e22d7b00.7.azurestaticapps.net/updater/latest.json"
|
||||||
|
],
|
||||||
"windows": {
|
"windows": {
|
||||||
"installMode": "passive"
|
"installMode": "passive"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"version": "0.1.0",
|
||||||
|
"notes": "Initial Heicode desktop release. Future versions will populate this manifest automatically.",
|
||||||
|
"pub_date": "2026-05-12T00:00:00Z",
|
||||||
|
"platforms": {}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user