diff --git a/cc-haha/desktop/scripts/UPDATER.md b/cc-haha/desktop/scripts/UPDATER.md index 19de145..710b28c 100644 --- a/cc-haha/desktop/scripts/UPDATER.md +++ b/cc-haha/desktop/scripts/UPDATER.md @@ -1,62 +1,96 @@ # Desktop 自动更新发版手册 -> 客户端 UI(`UpdateChecker.tsx` + `updateStore.ts`)已经做好。本文只讲**每次发版**怎么把 manifest 喂进去。 +> 客户端 UI(`UpdateChecker.tsx` + `updateStore.ts`)已经在 AppShell 里挂好。本文只讲**每次发版**怎么把新版本喂进去。 -## 一次性准备 +## 当前已配置的资源 -1. **拿到 minisign 私钥**。`tauri.conf.json` 里的 `pubkey` 对应一把私钥,发版机必须有。如果丢了,重新生成一对并同步更新 `pubkey`,所有已经装在用户机上的旧版本将无法验证新版本(必须手工换包)。 -2. **设置签名环境变量**(每次 build 都要): - ```pwsh - $env:TAURI_SIGNING_PRIVATE_KEY = "" - $env:TAURI_SIGNING_PRIVATE_KEY_PASSWORD = "" - ``` -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//` 占位。 +| 资源 | 值 | +|---|---| +| 更新清单 URL(写在 `tauri.conf.json`) | `https://ashy-dune-0e22d7b00.7.azurestaticapps.net/updater/latest.json` | +| MSI/DMG 托管 | Azure Blob,account `heicodeblob`,容器 `msi`(公共只读) | +| 公共下载 URL 模板 | `https://heicodeblob.blob.core.windows.net/msi/desktop//` | +| Tauri 签名公钥(已在 `tauri.conf.json`) | `RWRkyA8Y1qnKt8KPVg5IDvieсFnv1aoeWMJRZjdRc5r7h41pEmLe3yb/` | +| 私钥位置(本地) | `C:\Users\陈晨\.heicode-updater\heicode_updater.key` | +| 凭证文件 | `scripts/.env.release`(**已 gitignore,不要 commit**) | -## 每次发版 +**重要:私钥丢了等于不能再发更新**。请把 `C:\Users\陈晨\.heicode-updater\heicode_updater.key` 和它的 `.pub` 文件复制一份到云盘/密码管理器/U 盘。 + +## 每次发版(一条命令搞定上传 + 清单) + +### 第 1 步:改版本号 + +编辑两处,保持一致: +- `cc-haha/desktop/src-tauri/tauri.conf.json` → `"version": "0.1.1"` +- `cc-haha/desktop/package.json` → `"version": "0.1.1"` + +### 第 2 步:加载凭证 + 打包 + +PowerShell: +```powershell +cd cc-haha\desktop + +# 加载凭证(私钥路径 + Azure key) +Get-Content scripts\.env.release | ForEach-Object { + if ($_ -match '^([^#=]+)=(.*)$') { + [Environment]::SetEnvironmentVariable($Matches[1].Trim(), $Matches[2].Trim(), 'Process') + } +} + +# 把私钥文件内容塞进 TAURI_SIGNING_PRIVATE_KEY(tauri 期望的是内容而不是路径) +$env:TAURI_SIGNING_PRIVATE_KEY = Get-Content $env:TAURI_SIGNING_PRIVATE_KEY_PATH -Raw -```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 - -# 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` 检查;命中新版本会弹出右上角更新框。 +产物(Windows): +- `src-tauri/target/release/bundle/nsis/heicode_0.1.1_x64-setup.exe` +- `src-tauri/target/release/bundle/nsis/heicode_0.1.1_x64-setup.exe.sig` ← 自动产生的签名 -## 兜底 / 调试 +Mac 类似,产物在 `bundle/macos/`。 -- 手动触发检查:Settings 页面里有"检查更新"按钮(`UpdateChecker` + `updateStore.checkForUpdates`)。 -- 用户主动忽略某个版本:右上框点"稍后",写 localStorage `cc-haha-dismissed-update-version`。下一个版本号会再次弹。 -- 验证 manifest:浏览器直接访问 `https:///updater/latest.json` 应能下载 JSON。 +### 第 3 步:跑发布脚本(上传 + 生成清单) -## 当前状态 +```powershell +node scripts\release-desktop.mjs ` + --notes "0.1.1 修复了 X,新增了 Y" ` + --windows-x86_64 src-tauri\target\release\bundle\nsis\heicode_0.1.1_x64-setup.exe +``` -- ✅ 客户端 UI:完整 -- ✅ `endpoints`:已指向 Azure SWA -- ⏳ `latest.json` 当前只有占位 `platforms: {}`,意味着任何客户端 check 都会返回"已是最新"。第一次走完上面 4 步后才开始真实推送更新。 +脚本会: +1. 把 `.exe` + `.exe.sig` 传到 `https://heicodeblob.blob.core.windows.net/msi/desktop/0.1.1/` +2. 把 `website/public/updater/latest.json` 改写成指向这次的 URL + 嵌入签名 + +mac 同时有产物的话: +```powershell +node scripts\release-desktop.mjs ` + --notes "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 +``` + +### 第 4 步:把清单 push 到 website 触发 Azure SWA 重新部署 + +```powershell +cd ..\..\website +git add public\updater\latest.json +git commit -m "release: desktop 0.1.1" +git push # Azure SWA 自动重新部署,~1 分钟生效 +``` + +部署完后:**所有装着旧版本的用户**下次启动 5 秒后会看到右上角弹窗"有更新可用"。 + +## 怎么验证有没有跑通 + +1. 浏览器打开 `https://ashy-dune-0e22d7b00.7.azurestaticapps.net/updater/latest.json` —— 应能看到 JSON。 +2. 浏览器打开 `platforms.windows-x86_64.url` —— 应能直接下载 MSI。 +3. 装一个比 manifest 里 `version` 旧的 Heicode 客户端,启动 5 秒后看右上角。 + +## 万一出问题 + +- **手动触发检查**:客户端 Settings 页 → 检查更新。 +- **看更新报错**:`updateStore.error` 在 React DevTools 能看到;常见错误:`signature mismatch`(私钥换了但用户客户端里还嵌着旧 pubkey)、`404`(manifest URL 错)。 +- **私钥泄漏 / 想换**:重新跑 `bun x tauri signer generate`,更新 `tauri.conf.json` 的 `pubkey`,重发一次 MSI。**所有装着旧版本的用户都得手动重装这一版**才能继续走自动更新通道。 + +## ⚠ pubkey 已经换过一次 + +本文档创建时,原仓库里的 `pubkey` 已被替换为 `C:\Users\陈晨\.heicode-updater\heicode_updater.key` 对应的公钥。**如果在此之前已经分发过 MSI 给真实用户**,那些用户的客户端**没办法**自动升级到新签名链 —— 必须给他们一个新 MSI 让他们手动安装一次,之后才能继续 OTA。 diff --git a/cc-haha/desktop/scripts/release-desktop.mjs b/cc-haha/desktop/scripts/release-desktop.mjs new file mode 100644 index 0000000..70cb9be --- /dev/null +++ b/cc-haha/desktop/scripts/release-desktop.mjs @@ -0,0 +1,143 @@ +#!/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 = "" + * 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 -- ') +} + +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') diff --git a/cc-haha/desktop/src-tauri/tauri.conf.json b/cc-haha/desktop/src-tauri/tauri.conf.json index 2b53e08..4174b60 100644 --- a/cc-haha/desktop/src-tauri/tauri.conf.json +++ b/cc-haha/desktop/src-tauri/tauri.conf.json @@ -31,7 +31,7 @@ }, "plugins": { "updater": { - "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDlCOUIwRDExQTc5RTFGMzYKUldRMkg1Nm5FUTJibTJ2cGlHY0pkL0dGemxXMUlzc01pVTVMM1U3WGpmWUtrUC8wK2ErSXhLKzEK", + "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEI3Q0FBOUQ2MTgwRkM4NjQKUldSa3lBOFkxcW5LdDhLUFZnNUlEdmllY0ZudjFhb2VXTUpSWmpkUmM1cjdoNDFwRW1MZTN5Yi8K", "endpoints": [ "https://ashy-dune-0e22d7b00.7.azurestaticapps.net/updater/latest.json" ],