99 lines
5.8 KiB
JavaScript
99 lines
5.8 KiB
JavaScript
import crypto from "node:crypto";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { execFileSync } from "node:child_process";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
const releaseRoot = path.join(repoRoot, "release");
|
|
const sha256Bytes = (value) => crypto.createHash("sha256").update(value).digest("hex");
|
|
const sha256 = (file) => sha256Bytes(fs.readFileSync(file));
|
|
const archiveEntry = (archive, entry) => execFileSync("tar", ["-xOf", archive, entry], { maxBuffer: 32 * 1024 * 1024 });
|
|
const packageJson = JSON.parse(fs.readFileSync(path.join(repoRoot, "web/package.json"), "utf8"));
|
|
const engineManifest = JSON.parse(fs.readFileSync(path.join(repoRoot, "web/app/public/engine-manifest.json"), "utf8"));
|
|
const binaryArchive = path.join(releaseRoot, "blender-web-offline.tar.gz");
|
|
const sourceArchive = path.join(releaseRoot, "blender-web-corresponding-source.tar.gz");
|
|
const sbom = path.join(repoRoot, "docs/web/sbom.spdx.json");
|
|
const sumsPath = path.join(releaseRoot, "SHA256SUMS.txt");
|
|
const packageJsonPath = path.join(repoRoot, "web/package.json");
|
|
const packageLockPath = path.join(repoRoot, "web/package-lock.json");
|
|
const engineManifestPath = path.join(repoRoot, "web/app/public/engine-manifest.json");
|
|
const embeddedPackageJsonBytes = archiveEntry(sourceArchive, "web/package.json");
|
|
const embeddedPackageLockBytes = archiveEntry(sourceArchive, "web/package-lock.json");
|
|
const embeddedEngineManifestBytes = archiveEntry(binaryArchive, "blender-web-offline/app/engine-manifest.json");
|
|
const embeddedMetadataBytes = archiveEntry(binaryArchive, "blender-web-offline/release-metadata.json");
|
|
const embeddedSbomBytes = archiveEntry(binaryArchive, "blender-web-offline/sbom.spdx.json");
|
|
const sourceManifest = JSON.parse(archiveEntry(sourceArchive, "SOURCE_MANIFEST.json").toString("utf8"));
|
|
const embeddedMetadata = JSON.parse(embeddedMetadataBytes.toString("utf8"));
|
|
const commit = execFileSync("git", ["rev-parse", "HEAD"], { cwd: repoRoot, encoding: "utf8" }).trim();
|
|
const worktreeDirty = execFileSync("git", ["status", "--porcelain", "--untracked-files=all"], {
|
|
cwd: repoRoot,
|
|
encoding: "utf8",
|
|
}).trim().length > 0;
|
|
|
|
if (!/^\d+\.\d+\.\d+-rc\.\d+$/.test(packageJson.version)) throw new Error("package version is not an RC semver");
|
|
if (engineManifest.releaseId !== `blender-wasm-${packageJson.version}`) throw new Error("engine releaseId does not match RC semver");
|
|
if (embeddedMetadata.productVersion !== packageJson.version || embeddedMetadata.engineReleaseId !== engineManifest.releaseId) {
|
|
throw new Error("binary release metadata does not match the frozen RC identity");
|
|
}
|
|
if (sourceManifest.version !== packageJson.version || sourceManifest.engineReleaseId !== engineManifest.releaseId) {
|
|
throw new Error("source manifest does not match the frozen RC identity");
|
|
}
|
|
for (const [label, actual, expected] of [
|
|
["source package.json", embeddedPackageJsonBytes, fs.readFileSync(packageJsonPath)],
|
|
["source package-lock.json", embeddedPackageLockBytes, fs.readFileSync(packageLockPath)],
|
|
["binary engine manifest", embeddedEngineManifestBytes, fs.readFileSync(engineManifestPath)],
|
|
["binary SBOM", embeddedSbomBytes, fs.readFileSync(sbom)],
|
|
]) {
|
|
if (!actual.equals(expected)) throw new Error(`${label} does not match the frozen workspace bytes`);
|
|
}
|
|
const checksumEntries = fs.readFileSync(sumsPath, "utf8").trim().split("\n").map((line) => line.match(/^([a-f0-9]{64}) ([^/]+)$/));
|
|
if (checksumEntries.some((entry) => entry === null) || checksumEntries.length !== 2) {
|
|
throw new Error("SHA256SUMS.txt must contain exactly two canonical archive entries");
|
|
}
|
|
const checksums = new Map(checksumEntries.map((entry) => [entry[2], entry[1]]));
|
|
for (const file of [binaryArchive, sourceArchive]) {
|
|
if (checksums.get(path.basename(file)) !== sha256(file)) throw new Error(`SHA256SUMS.txt does not match ${path.basename(file)}`);
|
|
}
|
|
|
|
const artifact = (file, relative) => ({
|
|
path: relative,
|
|
bytes: fs.statSync(file).size,
|
|
sha256: sha256(file),
|
|
});
|
|
const operations = ["deploy", "upgrade", "rollback", "diagnostics", "rehearsal"]
|
|
.map((name) => [name, path.join(releaseRoot, "operations-reports", `${name}.json`)]);
|
|
for (const [name, file] of operations) {
|
|
if (!fs.existsSync(file)) throw new Error(`RC operations report is missing: ${name}`);
|
|
}
|
|
const manifest = {
|
|
schemaVersion: 1,
|
|
rcId: `web-blender-${packageJson.version}`,
|
|
semver: packageJson.version,
|
|
gitCommit: commit,
|
|
sourceBinding: {
|
|
mode: "corresponding-source-archive",
|
|
worktreeDirty,
|
|
note: "The source archive SHA-256 binds the candidate bytes independently of the base commit.",
|
|
},
|
|
engineReleaseId: engineManifest.releaseId,
|
|
artifacts: {
|
|
binaryArchive: artifact(binaryArchive, "release/blender-web-offline.tar.gz"),
|
|
sourceArchive: artifact(sourceArchive, "release/blender-web-corresponding-source.tar.gz"),
|
|
sbom: artifact(sbom, "docs/web/sbom.spdx.json"),
|
|
},
|
|
bindings: {
|
|
packageJsonSha256: sha256(packageJsonPath),
|
|
packageLockSha256: sha256(packageLockPath),
|
|
ledgerSha256: sha256(path.join(repoRoot, "docs/status/parity-ledger.json")),
|
|
engineManifestSha256: sha256(engineManifestPath),
|
|
embeddedReleaseMetadataSha256: sha256Bytes(embeddedMetadataBytes),
|
|
sha256SumsSha256: sha256(sumsPath),
|
|
},
|
|
operations: Object.fromEntries(operations.map(([name, file]) => [name, artifact(file, path.relative(repoRoot, file).replaceAll(path.sep, "/"))])),
|
|
};
|
|
const output = path.join(releaseRoot, "RC_MANIFEST.json");
|
|
fs.writeFileSync(output, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
fs.writeFileSync(`${output}.sha256`, `${sha256(output)} ${path.basename(output)}\n`);
|
|
process.stdout.write(`rc-manifest-ok id=${manifest.rcId} commit=${commit} binary=${manifest.artifacts.binaryArchive.sha256} source=${manifest.artifacts.sourceArchive.sha256} sbom=${manifest.artifacts.sbom.sha256}\n`);
|