#!/usr/bin/env node // Safe runner for the k6 load-test suite. // // SKIPs cleanly (exit 0) when the k6 binary is missing or the target URL is // unreachable, so this script is safe to wire into CI lanes that may run on // machines without k6 installed. Only fails (non-zero exit) when k6 itself // reports a threshold or runtime failure. import path from "node:path"; import url from "node:url"; import { spawnSync } from "node:child_process"; const DEFAULT_TARGET_URL = "http://localhost:3000"; const DEFAULT_SCENARIO_PATH = "tests/load/scenarios.js"; const REACHABILITY_TIMEOUT_SECONDS = 3; /** * Test whether the k6 binary is available on PATH. * Returns the trimmed version string on success, null on failure. */ export function detectK6Binary({ spawn = spawnSync } = {}) { const result = spawn("k6", ["version"], { encoding: "utf8" }); if (result.error || result.status !== 0) { return null; } return (result.stdout || "").trim(); } /** * Test whether the configured target URL responds to an HTTP request. * Uses curl with a short timeout so the runner does not hang when the * target environment is offline. Returns true on reachable, false otherwise. */ export function isTargetReachable(targetUrl, { spawn = spawnSync, timeoutSeconds = REACHABILITY_TIMEOUT_SECONDS } = {}) { const result = spawn( "curl", [ "-fsS", "--max-time", String(timeoutSeconds), "-o", "/dev/null", "-w", "%{http_code}", targetUrl, ], { encoding: "utf8" }, ); if (result.error) { return false; } // curl -fsS exits non-zero on HTTP >= 400, but for reachability we accept // any successful TCP+HTTP roundtrip. Re-check by looking at the captured // status code in stdout when curl exited non-zero. if (result.status === 0) { return true; } const code = (result.stdout || "").trim(); return /^[1-5]\d\d$/.test(code); } /** * Parse argv into a configuration object. * --target override K6_TARGET_URL * --scenario override the default composite scenario script * --skip-reachability skip the curl probe (useful when the target is * only reachable from within a Docker network) */ export function parseArgs(argv) { const config = { targetUrl: null, scenarioPath: null, skipReachability: false, }; for (let index = 0; index < argv.length; index += 1) { const arg = argv[index]; if (arg === "--target" || arg === "--target-url") { config.targetUrl = argv[index + 1] || null; index += 1; } else if (arg === "--scenario") { config.scenarioPath = argv[index + 1] || null; index += 1; } else if (arg === "--skip-reachability") { config.skipReachability = true; } } return config; } /** * Resolve the final runtime configuration from argv + env, applying defaults. */ export function resolveConfig({ argv = [], env = process.env } = {}) { const parsed = parseArgs(argv); const targetUrl = parsed.targetUrl || env.K6_TARGET_URL || DEFAULT_TARGET_URL; const scenarioPath = parsed.scenarioPath || DEFAULT_SCENARIO_PATH; return { targetUrl, scenarioPath, skipReachability: parsed.skipReachability, }; } /** * Run the load-test suite. Returns an object describing the outcome so the * CLI wrapper can map it to an exit code. */ export function runLoadTests({ argv = [], env = process.env, spawn = spawnSync, stdout = (line) => process.stdout.write(`${line}\n`), stderr = (line) => process.stderr.write(`${line}\n`), } = {}) { const config = resolveConfig({ argv, env }); const version = detectK6Binary({ spawn }); if (!version) { stdout( "SKIP k6 binary not installed; download from https://k6.io/docs/getting-started/installation/", ); return { status: "skip", reason: "binary-missing", exitCode: 0 }; } if (!config.skipReachability && !isTargetReachable(config.targetUrl, { spawn })) { stdout(`SKIP target ${config.targetUrl} not reachable`); return { status: "skip", reason: "target-unreachable", exitCode: 0 }; } stdout(`PASS k6 detected (${version}); running ${config.scenarioPath} against ${config.targetUrl}`); const result = spawn("k6", ["run", config.scenarioPath], { stdio: "inherit", env: { ...env, K6_TARGET_URL: config.targetUrl }, }); if (result.error) { stderr(`FAIL k6 run failed to start: ${result.error.message}`); return { status: "fail", reason: "spawn-error", exitCode: 1 }; } if (typeof result.status === "number" && result.status !== 0) { stderr(`FAIL k6 run exited with status ${result.status}`); return { status: "fail", reason: "non-zero-exit", exitCode: result.status }; } return { status: "pass", reason: "ok", exitCode: 0 }; } const isCli = process.argv[1] && url.fileURLToPath(import.meta.url) === path.resolve(process.argv[1]); if (isCli) { const outcome = runLoadTests({ argv: process.argv.slice(2) }); process.exit(outcome.exitCode); }