aps-agent/packaging/build-upgrade-manifest.mjs

138 lines
5.3 KiB
JavaScript
Raw Normal View History

#!/usr/bin/env node
/**
* APS 桌面端离线升级清单生成器(packaging 内新增文件,不占用 scripts/ 写范围)。
*
* 用法:
* node packaging/build-upgrade-manifest.mjs \
* --installer release/aps-agent-desktop-0.2.0.exe \
* --version 0.2.0 \
* --out packaging/upgrade-manifest.json \
* [--product aps-agent-desktop] [--platform win32] [--arch x64] \
* [--releasedAt 2026-08-02T00:00:00Z] [--minSupportedVersion 0.1.0]
*
* # 校验已有清单与安装包是否一致(CI / 上传前自检)
* node packaging/build-upgrade-manifest.mjs --verify packaging/upgrade-manifest.json
*
* 设计说明:
* - 签名/SBOM 上链需要外部证书与信任根,本轮只生成「校验和 + 大小 + 版本」的完整性清单;
* signature / provenance 字段预留为可选占位,由外部流程填充后发布。
* - 输出文件与 electron-builder 产物同目录使用时,fileName 建议与 artifactName 模板一致:
* aps-agent-desktop-${version}.${ext}
*/
import { createHash } from 'node:crypto';
import { createReadStream } from 'node:fs';
import { readFile, stat, writeFile } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
function usage() {
process.stderr.write(
`usage: node ${path.basename(process.argv[1] || 'build-upgrade-manifest.mjs')} ` +
`--installer <path> --version <v> --out <path> [--product p] [--platform p] [--arch a] [--releasedAt ISO] [--minSupportedVersion v]\n` +
` node ${path.basename(process.argv[1] || 'build-upgrade-manifest.mjs')} --verify <manifest.json>\n`,
);
process.exit(2);
}
function parseArgs(argv) {
const options = {};
for (let i = 0; i < argv.length; i += 1) {
const arg = argv[i];
const next = () => argv[++i];
if (arg === '--installer') options.installer = next();
else if (arg === '--version') options.version = next();
else if (arg === '--out') options.out = next();
else if (arg === '--product') options.product = next();
else if (arg === '--platform') options.platform = next();
else if (arg === '--arch') options.arch = next();
else if (arg === '--releasedAt') options.releasedAt = next();
else if (arg === '--minSupportedVersion') options.minSupportedVersion = next();
else if (arg === '--verify') options.verify = next();
else if (arg === '--help' || arg === '-h') usage();
else {
process.stderr.write(`unknown argument: ${arg}\n`);
usage();
}
}
return options;
}
async function computeSha256(filePath) {
const hash = createHash('sha256');
await new Promise((resolve, reject) => {
const stream = createReadStream(filePath);
stream.on('data', (chunk) => hash.update(chunk));
stream.on('end', resolve);
stream.on('error', reject);
});
return hash.digest('hex');
}
async function verifyManifest(manifestPath) {
const manifest = JSON.parse(await readFile(manifestPath, 'utf8'));
const required = ['schemaVersion', 'product', 'version', 'package'];
for (const key of required) {
if (!manifest[key]) throw new Error(`manifest missing required field: ${key}`);
}
const pkg = manifest.package;
const installerPath = path.resolve(path.dirname(manifestPath), pkg.fileName);
let info;
try {
info = await stat(installerPath);
} catch (error) {
throw new Error(`installer not found next to manifest: ${installerPath} (${error.message})`);
}
if (info.size !== pkg.size) {
throw new Error(`size mismatch: manifest=${pkg.size} actual=${info.size}`);
}
const actual = await computeSha256(installerPath);
if (actual !== pkg.sha256) {
throw new Error(`sha256 mismatch: manifest=${pkg.sha256} actual=${actual}`);
}
return { manifest, installerPath };
}
async function main() {
const options = parseArgs(process.argv.slice(2));
if (options.verify) {
const { manifest, installerPath } = await verifyManifest(options.verify);
process.stdout.write(
`OK ${manifest.version} ${installerPath} size=${manifest.package.size} sha256=${manifest.package.sha256}\n`,
);
return;
}
if (!options.installer || !options.version || !options.out) usage();
const installerPath = path.resolve(options.installer);
const info = await stat(installerPath);
if (!info.isFile()) throw new Error(`not a file: ${installerPath}`);
const sha256 = await computeSha256(installerPath);
const manifest = {
schemaVersion: 1,
product: options.product || 'aps-agent-desktop',
version: options.version,
...(options.platform ? { platform: options.platform } : {}),
...(options.arch ? { arch: options.arch } : {}),
package: {
fileName: path.basename(installerPath),
sha256,
size: info.size,
},
releasedAt: options.releasedAt || new Date().toISOString(),
...(options.minSupportedVersion ? { minSupportedVersion: options.minSupportedVersion } : {}),
};
const outPath = path.resolve(options.out);
await writeFile(outPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
process.stdout.write(
`wrote ${path.relative(process.cwd(), outPath)}: ${manifest.version} size=${info.size} sha256=${sha256}\n`,
);
}
main().catch((error) => {
process.stderr.write(`ERROR: ${error.message}\n`);
process.exit(1);
});