UNPKG

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
#!/usr/bin/env node 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, };