Files
workinf_Blender_Wasm/tools/web/run-ci-lane.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

187 lines
9.5 KiB
JavaScript

import assert from "node:assert/strict";
import crypto from "node:crypto";
import fs from "node:fs";
import net from "node:net";
import os from "node:os";
import path from "node:path";
import { spawnSync } 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 lane = process.argv[2];
const selfTestFailure = process.argv.includes("--self-test-failure");
assert.ok(["quick", "chromium", "release"].includes(lane), "usage: run-ci-lane.mjs <quick|chromium|release>");
const laneCommands = {
quick: [
["lockfile-install", "npm", ["--prefix", "web", "ci", "--ignore-scripts"]],
["typecheck", "npm", ["--prefix", "web", "run", "typecheck"]],
["lint", "npm", ["--prefix", "web", "run", "lint"]],
["unit", "npm", ["--prefix", "web", "test"]],
["status", "npm", ["--prefix", "web", "run", "test:status-consistency"]],
["evidence-schema", "npm", ["--prefix", "web", "run", "test:release-evidence"]],
["rc-docs", "npm", ["--prefix", "web", "run", "test:rc-docs"]],
],
chromium: [
["lockfile-install", "npm", ["--prefix", "web", "ci", "--ignore-scripts"]],
["p0-user-loop", "npm", ["--prefix", "web", "run", "test:v1-user-loop"], "browser"],
["offline-browser-smoke", "npm", ["--prefix", "web", "run", "test:browser"], "browser"],
["network-interruption", "npm", ["--prefix", "web", "run", "test:network-interruption"], "browser"],
["device-loss", "npm", ["--prefix", "web", "run", "test:device-loss"], "browser"],
["oom-recovery", "npm", ["--prefix", "web", "run", "test:oom-recovery"], "browser"],
["opfs-quota", "npm", ["--prefix", "web", "run", "test:storage-quota"], "browser"],
["malicious-blend", "npm", ["--prefix", "web", "run", "test:malicious-blends"]],
["malicious-archive", "npm", ["--prefix", "web", "run", "test:asset-library"], "browser"],
],
release: [
["lockfile-install", "npm", ["--prefix", "web", "ci", "--ignore-scripts"]],
["full-e2e", "npm", ["--prefix", "web", "run", "test:e2e", "--", "--workers=1"], "browser"],
["performance-100k-1m", "npm", ["--prefix", "web", "run", "test:release-performance"]],
["performance-10m", "npm", ["--prefix", "web", "run", "test:geometry-10m-performance"], "browser"],
["performance-texture-4k", "npm", ["--prefix", "web", "run", "test:texture-4k-performance"], "browser"],
["performance-texture-8k", "npm", ["--prefix", "web", "run", "test:texture-8k-performance"], "browser"],
["performance-simulation", "npm", ["--prefix", "web", "run", "test:simulation-cache-performance"], "browser"],
["performance-long-media", "npm", ["--prefix", "web", "run", "test:long-media-performance"], "browser"],
["vdb-native", "npm", ["--prefix", "web", "run", "test:vdb-native"]],
["vdb-server", "npm", ["--prefix", "web", "run", "test:vdb-server"]],
["vdb-opfs", "npm", ["--prefix", "web", "run", "test:vdb-opfs"], "browser"],
["vdb-webgpu", "npm", ["--prefix", "web", "run", "test:vdb-webgpu"], "browser"],
["vdb-viewport", "npm", ["--prefix", "web", "run", "test:vdb-viewport"], "browser"],
["vdb-faults", "npm", ["--prefix", "web", "run", "test:vdb-faults"], "browser"],
["v1-acceptance", "npm", ["--prefix", "web", "run", "release:v1-acceptance"], "acceptance"],
["offline-reproducibility", "npm", ["--prefix", "web", "run", "release:offline"]],
["archive-browser", "npm", ["--prefix", "web", "run", "test:archive-offline"], "browser"],
["binary-archive", "npm", ["--prefix", "web", "run", "test:binary-archive"]],
["source-archive", "npm", ["--prefix", "web", "run", "test:source-archive"]],
["deployment-runbook", "npm", ["--prefix", "web", "run", "test:deployment-runbook"]],
["upgrade-runbook", "npm", ["--prefix", "web", "run", "test:upgrade-runbook"]],
["rollback-runbook", "npm", ["--prefix", "web", "run", "test:rollback-runbook"]],
["operations-diagnostics", "npm", ["--prefix", "web", "run", "test:operations-diagnostics"]],
["operations-rehearsal", "npm", ["--prefix", "web", "run", "test:operations-rehearsal"]],
["rc-manifest", "npm", ["--prefix", "web", "run", "release:rc-manifest"]],
],
};
const commands = selfTestFailure
? [["injected-failure", process.execPath, ["-e", "process.stderr.write('injected CI lane failure\\n'); process.exit(17)"]]]
: laneCommands[lane];
const reportPath = path.resolve(process.env.CI_REPORT_PATH ?? path.join(releaseRoot, "ci-reports", `${lane}.json`));
const logRoot = path.join(path.dirname(reportPath), `${path.basename(reportPath, ".json")}-logs`);
fs.mkdirSync(logRoot, { recursive: true });
const digest = (value) => crypto.createHash("sha256").update(value).digest("hex");
const fileDigest = (file) => fs.existsSync(file) && fs.statSync(file).isFile() ? digest(fs.readFileSync(file)) : null;
function capture(command, args) {
const result = spawnSync(command, args, { cwd: repoRoot, encoding: "utf8", env: { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1" }, maxBuffer: 8 * 1024 * 1024 });
return result.status === 0 ? `${result.stdout ?? ""}${result.stderr ?? ""}`.trim() : "unavailable";
}
async function freePortRange(length = 1) {
for (let attempt = 0; attempt < 100; attempt++) {
const base = 20_000 + Math.floor(Math.random() * (40_000 - length));
const servers = [];
try {
for (let offset = 0; offset < length; offset++) {
const server = net.createServer();
await new Promise((resolve, reject) => {
server.once("error", reject);
server.listen(base + offset, "127.0.0.1", resolve);
});
servers.push(server);
}
return base;
}
catch {
// Try another contiguous range.
}
finally {
await Promise.all(servers.map((server) => new Promise((resolve) => server.close(resolve))));
}
}
throw new Error(`unable to allocate ${length} loopback port(s)`);
}
function artifactBindings() {
const files = {
packageJson: "web/package.json",
lockfile: "web/package-lock.json",
ledger: "docs/status/parity-ledger.json",
engineManifest: "web/app/public/engine-manifest.json",
releaseNotes: "docs/web/RELEASE_NOTES.md",
knownLimitations: "docs/web/KNOWN_LIMITATIONS.md",
releaseRecovery: "docs/web/RELEASE_RECOVERY.md",
sbom: "docs/web/sbom.spdx.json",
binaryArchive: "release/blender-web-offline.tar.gz",
sourceArchive: "release/blender-web-corresponding-source.tar.gz",
sha256Sums: "release/SHA256SUMS.txt",
rcManifest: "release/RC_MANIFEST.json",
};
return Object.fromEntries(Object.entries(files).map(([name, relative]) => {
const releaseOnly = ["binaryArchive", "sourceArchive", "sha256Sums", "rcManifest"].includes(name);
return [name, { path: relative, sha256: releaseOnly && lane !== "release" ? null : fileDigest(path.join(repoRoot, relative)) }];
}));
}
const environment = {
os: `${os.platform()} ${os.release()} ${os.arch()}`,
node: process.version,
npm: capture("npm", ["--version"]),
chrome: capture(process.env.CHROME_PATH ?? "google-chrome", ["--version"]),
blender: capture(process.env.BLENDER_BIN ?? "blender", ["--version"]).split("\n")[0],
emscripten: capture("bash", ["-lc", "source tools/web/emscripten-env.sh >/dev/null 2>&1 && emcc --version | head -1"]),
};
const commit = capture("git", ["rev-parse", "HEAD"]);
const records = [];
for (const [index, definition] of commands.entries()) {
const [id, command, args, portMode] = definition;
const env = { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1" };
let port;
if (portMode === "browser") {
port = await freePortRange();
env.WEB_TEST_PORT = String(port);
}
else if (portMode === "acceptance") {
port = await freePortRange(80);
env.WEB_ACCEPTANCE_PORT = String(port);
}
const started = Date.now();
const result = spawnSync(command, args, { cwd: repoRoot, encoding: "utf8", env, maxBuffer: 64 * 1024 * 1024 });
const output = `${result.stdout ?? ""}\n${result.stderr ?? ""}`.trim();
const logPath = path.join(logRoot, `${String(index + 1).padStart(2, "0")}-${id}.log`);
fs.writeFileSync(logPath, `${output}\n`);
const record = {
id,
command: [command, ...args].join(" "),
exitCode: result.status ?? 1,
durationMs: Date.now() - started,
...(port ? { loopbackPort: port } : {}),
output: { path: path.relative(repoRoot, logPath).replaceAll(path.sep, "/"), bytes: fs.statSync(logPath).size, sha256: fileDigest(logPath) },
};
records.push(record);
process.stdout.write(`[${lane} ${index + 1}/${commands.length}] ${id} exit=${record.exitCode} durationMs=${record.durationMs}${port ? ` port=${port}` : ""}\n`);
if (record.exitCode !== 0) break;
}
const status = records.length === commands.length && records.every((record) => record.exitCode === 0) ? "READY" : "FAILED";
const report = {
schemaVersion: 1,
lane,
status,
generatedAt: new Date().toISOString(),
commit,
environment,
bindings: artifactBindings(),
records,
retentionDays: lane === "quick" ? 7 : lane === "chromium" ? 14 : 30,
};
fs.mkdirSync(path.dirname(reportPath), { recursive: true });
const temporaryReport = `${reportPath}.tmp-${process.pid}`;
fs.writeFileSync(temporaryReport, `${JSON.stringify(report, null, 2)}\n`);
fs.renameSync(temporaryReport, reportPath);
fs.writeFileSync(`${reportPath}.sha256`, `${fileDigest(reportPath)} ${path.basename(reportPath)}\n`);
process.stdout.write(`ci-lane-report lane=${lane} status=${status} records=${records.length} path=${reportPath}\n`);
if (status !== "READY") process.exitCode = 1;