cypress-docker-swarm-runner
Version:
A CLI tool for running Cypress tests in parallel using Docker Swarm services.
146 lines (123 loc) • 5.25 kB
JavaScript
const { exec } = require("child_process");
const fs = require("fs");
// 📌 Execute Shell Commands with Timeout Handling
const execPromise = (command, timeout = 1800000) =>
new Promise((resolve, reject) => {
const child = exec(command, (error, stdout, stderr) => {
if (error) {
console.error(`❌ Command Failed: ${command}\n`, stderr);
reject(error);
} else {
resolve(stdout || stderr);
}
});
// Timeout protection to prevent infinite waits
const timeoutHandle = setTimeout(() => {
child.kill();
reject(new Error(`Timeout: Command "${command}" took too long`));
}, timeout);
child.stdout.on("data", (data) => console.log(`🟢 ${data.toString().trim()}`));
child.stderr.on("data", (data) => console.error(`🔴 ${data.toString().trim()}`));
child.on("exit", () => clearTimeout(timeoutHandle));
});
// 📌 Check if Docker Service Already Exists
const serviceExists = async (serviceName) => {
try {
await execPromise(`docker service inspect ${serviceName}`);
console.log(`🔍 Service ${serviceName} already exists. Skipping creation.`);
return true;
} catch {
return false;
}
};
// 📌 Monitor Docker Service Execution (Handles 15-20 min runs)
const monitorService = async (serviceName, maxDuration = 1800) => {
let elapsedTime = 0;
let failureCount = 0;
while (elapsedTime < maxDuration) {
const statusCommand = `docker service ps ${serviceName} --format "{{.CurrentState}}" | head -n 1`;
const serviceStatus = await execPromise(statusCommand).catch(() => "Failed");
console.log(`📊 Service ${serviceName} status: ${serviceStatus.trim()}`);
if (serviceStatus.includes("Complete")) return "Completed";
if (serviceStatus.includes("Failed")) {
failureCount++;
if (failureCount >= 3) return "Failed";
}
// Log progress every 5 minutes
if (elapsedTime % 300 === 0) {
console.log(`⏳ Service ${serviceName} is still running... (${elapsedTime / 60} min elapsed)`);
}
await new Promise((resolve) => setTimeout(resolve, 10000)); // Check every 10s
elapsedTime += 10;
}
console.log(`⏰ Timeout! Service ${serviceName} exceeded max duration.`);
return "Timeout";
};
// 📌 Create Cypress Test Services in Docker Swarm
const createServices = async ({ specTestCasesMap, SECRET_NAME, IMAGE_NAME }) => {
try {
const serviceCreationTasks = Array.from(specTestCasesMap.entries()).map(
async ([specPath, testCaseIds]) => {
const specFileName = specPath.split("/").pop().toLowerCase();
const normalizedSpecName = specFileName.replace(/[^a-z0-9-]/g, "-");
const serviceName = `cypress_test_service_${normalizedSpecName}`;
const tcids = testCaseIds.join("; ");
if (await serviceExists(serviceName)) return;
const command = `docker service create --name ${serviceName} \
--with-registry-auth \
--secret ${SECRET_NAME} \
--replicas 1 \
--env SPECFILE=${specPath} \
--entrypoint bash \
--restart-condition none \
${IMAGE_NAME} \
-c 'echo "Running spec file: $SPECFILE" && npx cypress run -b chrome --headless --spec $SPECFILE --env grep="${tcids}" || exit 1'`;
console.log(`🚀 Creating service: ${serviceName}`);
await execPromise(command);
console.log(`✅ Service ${serviceName} created successfully.`);
const result = await monitorService(serviceName, 1800); // 30 min timeout
console.log(`🔄 Service ${serviceName} finished with status: ${result}`);
// 📝 Save logs as HTML
try {
const stripAnsi = (await import('strip-ansi')).default;
const logFileName = `logs/${serviceName}.html`;
let logs = await execPromise(`docker service logs ${serviceName}`);
logs = stripAnsi(logs); // Optional: Clean colors
const htmlContent = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Logs for ${serviceName}</title>
<style>
body { font-family: monospace; white-space: pre-wrap; background: #111; color: #eee; padding: 20px; }
.error { color: red; }
.success { color: lightgreen; }
</style>
</head>
<body>
<h1>Logs for ${serviceName}</h1>
<pre>${logs}</pre>
</body>
</html>
`;
fs.mkdirSync("logs", { recursive: true });
fs.writeFileSync(logFileName, htmlContent);
console.log(`📝 HTML Logs for ${serviceName} saved to ${logFileName}`);
} catch (logError) {
console.error(`⚠️ Failed to capture logs for ${serviceName}: ${logError.message}`);
}
console.log(`🗑 Removing service: ${serviceName}`);
await execPromise(`docker service rm ${serviceName}`);
}
);
await Promise.all(serviceCreationTasks);
} catch (error) {
console.error(`❌ Failed to create service. Reason: ${error.message || error}`);
throw error;
}
};
module.exports = {
createServices,
};