Files
workinf_Blender_Wasm/tools/web/check-deployment-runbook.mjs
mes123456 7c16b279ae
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled
Advance M7 workflows and release operations
2026-08-15 17:43:53 -04:00

165 lines
8.1 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";
import {
createDeploymentHttpServer,
listenDeploymentHttpServer,
} from "./deployment-http-server.mjs";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const releaseRoot = path.join(repoRoot, "release");
const sourceArchive = path.join(releaseRoot, "blender-web-corresponding-source.tar.gz");
const binaryArchive = path.join(releaseRoot, "blender-web-offline.tar.gz");
const sumsFile = path.join(releaseRoot, "SHA256SUMS.txt");
const workspace = fs.mkdtempSync(path.join(os.tmpdir(), "blender-deployment-runbook-"));
const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex");
function checksumEntries(value) {
return Object.fromEntries(value.trim().split("\n").map((line) => {
const match = line.match(/^([a-f0-9]{64}) ([^/]+)$/);
assert.ok(match, `invalid SHA256SUMS entry: ${line}`);
return [match[2], match[1]];
}));
}
function assertSafeArchive(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]), `deployment archive contains a link or special entry: ${line}`);
}
const entries = execFileSync("tar", ["-tzf", file], { encoding: "utf8", maxBuffer: 16 * 1024 * 1024 })
.split("\n")
.filter(Boolean);
assert.ok(entries.length > 0, "deployment archive is empty");
for (const entry of entries) {
assert.ok(!entry.startsWith("/"), `deployment archive contains an absolute path: ${entry}`);
assert.ok(!entry.split("/").includes(".."), `deployment archive contains path traversal: ${entry}`);
assert.ok(entry === "blender-web-offline/" || entry.startsWith("blender-web-offline/"), `unexpected deployment archive root: ${entry}`);
}
}
function classifyOrigin(value, contract) {
let origin;
try {
origin = new URL(value);
}
catch {
return { status: "BLOCKED", reason: "INVALID_ORIGIN" };
}
if (origin.username || origin.password || origin.pathname !== "/" || origin.search || origin.hash) {
return { status: "BLOCKED", reason: "UNSUPPORTED_ORIGIN_SHAPE" };
}
if (origin.protocol === `${contract.transport.production}:`) return { status: "READY", mode: "production-https" };
const local = new URL(contract.transport.localDevelopment);
if (origin.protocol === local.protocol && origin.hostname === local.hostname) {
return { status: "READY", mode: "loopback-http" };
}
return { status: "BLOCKED", reason: "HTTPS_OR_EXPLICIT_LOOPBACK_REQUIRED" };
}
function assertHeader(response, name, expected) {
assert.equal(response.headers.get(name), expected, `${name} drifted at ${response.url}`);
}
let server;
try {
const delivery = path.join(workspace, "delivery");
const installRoot = path.join(workspace, "install");
fs.mkdirSync(delivery);
fs.mkdirSync(installRoot);
for (const file of [binaryArchive, sourceArchive, sumsFile]) {
assert.ok(fs.statSync(file).isFile(), `release delivery is missing ${path.basename(file)}`);
fs.copyFileSync(file, path.join(delivery, path.basename(file)));
}
const deliveredArchive = path.join(delivery, path.basename(binaryArchive));
const sums = checksumEntries(fs.readFileSync(path.join(delivery, path.basename(sumsFile)), "utf8"));
assert.equal(sha256(deliveredArchive), sums[path.basename(binaryArchive)], "binary delivery checksum drifted");
assert.equal(sha256(path.join(delivery, path.basename(sourceArchive))), sums[path.basename(sourceArchive)], "source delivery checksum drifted");
assertSafeArchive(deliveredArchive);
const releases = path.join(installRoot, "releases");
fs.mkdirSync(releases);
const staging = fs.mkdtempSync(path.join(installRoot, ".install."));
execFileSync("tar", ["--no-same-owner", "--no-same-permissions", "-xzf", deliveredArchive, "-C", staging]);
const stagedBundle = path.join(staging, "blender-web-offline");
const archiveSha256 = sha256(deliveredArchive);
const installedRelease = path.join(releases, archiveSha256);
assert.ok(!fs.existsSync(installedRelease), "fresh install unexpectedly contains a release");
fs.renameSync(stagedBundle, installedRelease);
fs.rmdirSync(staging);
const pendingLink = path.join(installRoot, ".current.next");
fs.symlinkSync(path.join("releases", archiveSha256), pendingLink);
fs.renameSync(pendingLink, path.join(installRoot, "current"));
assert.equal(fs.realpathSync(path.join(installRoot, "current")), installedRelease);
assert.ok(!installedRelease.startsWith(`${repoRoot}${path.sep}`), "runbook installed inside the source workspace");
const bundleRoot = path.join(installRoot, "current");
const appRoot = path.join(bundleRoot, "app");
const contract = JSON.parse(fs.readFileSync(path.join(bundleRoot, "deployment-contract.json"), "utf8"));
const engine = JSON.parse(fs.readFileSync(path.join(appRoot, "engine-manifest.json"), "utf8"));
assert.equal(classifyOrigin("https://blender.example", contract).mode, "production-https");
assert.equal(classifyOrigin("https://blender.example:8443", contract).mode, "production-https");
assert.equal(classifyOrigin("http://127.0.0.1:8080", contract).mode, "loopback-http");
for (const blocked of [
"file:///tmp/blender/index.html",
"http://localhost:8080",
"http://0.0.0.0:8080",
"http://192.168.1.10:8080",
"https://user:secret@blender.example",
"https://blender.example/subpath",
]) assert.equal(classifyOrigin(blocked, contract).status, "BLOCKED", `${blocked} must be blocked`);
server = createDeploymentHttpServer({ root: appRoot, contract });
const origin = await listenDeploymentHttpServer(server);
assert.equal(classifyOrigin(origin, contract).mode, "loopback-http");
const indexResponse = await fetch(`${origin}/`, { method: "HEAD" });
assert.equal(indexResponse.status, 200);
for (const [name, value] of Object.entries(contract.responseHeaders.allResponses)) assertHeader(indexResponse, name, value);
assertHeader(indexResponse, "cache-control", "no-cache");
assertHeader(indexResponse, "content-type", contract.mimeTypes[".html"]);
assert.match(indexResponse.headers.get("etag") ?? "", /^"sha256-[a-f0-9]{64}"$/);
const wasmPath = new URL(engine.variants.find((variant) => variant.id === "single").resources.wasm.url, origin);
const rangeResponse = await fetch(wasmPath, { headers: { Range: "bytes=0-15" } });
assert.equal(rangeResponse.status, contract.rangeRequests.satisfiedStatus);
assert.equal((await rangeResponse.arrayBuffer()).byteLength, 16);
assertHeader(rangeResponse, "accept-ranges", "bytes");
assert.match(rangeResponse.headers.get("content-range") ?? "", /^bytes 0-15\/\d+$/);
const report = {
schemaVersion: 1,
task: "M6-17A",
status: "READY",
archive: { bytes: fs.statSync(deliveredArchive).size, sha256: archiveSha256 },
engineReleaseId: engine.releaseId,
install: {
emptyRoot: true,
outsideSourceWorkspace: true,
immutableReleaseDirectory: path.relative(installRoot, installedRelease),
currentTarget: fs.readlinkSync(path.join(installRoot, "current")),
},
transport: {
productionHttps: "READY",
loopbackHttp: "READY",
publicPlainHttp: "BLOCKED",
fileProtocol: "BLOCKED",
sameOriginRuntimeAssets: contract.transport.sameOriginRuntimeAssets,
},
installedServer: { status: indexResponse.status, rangeStatus: rangeResponse.status, isolationHeaders: contract.responseHeaders.allResponses },
};
const reportRoot = path.join(releaseRoot, "operations-reports");
fs.mkdirSync(reportRoot, { recursive: true });
fs.writeFileSync(path.join(reportRoot, "deploy.json"), `${JSON.stringify(report, null, 2)}\n`);
process.stdout.write(`deployment-runbook-ok release=${engine.releaseId} archive=${archiveSha256} install=empty https=ready loopback=ready public-http=blocked\n`);
}
finally {
if (server) await new Promise((resolve) => server.close(resolve));
fs.rmSync(workspace, { recursive: true, force: true });
}