140 lines
7.7 KiB
JavaScript
140 lines
7.7 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import crypto from "node:crypto";
|
|
import fs from "node:fs";
|
|
import os from "node:os";
|
|
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 archive = path.resolve(process.env.M6_BINARY_ARCHIVE ?? path.join(releaseRoot, "blender-web-offline.tar.gz"));
|
|
const sourceArchive = path.resolve(process.env.M6_SOURCE_ARCHIVE ?? path.join(releaseRoot, "blender-web-corresponding-source.tar.gz"));
|
|
const sumsPath = path.join(releaseRoot, "SHA256SUMS.txt");
|
|
const workspace = fs.mkdtempSync(path.join(os.tmpdir(), "blender-binary-archive-"));
|
|
|
|
const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex");
|
|
|
|
function walk(directory, base = directory) {
|
|
const files = [];
|
|
for (const entry of fs.readdirSync(directory, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name))) {
|
|
const absolute = path.join(directory, entry.name);
|
|
if (entry.isDirectory()) files.push(...walk(absolute, base));
|
|
else files.push(path.relative(base, absolute).replaceAll(path.sep, "/"));
|
|
}
|
|
return files;
|
|
}
|
|
|
|
function safeEntries(file) {
|
|
const verbose = execFileSync("tar", ["-tvzf", file], { encoding: "utf8", maxBuffer: 16 * 1024 * 1024 });
|
|
for (const line of verbose.split("\n").filter(Boolean)) {
|
|
assert.ok(["-", "d"].includes(line[0]), `binary archive contains a non-regular entry: ${line}`);
|
|
}
|
|
const entries = execFileSync("tar", ["-tzf", file], { encoding: "utf8", maxBuffer: 16 * 1024 * 1024 }).split("\n").filter(Boolean);
|
|
assert.ok(entries.length > 0, "binary archive is empty");
|
|
for (const entry of entries) {
|
|
assert.ok(!entry.startsWith("/") && !entry.split("/").includes(".."), `unsafe binary archive path: ${entry}`);
|
|
assert.ok(entry === "blender-web-offline/" || entry.startsWith("blender-web-offline/"), `unexpected binary archive root: ${entry}`);
|
|
}
|
|
}
|
|
|
|
try {
|
|
assert.ok(fs.statSync(archive).isFile(), `binary archive is missing: ${archive}`);
|
|
assert.ok(fs.statSync(sourceArchive).isFile(), `source archive is missing: ${sourceArchive}`);
|
|
assert.ok(fs.statSync(sumsPath).isFile(), `release checksum file is missing: ${sumsPath}`);
|
|
execFileSync("sha256sum", ["-c", path.basename(sumsPath)], { cwd: releaseRoot, stdio: "pipe" });
|
|
safeEntries(archive);
|
|
execFileSync("tar", ["--no-same-owner", "--no-same-permissions", "-xzf", archive, "-C", workspace]);
|
|
const bundle = path.join(workspace, "blender-web-offline");
|
|
assert.ok(!fs.realpathSync(bundle).startsWith(`${repoRoot}${path.sep}`), "binary archive was not extracted independently");
|
|
|
|
const required = [
|
|
"COPYING",
|
|
"DEPLOYMENT.md",
|
|
"SOURCE_OFFER.txt",
|
|
"deployment-contract.json",
|
|
"manifest.json",
|
|
"KNOWN_LIMITATIONS.md",
|
|
"operations-diagnostics.json",
|
|
"parity-ledger.json",
|
|
"RELEASE_NOTES.md",
|
|
"RELEASE_RECOVERY.md",
|
|
"release-metadata.json",
|
|
"sbom.spdx.json",
|
|
"third-party-notices.json",
|
|
"V1_SCOPE.md",
|
|
"app/index.html",
|
|
"app/engine-manifest.json",
|
|
];
|
|
for (const relative of required) assert.ok(fs.statSync(path.join(bundle, relative)).isFile(), `binary archive omits ${relative}`);
|
|
|
|
const manifest = JSON.parse(fs.readFileSync(path.join(bundle, "manifest.json"), "utf8"));
|
|
assert.equal(manifest.schemaVersion, 1, "binary manifest schema drifted");
|
|
assert.ok(Array.isArray(manifest.files) && manifest.files.length > 0, "binary manifest has no files");
|
|
const actualFiles = walk(bundle).filter((relative) => relative !== "manifest.json");
|
|
assert.deepEqual(manifest.files.map((entry) => entry.path), actualFiles, "binary manifest file list drifted");
|
|
for (const entry of manifest.files) {
|
|
const file = path.join(bundle, entry.path);
|
|
assert.equal(fs.statSync(file).size, entry.bytes, `binary manifest byte length drifted: ${entry.path}`);
|
|
assert.equal(sha256(file), entry.sha256, `binary manifest SHA-256 drifted: ${entry.path}`);
|
|
}
|
|
|
|
const contract = JSON.parse(fs.readFileSync(path.join(bundle, "deployment-contract.json"), "utf8"));
|
|
assert.equal(contract.schemaVersion, 1);
|
|
assert.equal(contract.transport.production, "https");
|
|
assert.equal(contract.transport.localDevelopment, "http://127.0.0.1");
|
|
assert.equal(contract.transport.fileProtocolSupported, false);
|
|
assert.deepEqual(contract.methods, ["GET", "HEAD"]);
|
|
|
|
const sbom = JSON.parse(fs.readFileSync(path.join(bundle, "sbom.spdx.json"), "utf8"));
|
|
assert.equal(sbom.spdxVersion, "SPDX-2.3");
|
|
assert.ok(sbom.packages.length > 0, "binary SBOM has no packages");
|
|
const engine = JSON.parse(fs.readFileSync(path.join(bundle, "app/engine-manifest.json"), "utf8"));
|
|
assert.equal(engine.schemaVersion, 2);
|
|
assert.equal(new Set(engine.variants.map((variant) => variant.id)).size, 2);
|
|
for (const variant of engine.variants) {
|
|
for (const resource of Object.values(variant.resources)) {
|
|
const file = path.join(bundle, "app", new URL(resource.url, "http://archive.local").pathname.slice(1));
|
|
assert.equal(sha256(file), resource.sha256, `${variant.id} ${resource.url} hash drifted`);
|
|
}
|
|
}
|
|
const releaseMetadata = JSON.parse(fs.readFileSync(path.join(bundle, "release-metadata.json"), "utf8"));
|
|
assert.equal(releaseMetadata.schemaVersion, 1);
|
|
assert.equal(releaseMetadata.engineReleaseId, engine.releaseId);
|
|
assert.equal(releaseMetadata.storage.indexedDbSchemaVersion, 6);
|
|
assert.equal(releaseMetadata.storage.opfsProjectManifestSchemaVersion, 1);
|
|
assert.equal(releaseMetadata.storage.migrationDirection, "forward-only");
|
|
assert.equal(releaseMetadata.storage.originBound, true);
|
|
assert.ok(releaseMetadata.app.workerAssets.length >= 3);
|
|
for (const relative of [...releaseMetadata.app.workerAssets, ...releaseMetadata.app.entryAssets]) {
|
|
assert.ok(fs.statSync(path.join(bundle, "app", relative)).isFile(), `release metadata asset is missing: ${relative}`);
|
|
}
|
|
const diagnostics = JSON.parse(fs.readFileSync(path.join(bundle, "operations-diagnostics.json"), "utf8"));
|
|
assert.deepEqual(diagnostics.entries.map((entry) => entry.domain).sort(), ["gpu", "hash", "isolation", "mime", "quota", "range", "worker"]);
|
|
const releaseNotes = fs.readFileSync(path.join(bundle, "RELEASE_NOTES.md"), "utf8");
|
|
assert.match(releaseNotes, /All 12 V1 family release slices are `READY`/);
|
|
assert.match(releaseNotes, /all 12 complete Blender 5\.2 family parity states are\s+`BLOCKED`/);
|
|
assert.match(releaseNotes, /\[Known limitations\]\(KNOWN_LIMITATIONS\.md\)/);
|
|
assert.match(releaseNotes, /\[Release recovery\]\(RELEASE_RECOVERY\.md\)/);
|
|
assert.match(fs.readFileSync(path.join(bundle, "KNOWN_LIMITATIONS.md"), "utf8"), /32,768-page ceiling \(2 GiB\)/);
|
|
assert.match(fs.readFileSync(path.join(bundle, "RELEASE_RECOVERY.md"), "utf8"), /sha256sum --check RC_MANIFEST\.json\.sha256/);
|
|
|
|
const report = {
|
|
schemaVersion: 1,
|
|
task: "M6-11",
|
|
status: "READY",
|
|
archive: { path: path.basename(archive), bytes: fs.statSync(archive).size, sha256: sha256(archive) },
|
|
correspondingSource: { path: path.basename(sourceArchive), bytes: fs.statSync(sourceArchive).size, sha256: sha256(sourceArchive) },
|
|
extractedRootOutsideWorkspace: true,
|
|
filesVerified: manifest.files.length,
|
|
keyFiles: Object.fromEntries(required.map((relative) => [relative, sha256(path.join(bundle, relative))])),
|
|
};
|
|
const reportRoot = path.join(releaseRoot, "archive-reports");
|
|
fs.mkdirSync(reportRoot, { recursive: true });
|
|
fs.writeFileSync(path.join(reportRoot, "binary.json"), `${JSON.stringify(report, null, 2)}\n`);
|
|
process.stdout.write(`binary-archive-ok files=${report.filesVerified} sha256=${report.archive.sha256}\n`);
|
|
}
|
|
finally {
|
|
fs.rmSync(workspace, { recursive: true, force: true });
|
|
}
|