Files
codex-market-app/scripts/publish-hot.js
T

153 lines
6.7 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);
// 这是原生 Electron 壳的最低版本,不是热更内容版本。
const minAppVersion = arg("--min", pkg.hotMinAppVersion || "1.5.0");
const notes = arg("--notes", "");
const mandatory = has("--mandatory");
const requiresFull = has("--requires-full");
const requiresFullAfterHot = has("--requires-full-after-hot");
const fullAppVersion = arg("--full-app-version", "");
// —— 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 = "mhgmh", REPO = "codex-market-app", TAG = "hot";
// Authenticated writes go through the direct upstream host; public download
// URLs must use update.tokendun.cc because that is what clients/users read.
const API = `https://up.tokendun.cc/api/v1/repos/${OWNER}/${REPO}`;
const DL = `https://update.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);
// —— 0) 版本复用守卫:远端 manifest 已是该版本时拒绝发布(除非 --force)。
// 客户端按 contentVersion 判断是否更新,同名覆盖会让已装用户永远收不到新内容。
try {
const rel = JSON.parse(execFileSync("curl", ["-s", `-H`, `Authorization: token ${token}`,
`${API}/releases/tags/${TAG}`], { encoding: "utf8", maxBuffer: 64 * 1024 * 1024 }));
const mAsset = (rel.assets || []).find((a) => a.name === "hotupdate.json");
if (mAsset) {
const remote = JSON.parse(execFileSync("curl", ["-s", "-H", `Authorization: token ${token}`,
mAsset.browser_download_url], { encoding: "utf8" }));
if (remote && remote.contentVersion === version && !has("--force")) {
console.error(`✖ 版本 ${version} 已发布过(远端 manifest contentVersion 相同)。`);
console.error(" 客户端会认为已是最新而忽略同名覆盖。请递增 --version 后重试,或确需覆盖时加 --force。");
process.exit(1);
}
}
} catch (e) { /* 拉不到远端 manifest 时不阻塞发布 */ }
// —— 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,
requiresFullAfterHot,
fullAppVersion,
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}`);
if (code !== "201") {
console.error(`上传 ${name} 失败,HTTP ${code}`);
process.exit(1);
}
}
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} 检测到。`);