Files
n8n/packages/testing/playwright/scripts/distribute-tests.mjs
T
alighasami 3d5eaf9445
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
first commit
2026-03-17 16:22:57 +03:30

137 lines
4.8 KiB
JavaScript

#!/usr/bin/env node
// @ts-check
/**
* n8n CI Adapter for Test Distribution
*
* Thin wrapper that calls `janitor orchestrate` for generic shard distribution,
* then maps capabilities to n8n-specific Docker images for the CI matrix.
*
* Usage:
* node distribute-tests.mjs --matrix <shards> --orchestrate # GitHub Actions matrix with images
* node distribute-tests.mjs --matrix <shards> # Simple matrix (no distribution)
* node distribute-tests.mjs <shards> <index> # Specs for a single shard
*/
import { execFileSync } from 'child_process';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const PLAYWRIGHT_DIR = path.resolve(__dirname, '..');
const REPO_ROOT = path.resolve(__dirname, '..', '..', '..', '..');
const JANITOR_CLI = path.resolve(__dirname, '..', '..', 'janitor', 'dist', 'cli.js');
const PLAYWRIGHT_PREFIX = path.relative(REPO_ROOT, PLAYWRIGHT_DIR) + path.sep;
const CONTAINER_STARTUP_TIME = 22_500; // 22.5s average per fixture
const CAPABILITY_IMAGES = {
email: ['mailpit'],
kafka: ['kafka'],
observability: ['victoriaLogs', 'victoriaMetrics', 'vector', 'jaeger', 'n8nTracer'],
oidc: ['keycloak'],
proxy: ['mockserver'],
'source-control': ['gitea'],
};
const BASE_IMAGES = ['postgres', 'redis', 'caddy', 'n8n', 'taskRunner'];
function getRequiredImages(capabilities) {
const images = new Set(BASE_IMAGES);
for (const cap of capabilities) {
const capImages = CAPABILITY_IMAGES[cap];
if (capImages) {
for (const img of capImages) images.add(img);
}
}
return [...images].sort();
}
function getOrchestration(numShards, options = {}) {
const cliArgs = ['orchestrate', `--shards=${numShards}`];
if (options.impact) cliArgs.push('--impact');
if (options.files) {
// Normalize repo-root-relative paths to playwright-root-relative
// git diff gives 'packages/testing/playwright/foo.ts', janitor expects 'foo.ts'
const normalized = options.files
.split(',')
.map((f) => (f.startsWith(PLAYWRIGHT_PREFIX) ? f.slice(PLAYWRIGHT_PREFIX.length) : f))
.join(',');
cliArgs.push(`--files=${normalized}`);
}
const output = execFileSync('node', [JANITOR_CLI, ...cliArgs], {
cwd: PLAYWRIGHT_DIR,
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'inherit'],
});
return JSON.parse(output);
}
const args = process.argv.slice(2);
const matrixMode = args.includes('--matrix');
const orchestrateMode = args.includes('--orchestrate');
const impactMode = args.includes('--impact');
const filesArg = args.find((a) => a.startsWith('--files='))?.slice('--files='.length) || undefined;
const shards = parseInt(args.find((a) => !a.startsWith('-')) ?? '');
if (!shards || shards < 1) {
console.error('Usage: node distribute-tests.mjs --matrix <shards> [--orchestrate] [--impact]');
console.error(' node distribute-tests.mjs <shards> <index>');
process.exit(1);
}
if (matrixMode) {
if (!orchestrateMode) {
const matrix = Array.from({ length: shards }, (_, i) => ({
shard: i + 1,
specs: '',
images: '',
}));
console.log(JSON.stringify(matrix));
} else {
const result = getOrchestration(shards, { impact: impactMode, files: filesArg });
if (result.shards.length === 0) {
console.error('\n⏭️ No specs to run — all filtered out by discovery/impact. Skipping.\n');
console.log(JSON.stringify([{ shard: 1, specs: '', images: '', skip: true }]));
} else {
console.error('\n📊 Shard Distribution:');
let maxShardTime = 0;
for (const shard of result.shards) {
const overhead = shard.fixtureCount * CONTAINER_STARTUP_TIME;
const totalTime = shard.testTime + overhead;
maxShardTime = Math.max(maxShardTime, totalTime);
const testMins = (shard.testTime / 60_000).toFixed(1);
const totalMins = (totalTime / 60_000).toFixed(1);
const caps =
shard.capabilities.length > 0 ? ` [${shard.capabilities.join(', ')}]` : '';
console.error(
` Shard ${shard.shard}: ${shard.specs.length} specs, ${testMins} min test + ${(overhead / 1000).toFixed(0)}s startup = ${totalMins} min${caps}`,
);
}
const totalTestMins = (result.totalTestTime / 60_000).toFixed(1);
console.error(`\n Total test time: ${totalTestMins} min`);
console.error(
` Expected wall-clock: ~${(maxShardTime / 60_000).toFixed(1)} min (longest shard)\n`,
);
const matrix = result.shards.map((shard) => ({
shard: shard.shard,
specs: shard.specs.join(' '),
images: getRequiredImages(shard.capabilities).join(' '),
}));
console.log(JSON.stringify(matrix));
}
}
} else {
const index = parseInt(args[1]);
if (isNaN(index) || index < 0 || index >= shards) {
console.error(`Index must be between 0 and ${shards - 1}`);
process.exit(1);
}
const result = getOrchestration(shards, { impact: impactMode, files: filesArg });
const shard = result.shards[index];
if (shard) {
console.log(shard.specs.join('\n'));
}
}