mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
194 lines
5.6 KiB
JavaScript
194 lines
5.6 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Freight e2e launcher — one command, no manual steps:
|
|
*
|
|
* node e2e/freight/scripts/e2e.mjs <up|run|open|ci|down> [cypress args...]
|
|
*
|
|
* - run/open/ci auto-start the docker stack (build + wait healthy) if it
|
|
* isn't already running, then launch Cypress pointed at the right ports.
|
|
* - Host ports default to 3101/5373/5383/5533/9310/9311; any default that is
|
|
* busy is replaced by the next free port. Chosen ports are written to
|
|
* e2e/freight/.e2e-ports.json (gitignored) and reused while the stack is
|
|
* up, so cypress and compose always agree.
|
|
* - Extra args are forwarded to Cypress: `pnpm e2e:freight:run --spec ...`.
|
|
*
|
|
* No dependencies — plain Node, spawns `docker compose` and `pnpm`.
|
|
*/
|
|
|
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
import { createServer } from "node:net";
|
|
import { dirname, join, resolve } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const e2eDir = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
const repoRoot = resolve(e2eDir, "..", "..");
|
|
const stateFile = join(e2eDir, ".e2e-ports.json");
|
|
const composeBase = ["compose", "-f", join(repoRoot, "docker-compose.e2e.yaml")];
|
|
|
|
const DEFAULT_PORTS = {
|
|
E2E_API_PORT: 3101,
|
|
E2E_PORTAL_PORT: 5373,
|
|
E2E_BACKOFFICE_PORT: 5383,
|
|
E2E_DB_PORT: 5533,
|
|
E2E_MINIO_PORT: 9310,
|
|
E2E_MINIO_CONSOLE_PORT: 9311,
|
|
};
|
|
|
|
// Long-running services that must be up before tests (minio-init exits).
|
|
const SERVICES = [
|
|
"postgres-freight-e2e",
|
|
"minio-e2e",
|
|
"freight-api-e2e",
|
|
"freight-portal-e2e",
|
|
"freight-backoffice-e2e",
|
|
];
|
|
|
|
function fail(msg) {
|
|
console.error(`\ne2e: ${msg}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
function preflight() {
|
|
try {
|
|
execFileSync("docker", ["info"], { stdio: "ignore" });
|
|
} catch {
|
|
fail("docker is not running (or not installed) — start Docker and retry.");
|
|
}
|
|
if (!existsSync(join(repoRoot, ".npmrc"))) {
|
|
fail(
|
|
".npmrc missing at repo root — image builds need GitHub Packages auth " +
|
|
"for @tria-plc (same file the main docker-compose.yaml uses).",
|
|
);
|
|
}
|
|
}
|
|
|
|
function isPortFree(port) {
|
|
return new Promise((res) => {
|
|
const srv = createServer();
|
|
srv.once("error", () => res(false));
|
|
srv.once("listening", () => srv.close(() => res(true)));
|
|
srv.listen(port);
|
|
});
|
|
}
|
|
|
|
function stackRunning(env) {
|
|
try {
|
|
const out = execFileSync(
|
|
"docker",
|
|
[...composeBase, "ps", "--services", "--status", "running"],
|
|
{ encoding: "utf8", env, stdio: ["ignore", "pipe", "ignore"] },
|
|
);
|
|
const running = new Set(out.split("\n").filter(Boolean));
|
|
return SERVICES.every((s) => running.has(s));
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* While the stack runs, ports are whatever it was started with (state file,
|
|
* else the defaults — a hand-started stack used the compose defaults). Only a
|
|
* fresh start gets to scan for free ports.
|
|
*/
|
|
async function resolvePorts() {
|
|
if (stackRunning(process.env)) {
|
|
return existsSync(stateFile)
|
|
? JSON.parse(readFileSync(stateFile, "utf8"))
|
|
: { ...DEFAULT_PORTS };
|
|
}
|
|
const ports = {};
|
|
const taken = new Set();
|
|
for (const [name, preferred] of Object.entries(DEFAULT_PORTS)) {
|
|
let port = preferred;
|
|
while (taken.has(port) || !(await isPortFree(port))) port += 1;
|
|
taken.add(port);
|
|
ports[name] = port;
|
|
if (port !== preferred)
|
|
console.log(`e2e: port ${preferred} busy → ${name}=${port}`);
|
|
}
|
|
return ports;
|
|
}
|
|
|
|
function envFor(ports) {
|
|
return {
|
|
...process.env,
|
|
...Object.fromEntries(
|
|
Object.entries(ports).map(([k, v]) => [k, String(v)]),
|
|
),
|
|
CYPRESS_BASE_URL: `http://localhost:${ports.E2E_BACKOFFICE_PORT}`,
|
|
CYPRESS_API_URL: `http://localhost:${ports.E2E_API_PORT}`,
|
|
CYPRESS_PORTAL_URL: `http://localhost:${ports.E2E_PORTAL_PORT}`,
|
|
E2E_DB_URL: `postgres://edr_e2e:edr_e2e@localhost:${ports.E2E_DB_PORT}/edr_freight_e2e`,
|
|
};
|
|
}
|
|
|
|
function compose(args, env) {
|
|
const { status } = spawnSync("docker", [...composeBase, ...args], {
|
|
stdio: "inherit",
|
|
env,
|
|
});
|
|
return status ?? 1;
|
|
}
|
|
|
|
function up(ports, env) {
|
|
preflight();
|
|
console.log(
|
|
`e2e: starting stack — api :${ports.E2E_API_PORT} portal :${ports.E2E_PORTAL_PORT} backoffice :${ports.E2E_BACKOFFICE_PORT} db :${ports.E2E_DB_PORT}`,
|
|
);
|
|
const status = compose(["up", "-d", "--build", "--wait"], env);
|
|
if (status !== 0)
|
|
fail(
|
|
"stack failed to become healthy. Inspect with:\n" +
|
|
" docker compose -f docker-compose.e2e.yaml logs freight-api-e2e",
|
|
);
|
|
writeFileSync(stateFile, JSON.stringify(ports, null, 2) + "\n");
|
|
}
|
|
|
|
function ensureUp(ports, env) {
|
|
if (stackRunning(env)) return;
|
|
up(ports, env);
|
|
}
|
|
|
|
function runPnpm(script, extra, env) {
|
|
const { status } = spawnSync(
|
|
"pnpm",
|
|
["--filter", "@edr/freight-e2e", "run", script, ...extra],
|
|
{ cwd: repoRoot, stdio: "inherit", env },
|
|
);
|
|
process.exit(status ?? 1);
|
|
}
|
|
|
|
const [cmd, ...extra] = process.argv.slice(2);
|
|
const ports = await resolvePorts();
|
|
const env = envFor(ports);
|
|
|
|
switch (cmd) {
|
|
case "up":
|
|
up(ports, env);
|
|
break;
|
|
case "run":
|
|
ensureUp(ports, env);
|
|
runPnpm("cy:run", extra, env);
|
|
break;
|
|
case "open":
|
|
ensureUp(ports, env);
|
|
runPnpm("cy:open", extra, env);
|
|
break;
|
|
case "ci":
|
|
ensureUp(ports, env);
|
|
process.exit(compose(["--profile", "cypress", "run", "--rm", "cypress", ...extra], env));
|
|
break;
|
|
case "down":
|
|
process.exit(
|
|
(() => {
|
|
const status = compose(["down", "-v", "--remove-orphans"], env);
|
|
rmSync(stateFile, { force: true });
|
|
return status;
|
|
})(),
|
|
);
|
|
break;
|
|
default:
|
|
fail(`unknown command "${cmd ?? ""}" — use up | run | open | ci | down`);
|
|
}
|