Files
codex-market-app/scripts/publish-hot.js
T
2026-07-15 18:01:56 +08:00

125 lines
5.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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} 检测到。`);