Files
openswarm/scripts/ci/verify-artifact-manifest.js
T

116 lines
4.9 KiB
JavaScript

#!/usr/bin/env node
// Builds or compares a SHA-256 manifest of every file under the packaged artifact root. With --write the current state becomes the manifest; without it, drift from the committed manifest fails the build. Catches non-deterministic build steps (zip timestamps, random ids in bundles) that survive even fully-pinned dependencies.
'use strict';
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const h = require('./lib/app-harness');
function parseArgs(argv) {
const out = { app: null, write: false, manifest: null, only: null };
for (let i = 0; i < argv.length; i++) {
if (argv[i] === '--app') out.app = argv[++i];
else if (argv[i] === '--write') out.write = true;
else if (argv[i] === '--manifest') out.manifest = argv[++i];
else if (argv[i] === '--only') out.only = argv[++i];
}
return out;
}
// Files that legitimately differ run-to-run (timestamped logs, OS metadata).
// Hashing them would force a manifest churn every build for no signal.
const SKIP = [
/\.DS_Store$/,
/[\\/]Thumbs\.db$/,
/[\\/]desktop\.ini$/,
/\.log$/,
];
function walk(dir, root, out = []) {
let entries = [];
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return out; }
for (const e of entries) {
const full = path.join(dir, e.name);
if (e.isDirectory()) walk(full, root, out);
else if (e.isFile()) {
const rel = path.relative(root, full).replace(/\\/g, '/');
if (SKIP.some((rx) => rx.test(rel))) continue;
out.push({ rel, full });
}
}
return out;
}
function sha256(file) {
const hash = crypto.createHash('sha256');
const buf = fs.readFileSync(file);
hash.update(buf);
return { sha: hash.digest('hex'), size: buf.length };
}
function defaultManifestPath() {
// The manifest lives in-repo under scripts/ci/ so CI compares to the
// committed snapshot, and we ship one per platform because Mac/Win/Linux
// produce different file sets.
return path.join(h.REPO_ROOT, 'scripts', 'ci', `dist-manifest.${process.platform}.json`);
}
function main() {
const args = parseArgs(process.argv.slice(2));
const appExe = h.packagedAppPath(args.app);
const root = process.platform === 'darwin'
? path.dirname(path.dirname(path.dirname(appExe)))
: path.dirname(appExe);
const manifestPath = args.manifest || defaultManifestPath();
process.stdout.write(`Artifact root: ${root}\n`);
process.stdout.write(`Manifest: ${manifestPath}\n`);
const files = walk(root, root);
const filtered = args.only ? files.filter((f) => new RegExp(args.only).test(f.rel)) : files;
process.stdout.write(`Hashing ${filtered.length} file(s)...\n`);
const current = {};
for (const f of filtered) {
const { sha, size } = sha256(f.full);
current[f.rel] = { sha, size };
}
if (args.write) {
fs.writeFileSync(manifestPath, JSON.stringify(current, null, 2));
process.stdout.write(`\nMANIFEST WRITE: ${Object.keys(current).length} entries -> ${manifestPath}\n`);
process.stdout.write('Commit this file so the next build can compare against it.\n');
process.exit(0);
}
let committed;
try { committed = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); }
catch { process.stderr.write(`\nMANIFEST FAIL: no committed manifest at ${manifestPath}. Run once with --write to seed it.\n`); process.exit(1); }
const added = [];
const removed = [];
const changed = [];
for (const k of Object.keys(current)) {
if (!committed[k]) added.push(k);
else if (committed[k].sha !== current[k].sha) changed.push({ k, was: committed[k], now: current[k] });
}
for (const k of Object.keys(committed)) {
if (!current[k]) removed.push(k);
}
if (added.length === 0 && removed.length === 0 && changed.length === 0) {
process.stdout.write('\nMANIFEST PASS: artifact is byte-identical to the committed manifest.\n');
process.exit(0);
}
process.stderr.write(`\nMANIFEST FAIL: artifact diverges from committed manifest.\n`);
if (added.length) { process.stderr.write(` added (${added.length}):\n`); for (const a of added.slice(0, 20)) process.stderr.write(` + ${a}\n`); if (added.length > 20) process.stderr.write(` ...+${added.length - 20} more\n`); }
if (removed.length) { process.stderr.write(` removed (${removed.length}):\n`); for (const r of removed.slice(0, 20)) process.stderr.write(` - ${r}\n`); if (removed.length > 20) process.stderr.write(` ...-${removed.length - 20} more\n`); }
if (changed.length) { process.stderr.write(` changed (${changed.length}):\n`); for (const c of changed.slice(0, 20)) process.stderr.write(` ~ ${c.k}\n was ${c.was.sha.slice(0, 12)}.. (${c.was.size}b), now ${c.now.sha.slice(0, 12)}.. (${c.now.size}b)\n`); if (changed.length > 20) process.stderr.write(` ...~${changed.length - 20} more\n`); }
process.stderr.write('\nIf the divergence is intentional, re-run with --write and commit the new manifest.\n');
process.exit(1);
}
main();