chore: isolate build artifacts and storybook smoke infra
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
const { spawn } = require("node:child_process");
|
||||
const fs = require("node:fs/promises");
|
||||
const path = require("node:path");
|
||||
|
||||
const repoRoot = path.resolve(__dirname, "../../..");
|
||||
const docsDir = path.resolve(repoRoot, "apps/docs");
|
||||
const stateFile = path.resolve(repoRoot, ".artifacts/test-results/storybook-server.json");
|
||||
|
||||
const baseURL = "http://127.0.0.1:6006";
|
||||
const startupTimeoutMs = 120_000;
|
||||
const shutdownTimeoutMs = 10_000;
|
||||
const pollIntervalMs = 250;
|
||||
const storybookCommand = process.platform === "win32" ? "pnpm.cmd" : "pnpm";
|
||||
const storybookArgs = ["run", "storybook"];
|
||||
const reuseExistingServer = !process.env.CI;
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function isServerResponsive() {
|
||||
try {
|
||||
const response = await fetch(baseURL, {
|
||||
redirect: "manual",
|
||||
signal: AbortSignal.timeout(1_000)
|
||||
});
|
||||
|
||||
return response.status < 404 || [400, 401, 402, 403].includes(response.status);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function waitFor(condition, timeoutMs, failureMessage) {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
|
||||
while (Date.now() < deadline) {
|
||||
if (await condition()) {
|
||||
return;
|
||||
}
|
||||
|
||||
await sleep(pollIntervalMs);
|
||||
}
|
||||
|
||||
throw new Error(failureMessage);
|
||||
}
|
||||
|
||||
async function ensureStateDir() {
|
||||
await fs.mkdir(path.dirname(stateFile), { recursive: true });
|
||||
}
|
||||
|
||||
async function readServerState() {
|
||||
try {
|
||||
const raw = await fs.readFile(stateFile, "utf8");
|
||||
return JSON.parse(raw);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function clearServerState() {
|
||||
await fs.rm(stateFile, { force: true });
|
||||
}
|
||||
|
||||
async function stopProcess(pid) {
|
||||
try {
|
||||
if (process.platform === "win32") {
|
||||
process.kill(pid, "SIGTERM");
|
||||
} else {
|
||||
process.kill(-pid, "SIGTERM");
|
||||
}
|
||||
} catch (error) {
|
||||
if (error && error.code === "ESRCH") {
|
||||
return;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
try {
|
||||
await waitFor(
|
||||
async () => !(await isServerResponsive()),
|
||||
shutdownTimeoutMs,
|
||||
`Timed out waiting ${shutdownTimeoutMs}ms for Storybook to stop.`
|
||||
);
|
||||
} catch {
|
||||
try {
|
||||
if (process.platform === "win32") {
|
||||
process.kill(pid, "SIGKILL");
|
||||
} else {
|
||||
process.kill(-pid, "SIGKILL");
|
||||
}
|
||||
} catch (error) {
|
||||
if (!error || error.code !== "ESRCH") {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function startStorybookServer() {
|
||||
if (await isServerResponsive()) {
|
||||
if (reuseExistingServer) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error(`${baseURL} is already in use, and CI runs must start their own Storybook server.`);
|
||||
}
|
||||
|
||||
const child = spawn(storybookCommand, storybookArgs, {
|
||||
cwd: docsDir,
|
||||
detached: process.platform !== "win32",
|
||||
env: process.env,
|
||||
shell: false,
|
||||
stdio: "ignore"
|
||||
});
|
||||
|
||||
const exitPromise = new Promise((_, reject) => {
|
||||
child.once("error", (error) => {
|
||||
reject(error);
|
||||
});
|
||||
|
||||
child.once("exit", (code, signal) => {
|
||||
reject(
|
||||
new Error(
|
||||
`Storybook exited before becoming ready (code: ${code ?? "null"}, signal: ${signal ?? "null"}).`
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
try {
|
||||
await Promise.race([
|
||||
waitFor(
|
||||
async () => isServerResponsive(),
|
||||
startupTimeoutMs,
|
||||
`Timed out waiting ${startupTimeoutMs}ms for Storybook on ${baseURL}.`
|
||||
),
|
||||
exitPromise
|
||||
]);
|
||||
} catch (error) {
|
||||
await stopProcess(child.pid);
|
||||
throw error;
|
||||
}
|
||||
|
||||
child.unref();
|
||||
|
||||
await ensureStateDir();
|
||||
await fs.writeFile(stateFile, JSON.stringify({ pid: child.pid }, null, 2));
|
||||
}
|
||||
|
||||
async function stopStorybookServer() {
|
||||
const state = await readServerState();
|
||||
|
||||
if (!state) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await stopProcess(state.pid);
|
||||
} finally {
|
||||
await clearServerState();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
clearServerState,
|
||||
readServerState,
|
||||
startStorybookServer,
|
||||
stopStorybookServer
|
||||
};
|
||||
Reference in New Issue
Block a user