Files
WeChat-AI/scripts/release-pack.mjs

247 lines
6.2 KiB
JavaScript

/**
* Build an OTA release pack from the monorepo (file list + sha256 + bodies).
*
* pnpm release:pack
* node scripts/release-pack.mjs
* node scripts/release-pack.mjs --bump minor
* node scripts/release-pack.mjs --version 0.3.0 --out dist/release
* node scripts/release-pack.mjs --no-bump
*
* Default: bump root package.json patch (0.2.0 → 0.2.1), write it back, then pack
* so OTA files + runtime version stay in sync without manual APP_VERSION.
*
* Writes:
* <out>/<version>/manifest.json
* <out>/<version>/files.json (paths + sha + base64 for push)
* package.json version (unless --no-write)
*/
import { createHash } from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { applyRootVersion, parseVersionArgs, repoRoot } from "./lib/version.mjs";
const root = repoRoot;
const ROOT_FILES = [
"package.json",
"pnpm-workspace.yaml",
"pnpm-lock.yaml",
"tsconfig.base.json",
];
const DIR_PREFIXES = [
"apps/api",
"packages/core",
"packages/db",
"packages/ilink",
"packages/llm",
"scripts",
];
const DENY_SEG = new Set([
"node_modules",
"data",
".git",
".wa-update-staging",
".wa-backup",
"dist",
"coverage",
]);
const INSTALL_TRIGGERS = new Set([
"pnpm-lock.yaml",
"package.json",
"pnpm-workspace.yaml",
"apps/api/package.json",
"packages/core/package.json",
"packages/db/package.json",
"packages/ilink/package.json",
"packages/llm/package.json",
]);
function parseArgs(argv) {
let parsed;
try {
parsed = parseVersionArgs(argv, 2);
} catch (e) {
console.error(String(e?.message || e));
process.exit(1);
}
const out = {
version: parsed.version,
bump: parsed.bump,
write: parsed.write,
outDir: path.join(root, "dist", "release"),
help: parsed.help,
};
for (let i = 0; i < parsed.rest.length; i++) {
const a = parsed.rest[i];
if (a === "--out" || a === "-o") {
out.outDir = path.resolve(parsed.rest[++i]);
} else {
console.error("unknown argument:", a);
process.exit(1);
}
}
if (out.help) {
console.log(`Usage: node scripts/release-pack.mjs [options]
(default) bump patch on root package.json, write, then pack
--bump patch|minor|major|none
--no-bump keep current package.json version
--version X set exact version (implies no auto-bump)
--no-write do not write package.json (pack only)
--out dir output root (default dist/release)
`);
process.exit(0);
}
return out;
}
function sha256(buf) {
return createHash("sha256").update(buf).digest("hex");
}
function isDenied(relPosix) {
const lower = relPosix.toLowerCase();
const base = lower.split("/").pop() || "";
if (base.startsWith(".env")) return true;
if (base === ".ds_store") return true;
if (/\.(db|db-wal|db-shm|log|bak)$/i.test(base)) return true;
for (const s of lower.split("/")) {
if (DENY_SEG.has(s)) return true;
}
return false;
}
function isAllowed(relPosix) {
if (!relPosix || isDenied(relPosix)) return false;
if (ROOT_FILES.includes(relPosix)) return true;
for (const dir of DIR_PREFIXES) {
if (relPosix !== dir && !relPosix.startsWith(dir + "/")) continue;
if (dir.startsWith("packages/")) {
const rest = relPosix.slice(dir.length + 1);
if (rest === "package.json" || rest === "tsconfig.json") return true;
if (rest.startsWith("src/")) return true;
return false;
}
return true;
}
return false;
}
function walk(absDir, relPrefix, list) {
let entries;
try {
entries = fs.readdirSync(absDir, { withFileTypes: true });
} catch {
return;
}
for (const ent of entries) {
const name = ent.name;
if (DENY_SEG.has(name.toLowerCase())) continue;
const rel = (relPrefix ? `${relPrefix}/${name}` : name).replace(/\\/g, "/");
const abs = path.join(absDir, name);
if (ent.isDirectory()) walk(abs, rel, list);
else if (ent.isFile() && isAllowed(rel)) list.push(rel);
}
}
function main() {
const args = parseArgs(process.argv);
let version;
try {
({ version } = applyRootVersion({
version: args.version,
bump: args.bump,
write: args.write,
}));
} catch (e) {
console.error(String(e?.message || e));
process.exit(1);
}
const list = [];
for (const f of ROOT_FILES) {
const abs = path.join(root, f);
if (fs.existsSync(abs) && isAllowed(f)) list.push(f);
}
for (const dir of DIR_PREFIXES) {
const abs = path.join(root, ...dir.split("/"));
if (fs.existsSync(abs)) walk(abs, dir, list);
}
list.sort();
const files = [];
let totalBytes = 0;
let requiresInstall = false;
for (const rel of list) {
const abs = path.join(root, ...rel.split("/"));
const buf = fs.readFileSync(abs);
const hash = sha256(buf);
files.push({
path: rel,
sha256: hash,
size: buf.length,
dataBase64: buf.toString("base64"),
});
totalBytes += buf.length;
if (INSTALL_TRIGGERS.has(rel)) requiresInstall = true;
}
const packLines = files
.map((f) => `${f.path}:${f.sha256}`)
.sort()
.join("\n");
const packSha256 = sha256(Buffer.from(packLines, "utf8"));
const manifest = {
version,
createdAt: new Date().toISOString(),
files: files.map(({ path: p, sha256: h, size }) => ({
path: p,
sha256: h,
size,
})),
requiresInstall,
totalBytes,
packSha256,
fileCount: files.length,
};
const outDir = path.join(args.outDir, version);
fs.mkdirSync(outDir, { recursive: true });
fs.writeFileSync(
path.join(outDir, "manifest.json"),
JSON.stringify(manifest, null, 2),
);
fs.writeFileSync(
path.join(outDir, "files.json"),
JSON.stringify({
version,
files: files.map((f) => ({
path: f.path,
sha256: f.sha256,
size: f.size,
dataBase64: f.dataBase64,
})),
}),
);
console.log(`Packed ${files.length} files (${totalBytes} bytes) → ${outDir}`);
console.log(`version=${version} packSha256=${packSha256.slice(0, 12)}…`);
console.log(`requiresInstall=${requiresInstall}`);
console.log(
`Next: /admin → 部署节点 → 上传通道包 → 选择 ${path.join(outDir, "files.json")}`,
);
}
main();