Advance M7 workflows and release operations
This commit is contained in:
70
tools/web/archive-deployment-server.mjs
Normal file
70
tools/web/archive-deployment-server.mjs
Normal file
@@ -0,0 +1,70 @@
|
||||
import assert from "node:assert/strict";
|
||||
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,
|
||||
loadDeploymentContract,
|
||||
} from "./deployment-http-server.mjs";
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
|
||||
function option(name, fallback) {
|
||||
const index = process.argv.indexOf(name);
|
||||
return index >= 0 ? process.argv[index + 1] : fallback;
|
||||
}
|
||||
|
||||
function assertSafeArchiveEntries(archive) {
|
||||
const entries = execFileSync("tar", ["-tzf", archive], { encoding: "utf8" })
|
||||
.split("\n")
|
||||
.filter(Boolean);
|
||||
assert.ok(entries.length > 0, "binary archive is empty");
|
||||
for (const entry of entries) {
|
||||
assert.ok(!entry.startsWith("/"), `binary archive contains an absolute path: ${entry}`);
|
||||
assert.ok(!entry.split("/").includes(".."), `binary archive contains path traversal: ${entry}`);
|
||||
assert.ok(entry === "blender-web-offline/" || entry.startsWith("blender-web-offline/"), `unexpected binary archive root: ${entry}`);
|
||||
}
|
||||
}
|
||||
|
||||
const archive = path.resolve(option("--archive", path.join(repoRoot, "release/blender-web-offline.tar.gz")));
|
||||
const port = Number.parseInt(option("--port", process.env.WEB_TEST_PORT ?? "5189"), 10);
|
||||
assert.ok(fs.statSync(archive).isFile(), `binary archive is missing: ${archive}`);
|
||||
assert.ok(Number.isSafeInteger(port) && port > 0 && port < 65536, `invalid deployment port: ${port}`);
|
||||
assertSafeArchiveEntries(archive);
|
||||
|
||||
const workspace = fs.mkdtempSync(path.join(os.tmpdir(), "blender-archive-server-"));
|
||||
execFileSync("tar", ["--no-same-owner", "--no-same-permissions", "-xzf", archive, "-C", workspace]);
|
||||
const bundleRoot = path.join(workspace, "blender-web-offline");
|
||||
const staticRoot = path.join(bundleRoot, "app");
|
||||
assert.ok(fs.statSync(path.join(staticRoot, "index.html")).isFile(), "extracted binary archive has no app/index.html");
|
||||
assert.ok(!staticRoot.startsWith(`${repoRoot}${path.sep}`), "archive server must not serve a workspace directory");
|
||||
|
||||
const contract = loadDeploymentContract(path.join(bundleRoot, "deployment-contract.json"));
|
||||
const server = createDeploymentHttpServer({ root: staticRoot, contract });
|
||||
let closing = false;
|
||||
|
||||
async function close(exitCode = 0) {
|
||||
if (closing) return;
|
||||
closing = true;
|
||||
await new Promise((resolve) => server.close(() => resolve()));
|
||||
fs.rmSync(workspace, { recursive: true, force: true });
|
||||
process.exit(exitCode);
|
||||
}
|
||||
|
||||
process.once("SIGINT", () => void close(0));
|
||||
process.once("SIGTERM", () => void close(0));
|
||||
process.once("uncaughtException", (error) => {
|
||||
process.stderr.write(`${error.stack ?? error}\n`);
|
||||
void close(1);
|
||||
});
|
||||
process.once("unhandledRejection", (error) => {
|
||||
process.stderr.write(`${error instanceof Error ? error.stack : String(error)}\n`);
|
||||
void close(1);
|
||||
});
|
||||
|
||||
const origin = await listenDeploymentHttpServer(server, { port });
|
||||
process.stdout.write(`archive-deployment-server ${origin} root=${staticRoot} archive=${archive}\n`);
|
||||
|
||||
Reference in New Issue
Block a user