127 lines
4.4 KiB
JavaScript
127 lines
4.4 KiB
JavaScript
|
|
#!/usr/bin/env node
|
|||
|
|
// publish-codex-installer.js —— 发布 Codex 安装器资源到自建 gitea。
|
|||
|
|
//
|
|||
|
|
// 用法:
|
|||
|
|
// node scripts/publish-codex-installer.js /Users/mac/Downloads/Codex-Windows-x64.msix
|
|||
|
|
// node scripts/publish-codex-installer.js /Users/mac/Downloads/ChatGPT.dmg
|
|||
|
|
// token 可设置 GIT_TOKEN,或写入 ~/.tokendun-git-token。
|
|||
|
|
"use strict";
|
|||
|
|
|
|||
|
|
const fs = require("fs");
|
|||
|
|
const os = require("os");
|
|||
|
|
const path = require("path");
|
|||
|
|
const crypto = require("crypto");
|
|||
|
|
const { execFileSync, spawnSync } = require("child_process");
|
|||
|
|
|
|||
|
|
const OWNER = "mhgmh";
|
|||
|
|
const REPO = "codex-market-app";
|
|||
|
|
const TAG = "codex-installers";
|
|||
|
|
const MSIX_NAME = "Codex-Windows-x64.msix";
|
|||
|
|
const DMG_NAME = "ChatGPT.dmg";
|
|||
|
|
const ALLOWED_NAMES = new Set([MSIX_NAME, DMG_NAME]);
|
|||
|
|
const API = `https://up.tokendun.cc/api/v1/repos/${OWNER}/${REPO}`;
|
|||
|
|
const DL = `https://update.tokendun.cc/${OWNER}/${REPO}/releases/download/${TAG}`;
|
|||
|
|
|
|||
|
|
const installerPath = path.resolve(process.argv[2] || "/Users/mac/Downloads/Codex-Windows-x64.msix");
|
|||
|
|
const installerName = path.basename(installerPath);
|
|||
|
|
if (!ALLOWED_NAMES.has(installerName)) {
|
|||
|
|
console.error(`安装包文件名必须是 ${Array.from(ALLOWED_NAMES).join(" 或 ")},当前是 ${installerName}`);
|
|||
|
|
process.exit(1);
|
|||
|
|
}
|
|||
|
|
if (!fs.existsSync(installerPath)) {
|
|||
|
|
console.error(`安装包不存在:${installerPath}`);
|
|||
|
|
process.exit(1);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function hashFile(file) {
|
|||
|
|
return new Promise((resolve, reject) => {
|
|||
|
|
const h = crypto.createHash("sha256");
|
|||
|
|
fs.createReadStream(file)
|
|||
|
|
.on("data", (chunk) => h.update(chunk))
|
|||
|
|
.on("error", reject)
|
|||
|
|
.on("end", () => resolve(h.digest("hex")));
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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; }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function upload(rel, file, name) {
|
|||
|
|
const result = spawnSync("curl", [
|
|||
|
|
"--progress-bar",
|
|||
|
|
"-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", stdio: ["ignore", "pipe", "inherit"], maxBuffer: 64 * 1024 * 1024 });
|
|||
|
|
if (result.error) throw result.error;
|
|||
|
|
const code = String(result.stdout || "").trim();
|
|||
|
|
console.log(`上传 ${name} → HTTP ${code}`);
|
|||
|
|
if (code !== "201") {
|
|||
|
|
console.error(`上传失败:${name}`);
|
|||
|
|
process.exit(1);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
(async () => {
|
|||
|
|
const stat = fs.statSync(installerPath);
|
|||
|
|
console.log(`准备发布 ${installerName}:${(stat.size / 1048576).toFixed(2)}MB`);
|
|||
|
|
const sha256 = await hashFile(installerPath);
|
|||
|
|
const shaPath = path.join(os.tmpdir(), `${installerName}.sha256`);
|
|||
|
|
fs.writeFileSync(shaPath, `${sha256} ${installerName}\n`);
|
|||
|
|
console.log(`sha256=${sha256}`);
|
|||
|
|
|
|||
|
|
let rel = apiJson("GET", `/releases/tags/${encodeURIComponent(TAG)}`);
|
|||
|
|
if (!rel || !rel.id) {
|
|||
|
|
console.log(`创建 release ${TAG} …`);
|
|||
|
|
rel = apiJson("POST", "/releases", {
|
|||
|
|
tag_name: TAG,
|
|||
|
|
target_commitish: "main",
|
|||
|
|
name: "Codex 安装器资源",
|
|||
|
|
body: "TD工具箱内置安装器使用的远程安装包资源。",
|
|||
|
|
draft: false,
|
|||
|
|
prerelease: false,
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
if (!rel || !rel.id) {
|
|||
|
|
console.error(`无法创建/获取 release ${TAG}`);
|
|||
|
|
process.exit(1);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const existing = new Map((rel.assets || []).map((a) => [a.name, a.id]));
|
|||
|
|
for (const name of [installerName, `${installerName}.sha256`]) {
|
|||
|
|
if (existing.has(name)) {
|
|||
|
|
api("DELETE", `/releases/${rel.id}/assets/${existing.get(name)}`);
|
|||
|
|
console.log(`删除旧 asset: ${name}`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
upload(rel, installerPath, installerName);
|
|||
|
|
upload(rel, shaPath, `${installerName}.sha256`);
|
|||
|
|
|
|||
|
|
console.log("\n发布完成:");
|
|||
|
|
console.log(` ${DL}/${installerName}`);
|
|||
|
|
console.log(` ${DL}/${installerName}.sha256`);
|
|||
|
|
})();
|