Docker Build and Push / build-image (push) Successful in 1m5s
- check-version: 分组校验 (前端 package.json / 外壳 tauri.conf+Cargo), 两组不再要求相等; 分支前缀校验只针对前端版本 - build-ota: build_id 改为 YYYYMMDDHHmmss (北京时间, 与前端 BUILD_ID 同格式); sequence 保持毫秒仅用于判新 - 我的页: 当前版本恢复 v0.2.0 · build <原始BUILD_ID>; 新版本附 OTA build_id; 底部替换为外壳版本 (Tauri getVersion, Web 端 —) - README: 合并遗留 readme 全部内容并按决议重写版本管理章节; 新增 CHANGELOG.md (PATCH 仅明确提出时自增)
156 lines
6.5 KiB
JavaScript
156 lines
6.5 KiB
JavaScript
#!/usr/bin/env node
|
|
// OTA 升级包生成脚本 (兼容 tauri-plugin-hotswap)
|
|
// 用法:
|
|
// node scripts/build-ota.mjs [--out <dir>] [--sequence <n>] [--min-binary <x>] [--notes <s>] [--no-sign] [--key <path>]
|
|
// 流程:
|
|
// 1) 校验 dist/ 已构建
|
|
// 2) 生成 bundle.tar.gz (dist/ 内容)
|
|
// 3) minisign 签名 (除非 --no-sign; 私钥优先读 MINISIGN_PRIVATE_KEY 环境变量)
|
|
// 4) 写 latest.json (manifest, 含 sequence/version/signature/bundle_size/notes/...)
|
|
// 5) 全部产物输出到 out 目录 (默认 ota-package/)
|
|
//
|
|
// minisign 安装: 见 readme "OTA 升级" 章节
|
|
import fs from 'node:fs'
|
|
import path from 'node:path'
|
|
import crypto from 'node:crypto'
|
|
import { fileURLToPath } from 'node:url'
|
|
import { spawnSync } from 'node:child_process'
|
|
|
|
const __filename = fileURLToPath(import.meta.url)
|
|
const __dirname = path.dirname(__filename)
|
|
const ROOT = path.resolve(__dirname, '..')
|
|
|
|
const args = process.argv.slice(2)
|
|
function getArg(name, def) {
|
|
const i = args.indexOf(name)
|
|
return i >= 0 && args[i + 1] ? args[i + 1] : def
|
|
}
|
|
const OUT_DIR = path.resolve(getArg('--out', 'ota-package'))
|
|
const SEQUENCE = Number(getArg('--sequence', String(Date.now()))) // 默认用时间戳做单调计数
|
|
const MIN_BINARY = getArg('--min-binary', '')
|
|
const NOTES = getArg('--notes', '')
|
|
// 构建对应的 git commit sha (发布说明=commit message, sha 单独一行展示)
|
|
const COMMIT = getArg('--commit', '')
|
|
const NO_SIGN = args.includes('--no-sign')
|
|
// 私钥优先来源: MINISIGN_PRIVATE_KEY 环境变量 (CI 注入) → 文件 (--key 路径)
|
|
const KEY_PATH = path.resolve(getArg('--key', path.join(OUT_DIR, 'hotswap.key')))
|
|
const KEY_FROM_ENV = !!process.env.MINISIGN_PRIVATE_KEY
|
|
// 旧版 tauri.conf.json 里的 pubkey (自动同步过去, 方便用户复制)
|
|
const TAURI_CONF = path.join(ROOT, 'src-tauri', 'tauri.conf.json')
|
|
|
|
const pkg = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8'))
|
|
const APP_VERSION = pkg.version || '0.0.0'
|
|
|
|
const DIST = path.join(ROOT, 'dist')
|
|
if (!fs.existsSync(path.join(DIST, 'index.html'))) {
|
|
console.error('[build-ota] 错误: dist/index.html 不存在, 请先执行 npm run build')
|
|
process.exit(1)
|
|
}
|
|
|
|
// 准备输出目录
|
|
fs.mkdirSync(OUT_DIR, { recursive: true })
|
|
|
|
// 1) 生成 bundle.tar.gz
|
|
const BUNDLE_NAME = 'iboard-ota.tar.gz'
|
|
const BUNDLE_PATH = path.join(OUT_DIR, BUNDLE_NAME)
|
|
const r = spawnSync('tar', ['-czf', BUNDLE_PATH, '-C', DIST, '.'], { stdio: 'inherit' })
|
|
if (r.status !== 0) {
|
|
console.error('[build-ota] tar 打包失败')
|
|
process.exit(1)
|
|
}
|
|
const bundleStat = fs.statSync(BUNDLE_PATH)
|
|
const bundleSha256 = crypto.createHash('sha256').update(fs.readFileSync(BUNDLE_PATH)).digest('hex')
|
|
|
|
// 2) 签名 (minisign)
|
|
let signature = ''
|
|
let pubkey = ''
|
|
if (!NO_SIGN) {
|
|
// CI 注入: 私钥走环境变量, 写到临时文件, 签名后立即删除 (避免落盘)
|
|
let KEY_PATH_TMP = null
|
|
let KEY_PATH_USE = KEY_PATH
|
|
if (KEY_FROM_ENV) {
|
|
KEY_PATH_TMP = path.join(OUT_DIR, `.hotswap.key.${process.pid}`)
|
|
fs.writeFileSync(KEY_PATH_TMP, process.env.MINISIGN_PRIVATE_KEY, { mode: 0o600 })
|
|
KEY_PATH_USE = KEY_PATH_TMP
|
|
}
|
|
if (!fs.existsSync(KEY_PATH_USE)) {
|
|
console.error('[build-ota] 私钥不存在:', KEY_PATH_USE)
|
|
console.error(' 注入方式二选一:')
|
|
console.error(' 1) CI: 设环境变量 MINISIGN_PRIVATE_KEY (整段 .key 文件内容, 含首行 untrusted comment)')
|
|
console.error(' 2) 本机: 放 ' + KEY_PATH + ', 或用 --key <path>')
|
|
console.error(' 生成方法: minisign -G -W -p ota-package/hotswap.pub -s ota-package/hotswap.key')
|
|
process.exit(1)
|
|
}
|
|
// minisign -Sm <file> -s <key> 生成 detached signature
|
|
const sigArgs = ['-Sm', BUNDLE_PATH, '-s', KEY_PATH_USE]
|
|
const sigR = spawnSync('minisign', sigArgs, { stdio: 'inherit' })
|
|
// 清理 CI 临时私钥 (无论成功失败都删)
|
|
if (KEY_PATH_TMP) {
|
|
try { fs.unlinkSync(KEY_PATH_TMP) } catch { /* ignore */ }
|
|
KEY_PATH_TMP = null
|
|
}
|
|
if (sigR.status !== 0) {
|
|
console.error('[build-ota] minisign 签名失败')
|
|
process.exit(1)
|
|
}
|
|
const sigPath = BUNDLE_PATH + '.minisig'
|
|
signature = fs.readFileSync(sigPath, 'utf8').trim()
|
|
|
|
// 尝试同步 pubkey 到 tauri.conf.json (如果存在)
|
|
const PUB_PATH = path.join(path.dirname(KEY_PATH), 'hotswap.pub')
|
|
if (fs.existsSync(PUB_PATH)) {
|
|
pubkey = fs.readFileSync(PUB_PATH, 'utf8').trim()
|
|
const lines = pubkey.split(/\r?\n/)
|
|
pubkey = (lines.find((l) => l.startsWith('RWR') || l.startsWith('RWT') || l.startsWith('RWQ')) || '').trim()
|
|
}
|
|
}
|
|
|
|
// 3) 写 latest.json
|
|
const minBinary = MIN_BINARY || (() => {
|
|
// 默认从 src-tauri/tauri.conf.json 读 version
|
|
try { return JSON.parse(fs.readFileSync(TAURI_CONF, 'utf8')).version || APP_VERSION }
|
|
catch { return APP_VERSION }
|
|
})()
|
|
const manifest = {
|
|
version: APP_VERSION + '-ota',
|
|
sequence: SEQUENCE,
|
|
min_binary_version: minBinary,
|
|
bundle: BUNDLE_NAME,
|
|
bundle_size: bundleStat.size,
|
|
bundle_sha256: bundleSha256,
|
|
signature,
|
|
notes: String(NOTES).slice(0, 400),
|
|
commit: COMMIT || undefined,
|
|
pub_date: new Date().toISOString(),
|
|
mandatory: false,
|
|
// 内部用: 前端 web 模式比对 build_id。
|
|
// 格式与前端 vite BUILD_ID 一致 (YYYYMMDDHHmmss, 北京时间); sequence 保持毫秒单调递增
|
|
build_id: (() => {
|
|
const d = new Date(Date.now() + 8 * 3600 * 1000)
|
|
const p = (n, w) => String(n).padStart(w || 2, '0')
|
|
return `${d.getUTCFullYear()}${p(d.getUTCMonth() + 1)}${p(d.getUTCDate())}${p(d.getUTCHours())}${p(d.getUTCMinutes())}${p(d.getUTCSeconds())}`
|
|
})(),
|
|
}
|
|
fs.writeFileSync(path.join(OUT_DIR, 'latest.json'), JSON.stringify(manifest, null, 2))
|
|
|
|
// 4) 提示
|
|
console.log('')
|
|
console.log('=== OTA 包生成完成 ===')
|
|
console.log('输出目录: ', OUT_DIR)
|
|
console.log('版本: ', manifest.version)
|
|
console.log('sequence: ', manifest.sequence)
|
|
console.log('bundle 大小: ', (bundleStat.size / 1024).toFixed(1) + ' KB')
|
|
console.log('bundle sha256:', bundleSha256)
|
|
console.log('签名: ', signature ? '(已生成, 写入 latest.json)' : '(未签名, --no-sign)')
|
|
if (pubkey) {
|
|
console.log('')
|
|
console.log('请将下面这一行 pubkey 写到 src-tauri/tauri.conf.json -> plugins.hotswap.pubkey:')
|
|
console.log(' ' + pubkey)
|
|
}
|
|
|
|
console.log('')
|
|
console.log('下一步: 把这两个文件发布到 OTA 存储:')
|
|
console.log(' ' + BUNDLE_NAME)
|
|
console.log(' latest.json')
|
|
console.log('并确保 server 进程环境变量 OTA_PACKAGE_DIR 指向该目录。')
|