Files
projectbrain-Public/scripts/generate-release-metadata.mjs
T

65 lines
3.8 KiB
JavaScript

#!/usr/bin/env node
import { execFileSync } from "node:child_process";
import { createHash, randomUUID } from "node:crypto";
import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
import { resolve, relative } from "node:path";
const root = resolve(import.meta.dirname, "..");
const outputFlag = process.argv.indexOf("--output");
const output = resolve(root, outputFlag >= 0 ? process.argv[outputFlag + 1] : "release-artifacts");
const sha256 = (value) => createHash("sha256").update(value).digest("hex");
const command = (program, args) => execFileSync(program, args, { cwd: root, encoding: "utf8", windowsHide: true }).trim();
const pnpmOutput = process.env.npm_execpath
? command(process.execPath, [process.env.npm_execpath, "list", "--prod", "--json", "--depth", "Infinity"])
: command("pnpm", ["list", "--prod", "--json", "--depth", "Infinity"]);
const listed = JSON.parse(pnpmOutput);
const components = new Map();
const collect = (dependencies = {}) => {
for (const [name, dependency] of Object.entries(dependencies)) {
if (dependency.version) components.set(`${name}@${dependency.version}`, { type: "library", name, version: dependency.version, purl: `pkg:npm/${encodeURIComponent(name)}@${dependency.version}` });
collect(dependency.dependencies);
}
};
for (const project of listed) collect(project.dependencies);
const packageJson = JSON.parse(await readFile(resolve(root, "package.json"), "utf8"));
const serialNumber = `urn:uuid:${randomUUID()}`;
const sbom = {
bomFormat: "CycloneDX",
specVersion: "1.6",
serialNumber,
version: 1,
metadata: { component: { type: "application", name: packageJson.name, version: packageJson.version }, tools: { components: [{ type: "application", name: "ProjectBrain release metadata generator", version: "1.0.0" }] } },
components: [...components.values()].sort((left, right) => left.purl.localeCompare(right.purl)),
};
const artifactFiles = [];
const collectFiles = async (directory) => {
for (const entry of await readdir(directory, { withFileTypes: true })) {
const path = resolve(directory, entry.name);
if (entry.isDirectory()) await collectFiles(path);
else artifactFiles.push({ name: relative(root, path).replaceAll("\\", "/"), digest: { sha256: sha256(await readFile(path)) }, bytes: (await stat(path)).size });
}
};
await collectFiles(resolve(root, "dist"));
for (const name of ["Dockerfile.frontend", "Dockerfile.backend", "Dockerfile.unraid", "pnpm-lock.yaml"]) {
const path = resolve(root, name);
try { artifactFiles.push({ name, digest: { sha256: sha256(await readFile(path)) }, bytes: (await stat(path)).size }); } catch (error) { if (error.code !== "ENOENT") throw error; }
}
artifactFiles.sort((left, right) => left.name.localeCompare(right.name));
const revision = command("git", ["rev-parse", "HEAD"]);
const provenance = {
predicateType: "https://slsa.dev/provenance/v1",
subject: artifactFiles,
predicate: {
buildDefinition: { buildType: "https://projectbrain.local/build/v1", externalParameters: { node: process.version, packageManager: packageJson.packageManager, revision }, resolvedDependencies: [{ uri: "git+local:projectbrain", digest: { sha1: revision } }] },
runDetails: { builder: { id: "https://projectbrain.local/ci" }, metadata: { invocationId: process.env.GITHUB_RUN_ID ?? `local-${revision.slice(0, 12)}`, startedOn: new Date().toISOString(), sourceDirty: command("git", ["status", "--porcelain"]) !== "" } },
},
sbom: { file: "projectbrain.cdx.json", sha256: sha256(JSON.stringify(sbom)) },
};
await mkdir(output, { recursive: true });
await writeFile(resolve(output, "projectbrain.cdx.json"), `${JSON.stringify(sbom, null, 2)}\n`);
await writeFile(resolve(output, "projectbrain.provenance.json"), `${JSON.stringify(provenance, null, 2)}\n`);
console.log(`Generated ${components.size} component SBOM and provenance for ${artifactFiles.length} artifacts in ${output}.`);