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

138 lines
5.3 KiB
JavaScript
Raw Permalink 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
/**
* 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);
});