everything-dev
Version:
A consolidated product package for building Module Federation apps with oRPC APIs.
271 lines (269 loc) • 11.3 kB
JavaScript
import { mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { spawnSync } from "node:child_process";
import process from "node:process";
//#region src/services/deploy/smoke.ts
const BOOT_DEADLINE_MS = 24e4;
const POLL_INTERVAL_MS = 1e3;
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
function run(cmd, args) {
const result = spawnSync(cmd, args, { encoding: "utf8" });
if (result.error) throw result.error;
return {
status: result.status,
stdout: result.stdout ?? "",
stderr: result.stderr ?? ""
};
}
const docker = (args) => run("docker", args);
async function fetchStatusOk(url) {
const response = await fetch(url);
if (!response.ok) throw new Error(`${url} → ${response.status}`);
return response;
}
/** Bounded poll: resolves with the check's result once it passes, rejects at the deadline. */
async function pollUntil(name, check, deadlineMs = BOOT_DEADLINE_MS) {
const deadline = Date.now() + deadlineMs;
let lastFailure = "no attempt";
let attempt = 0;
while (Date.now() < deadline) {
attempt += 1;
try {
const detail = await check();
console.log(` ✓ ${name} (attempt ${attempt})`);
return detail;
} catch (error) {
lastFailure = error instanceof Error ? error.message : String(error);
}
await sleep(POLL_INTERVAL_MS);
}
throw new Error(`${name} did not pass within the boot deadline — last failure: ${lastFailure}`);
}
/** Resolves "exited" once the boot container has exited — the start stack
* exits non-zero when a service fails to boot, so a dead container means the
* remaining pollUntil time would be wasted polling nothing. Resolves
* undefined when aborted. */
function watchContainerExit(container) {
const controller = { aborted: false };
return {
done: (async () => {
while (!controller.aborted) {
if ((docker([
"inspect",
"-f",
"{{.State.Running}}",
container
]).stdout ?? "").trim() === "false") return "exited";
await sleep(2e3);
}
})(),
abort: () => controller.aborted = true
};
}
/** While the container is still running, probe the CDN from inside it — a
* boot failure on :3000 is either the CDN edge (network-class: unreachable,
* 429/5xx, challenge) or a boot bug (module eval, shared-dependency skew,
* server start), and the two need opposite fixes. Falls back to a one-off
* container from the same image (equivalent network class) when the boot
* container already exited. */
function probeCdnFromContainer(container, image, account, gateway) {
const base = `${process.env.SMOKE_CDN_BASE ?? "https://cdn.everything.dev"}/bundles/${account}/${gateway}/host`;
const script = `
const base = ${JSON.stringify(base)};
(async () => {
const urls = [base + "/mf-manifest.json"];
try {
const manifest = await fetch(urls[0]).then((r) => (r.ok ? r.json() : null));
const expose = manifest?.exposes?.[0] ?? {};
const files = expose.assets?.js?.sync ?? expose.files?.js?.sync ?? [];
for (const f of files.slice(0, 3)) urls.push(base + "/" + f);
} catch {}
for (const url of urls) {
try {
const res = await fetch(url);
const bytes = res.ok ? (await res.arrayBuffer()).byteLength : 0;
console.log("probe " + url + " -> " + res.status + (bytes ? " " + bytes + "B" : ""));
} catch (error) {
console.log("probe " + url + " -> FETCH_FAILED " + String(error));
}
}
})();
`;
let probe = docker([
"exec",
container,
"node",
"-e",
script
]);
if (probe.status !== 0) probe = docker([
"run",
"--rm",
image,
"node",
"-e",
script
]);
const output = `${probe.stdout}${probe.stderr}`.trim();
console.error("--- in-container CDN probe ---");
if (output) console.error(output);
else console.error(`(no output — probe exit ${probe.status}: ${probe.stderr.trim()})`);
const verdicts = output.split("\n").filter((line) => line.startsWith("probe ")).map((line) => {
const status = line.split(" -> ")[1] ?? "";
if (status.startsWith("FETCH_FAILED")) return "unreachable";
if (Number.parseInt(status, 10) === 200) return "ok";
return "edge-error";
});
return verdicts.length === 0 ? "probe did not run — see probe exit above" : verdicts.every((v) => v === "ok") ? "CDN reachable from inside the container — app-class boot failure; the cause is in the captured logs, not the network" : verdicts.some((v) => v === "unreachable") ? "CDN unreachable from inside the container — network-class failure (runner egress or CDN edge)" : "CDN answered non-200 from inside the container — edge or published-config mismatch (see statuses above)";
}
/** Copy the stack's .bos/logs out of the container (into the CI artifact
* path) and capture the full container stdout — the failure reason for a
* failed service only exists inside the container, which otherwise gets
* deleted, and a tail window can cut the error behind spinner output. */
function captureContainerLogs(container, dockerLogs) {
const target = join(".bos", "logs", "smoke");
mkdirSync(target, { recursive: true });
const cp = docker([
"cp",
`${container}:/app/.bos/logs/.`,
target
]);
if (cp.status !== 0) console.error(`smoke: could not copy container logs: ${cp.stderr.trim()}`);
else {
const walk = (dir) => readdirSync(dir, { withFileTypes: true }).flatMap((entry) => entry.isDirectory() ? walk(join(dir, entry.name)) : [join(dir, entry.name)]);
for (const file of walk(target)) {
if (!file.endsWith(".log")) continue;
const lines = readFileSync(file, "utf8").split("\n");
console.error(`--- ${file} (last 60 of ${lines.length} lines) ---`);
console.error(lines.slice(-60).join("\n"));
}
}
writeFileSync(join(target, "smoke-stdout.log"), dockerLogs.join("\n"));
console.error(`--- container stdout (full, ${dockerLogs.length} lines) → ${target}/smoke-stdout.log ---`);
console.error(dockerLogs.slice(-100).join("\n"));
}
async function runSmoke(options) {
const port = options.port ?? 3e3;
const keep = options.keep ?? false;
const baseUrl = `http://localhost:${port}`;
const container = `everything-dev-smoke-${process.pid}`;
const image = options.image;
const envPairs = {
BOS_ACCOUNT: options.account ?? process.env.SMOKE_BOS_ACCOUNT ?? "dev.everything.near",
BOS_GATEWAY: options.gateway ?? process.env.SMOKE_BOS_GATEWAY ?? "everything.dev",
API_DATABASE_URL: options.apiDatabaseUrl ?? process.env.SMOKE_API_DATABASE_URL ?? "postgres://everythingdev:everythingdev@host.docker.internal:5434/api_test_db",
AUTH_DATABASE_URL: options.authDatabaseUrl ?? process.env.SMOKE_AUTH_DATABASE_URL ?? "postgres://everythingdev:everythingdev@host.docker.internal:5435/auth_test_db",
BETTER_AUTH_SECRET: options.authSecret ?? process.env.SMOKE_BETTER_AUTH_SECRET ?? "smoke-test-secret-do-not-use-in-production"
};
const up = docker([
"run",
"-d",
"--name",
container,
"-p",
`${port}:${port}`,
"--add-host=host.docker.internal:host-gateway",
...Object.entries(envPairs).flatMap(([key, value]) => ["-e", `${key}=${value}`]),
image
]);
if (up.status !== 0) return {
ok: false,
image,
error: `docker run failed: ${up.stderr.trim()}`
};
try {
const exitWatch = watchContainerExit(container);
const bootChecks = Promise.all([
pollUntil("health", () => fetchStatusOk(`${baseUrl}/health`).then((r) => `status ${r.status}`)),
pollUntil("ssr page", async () => {
const response = await fetchStatusOk(`${baseUrl}/`);
const contentType = response.headers.get("content-type") ?? "";
if (!contentType.includes("text/html")) throw new Error(`/ content-type ${contentType}`);
const body = await response.text();
if (!/<html/i.test(body)) throw new Error("/ did not return an HTML document");
if (body.includes("SSR unavailable")) throw new Error("/ served the client-shell fallback — the server render failed");
return `${body.length} bytes`;
}),
pollUntil("api spec", async () => {
const spec = await (await fetchStatusOk(`${baseUrl}/api/spec.json`)).json();
if (!spec.openapi && !spec.paths) throw new Error("/api/spec.json is not an OpenAPI document");
return `${Object.keys(spec.paths ?? {}).length} paths`;
})
]);
bootChecks.catch(() => {});
if (await Promise.race([bootChecks.then(() => "ready"), exitWatch.done]).finally(() => exitWatch.abort()) === "exited") throw new Error("boot container exited — a service failed to start (the cause is in the captured logs below)");
const [health, ssr, api] = await bootChecks;
console.log(` ℹ health: ${health}; ssr: ${ssr}; api: ${api}`);
console.log(` … bos mf check (in-image)`);
const mf = await pollUntil("bos mf check", async () => {
const result = docker([
"exec",
container,
"node",
"./node_modules/everything-dev/dist/cli.mjs",
"mf",
"check"
]);
if (result.status !== 0) throw new Error(`bos mf check failed (exit ${result.status})\n${result.stdout}${result.stderr}`);
return (result.stdout.trim().split("\n").at(-1) ?? "").slice(0, 400);
}, 6e4);
console.log(` … platform manifest (in-image)`);
const platformResult = docker([
"exec",
container,
"node",
"-e",
`const m = JSON.parse(require('fs').readFileSync('./packages/everything-dev/dist/platform-manifest.json','utf8')); if (m.kind !== 'everything-dev/platform-manifest' || !m.frameworkVersion) process.exit(1); console.log(m.frameworkVersion)`
]);
if (platformResult.status !== 0) throw new Error(`in-image platform manifest missing or invalid — the image was not built with the release contract (ADR 0034)\n${platformResult.stdout}${platformResult.stderr}`);
const platformVersion = platformResult.stdout.trim();
const sizeResult = docker([
"image",
"inspect",
"-f",
"{{.Size}}",
image
]);
if (sizeResult.status !== 0) throw new Error(`image inspect failed: ${sizeResult.stderr}`);
const mb = (Number(sizeResult.stdout.trim()) / 1048576).toFixed(1);
console.log(` ℹ image size: ${mb} MB (${image})`);
const summary = `health: ${health}; ssr: ${ssr}; api: ${api}; mf compat: ${mf}; platform: ${platformVersion}`;
console.log("smoke: PASS");
return {
ok: true,
image,
summary
};
} catch (error) {
const failure = error instanceof Error ? error.message : String(error);
console.error(`smoke: FAIL — ${failure}`);
const diagnosis = probeCdnFromContainer(container, image, envPairs.BOS_ACCOUNT, envPairs.BOS_GATEWAY);
console.error(`smoke diagnosis: ${diagnosis}`);
const stop = docker([
"stop",
"-t",
"15",
container
]);
if (stop.status !== 0) console.error(`smoke: graceful stop failed — ${stop.stderr.trim()}`);
const logs = docker(["logs", container]);
captureContainerLogs(container, `${logs.stdout}${logs.stderr}`.split("\n"));
if (/pins no version manifest|redeploy required/i.test(`${logs.stdout}${logs.stderr}`)) console.error("smoke: the published config predates version-manifest pins — run `bos deploy` to republish with pins; the smoke gate goes green after that");
return {
ok: false,
image,
diagnosis,
error: failure
};
} finally {
if (!keep) docker([
"rm",
"-f",
container
]);
else console.log(`smoke: kept container ${container}`);
}
}
//#endregion
export { runSmoke };
//# sourceMappingURL=smoke.mjs.map