#!/usr/bin/env node // publish-hot.js —— 发布热更新包到自建 gitea。 // // 用法: // GIT_TOKEN=xxx node scripts/publish-hot.js --version 1.5.1 [--min 1.5.0] \ // [--notes "更新说明"] [--mandatory] [--requires-full] // token 也可放在 ~/.tokendun-git-token 文件里(优先用环境变量 GIT_TOKEN)。 // // 流程:打包 electron/+renderer/ 成 zip → sha256(+可选签名) → 写 hotupdate.json // → gitea 的 tag=hot release:删旧 bundle/manifest、传新的。 "use strict"; const fs = require("fs"); const os = require("os"); const path = require("path"); const crypto = require("crypto"); const { execFileSync } = require("child_process"); const ROOT = path.join(__dirname, ".."); const pkg = JSON.parse(fs.readFileSync(path.join(ROOT, "package.json"), "utf8")); // —— 参数 —— const argv = process.argv.slice(2); function arg(name, def) { const i = argv.indexOf(name); return i >= 0 && argv[i + 1] && !argv[i + 1].startsWith("--") ? argv[i + 1] : def; } const has = (name) => argv.includes(name); const version = arg("--version", pkg.hotContentVersion || pkg.version); const minAppVersion = arg("--min", pkg.version); const notes = arg("--notes", ""); const mandatory = has("--mandatory"); const requiresFull = has("--requires-full"); // —— token —— let token = process.env.GIT_TOKEN || ""; if (!token) { try { token = fs.readFileSync(path.join(os.homedir(), ".tokendun-git-token"), "utf8").trim(); } catch {} } if (!token) { console.error("缺少 gitea token:设置 GIT_TOKEN 或写入 ~/.tokendun-git-token"); process.exit(1); } const OWNER = "backup-bot", REPO = "backup", TAG = "hot"; const API = `https://up.tokendun.cc/api/v1/repos/${OWNER}/${REPO}`; const DL = `https://up.tokendun.cc/${OWNER}/${REPO}/releases/download/${TAG}`; const OUT = path.join(ROOT, "dist-hot"); fs.mkdirSync(OUT, { recursive: true }); const bundleName = `bundle-${version}.zip`; const bundlePath = path.join(OUT, bundleName); // —— 1) 打包(zip,deflate,可被 lib/zip.js 解压)—— fs.rmSync(bundlePath, { force: true }); console.log(`打包 electron/ + renderer/ → ${bundleName} …`); execFileSync("zip", ["-r", "-X", "-q", bundlePath, "electron", "renderer", "-x", "*.map", "-x", "*/node_modules/*"], { cwd: ROOT, stdio: "inherit" }); // —— 2) sha256(+ 可选 ed25519 签名)—— const buf = fs.readFileSync(bundlePath); const sha256 = crypto.createHash("sha256").update(buf).digest("hex"); let signature = ""; try { const keyPath = path.join(os.homedir(), ".codex-hot-key.pem"); // 私钥(不入库) if (fs.existsSync(keyPath)) { const key = fs.readFileSync(keyPath); signature = crypto.sign(null, Buffer.from(`${sha256}:${version}`), key).toString("base64"); console.log("已用本地私钥签名"); } } catch (e) { console.warn("签名跳过:" + e.message); } // —— 3) manifest —— const manifest = { contentVersion: version, minAppVersion, requiresFull, bundleUrl: `${DL}/${bundleName}`, size: buf.length, sha256, signature, notes, mandatory, }; const manifestPath = path.join(OUT, "hotupdate.json"); fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2)); console.log(`sha256=${sha256} size=${(buf.length / 1048576).toFixed(2)}MB`); // —— 4) gitea API —— function api(method, urlPath, body) { const args = ["-s", "-X", method, "-H", `Authorization: token ${token}`, `${API}${urlPath}`]; if (body !== undefined) { args.push("-H", "Content-Type: application/json", "-d", JSON.stringify(body)); } return execFileSync("curl", args, { encoding: "utf8", maxBuffer: 64 * 1024 * 1024 }); } function apiJson(method, urlPath, body) { const out = api(method, urlPath, body); try { return JSON.parse(out); } catch { return null; } } // 确保 hot release 存在 let rel = apiJson("GET", `/releases/tags/${TAG}`); if (!rel || !rel.id) { console.log("创建 hot release …"); rel = apiJson("POST", `/releases`, { tag_name: TAG, target_commitish: "main", name: "热更新通道", body: "asar 热更新包(自动发布)", draft: false, prerelease: false }); } if (!rel || !rel.id) { console.error("无法创建/获取 hot release"); process.exit(1); } // 删除同名旧 asset for (const a of rel.assets || []) { if (a.name === bundleName || a.name === "hotupdate.json" || /^bundle-.*\.zip$/.test(a.name)) { api("DELETE", `/releases/${rel.id}/assets/${a.id}`); console.log("删除旧 asset:", a.name); } } // 上传 bundle + manifest function upload(file, name) { const code = execFileSync("curl", ["-s", "-o", "/dev/null", "-w", "%{http_code}", "-X", "POST", "-H", `Authorization: token ${token}`, "-F", `attachment=@${file};type=application/octet-stream`, `${API}/releases/${rel.id}/assets?name=${encodeURIComponent(name)}`], { encoding: "utf8" }); console.log(`上传 ${name} → HTTP ${code}`); } upload(bundlePath, bundleName); upload(manifestPath, "hotupdate.json"); console.log(`\n✅ 热更 v${version} 已发布。`); console.log(` minAppVersion=${minAppVersion} mandatory=${mandatory} requiresFull=${requiresFull}`); console.log(` 客户端将从 ${API}/releases/tags/${TAG} 检测到。`);