Files
openmaic/OpenMAIC/scripts/verify-package-artifacts.mjs
2026-08-16 14:58:47 +08:00

117 lines
3.9 KiB
JavaScript

import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import { createHash } from 'node:crypto';
import { lstatSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
import { basename, dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { OPENMAIC_PACKAGES, readManifest } from './openmaic-packages.mjs';
const DIGESTS_FILE = 'SHA256SUMS';
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
function expectedArtifacts() {
return OPENMAIC_PACKAGES.map((shortName) => {
const manifest = readManifest(shortName);
return {
shortName,
name: `@openmaic/${shortName}`,
version: manifest.version,
filename: `openmaic-${shortName}-${manifest.version}.tgz`,
};
});
}
function sha256(path) {
return createHash('sha256').update(readFileSync(path)).digest('hex');
}
function readPackedManifest(path) {
return JSON.parse(
execFileSync('tar', ['-xzOf', path, 'package/package.json'], { encoding: 'utf8' }),
);
}
function assertArtifactSet(directory, expected, includeDigests) {
const expectedNames = new Set(expected.map(({ filename }) => filename));
if (includeDigests) expectedNames.add(DIGESTS_FILE);
const entries = readdirSync(directory, { withFileTypes: true });
assert.deepEqual(
entries.map(({ name }) => name).sort(),
[...expectedNames].sort(),
`Release artifact directory must contain exactly ${[...expectedNames].sort().join(', ')}`,
);
for (const entry of entries) {
const path = join(directory, entry.name);
assert(
entry.isFile() && !lstatSync(path).isSymbolicLink(),
`${entry.name} must be a regular file`,
);
}
for (const artifact of expected) {
const manifest = readPackedManifest(join(directory, artifact.filename));
assert.equal(
manifest.name,
artifact.name,
`${artifact.filename} contains package ${manifest.name}`,
);
assert.equal(
manifest.version,
artifact.version,
`${artifact.filename} contains version ${manifest.version}`,
);
}
}
function writeDigests(directory, expected) {
assertArtifactSet(directory, expected, false);
const contents = expected
.map(({ filename }) => `${sha256(join(directory, filename))} ${filename}`)
.join('\n');
writeFileSync(join(directory, DIGESTS_FILE), `${contents}\n`);
console.log(`Recorded SHA-256 digests for ${expected.length} package tarballs.`);
}
function verifyDigests(directory, expected) {
assertArtifactSet(directory, expected, true);
const lines = readFileSync(join(directory, DIGESTS_FILE), 'utf8').trimEnd().split('\n');
const recorded = new Map();
for (const line of lines) {
const match = /^([0-9a-f]{64}) ([^/]+\.tgz)$/.exec(line);
assert(match, `Invalid ${DIGESTS_FILE} entry: ${JSON.stringify(line)}`);
assert(!recorded.has(match[2]), `${DIGESTS_FILE} lists ${match[2]} more than once`);
recorded.set(match[2], match[1]);
}
assert.deepEqual(
[...recorded.keys()].sort(),
expected.map(({ filename }) => filename).sort(),
`${DIGESTS_FILE} must list every allowed package tarball exactly once`,
);
for (const { filename } of expected) {
assert.equal(
sha256(join(directory, filename)),
recorded.get(filename),
`${filename} failed SHA-256 verification`,
);
}
console.log(`Verified SHA-256 digests for ${expected.length} package tarballs.`);
}
const args = process.argv.slice(2);
const write = args[0] === '--write';
const directoryArgument = args[write ? 1 : 0];
assert(
directoryArgument,
`Usage: node ${basename(process.argv[1])} [--write] <artifact-directory>`,
);
assert.equal(args.length, write ? 2 : 1, 'Unexpected command-line arguments');
const directory = resolve(root, directoryArgument);
const expected = expectedArtifacts();
if (write) writeDigests(directory, expected);
else verifyDigests(directory, expected);