#!/usr/bin/env node
// @ts-check
/**
* Aggregate downloaded shard artifacts into (1) a unified coverage report lcov
* and (2) the per-spec impact map, using the property-tested janitor
* `merge-coverage`. Replaces brittle inline `find`/`cp` bash in the workflows —
* the janitor CLI path lives here, in one place, instead of per-YAML-string.
*
* node aggregate-coverage.mjs --shards=
--out= [--unit-shards=]
*
* --unit-shards: directory of downloaded unit + integration + frontend lcov
* artifacts. Jest and vitest write absolute SF: paths in CI
* (/home/runner/work/n8n/n8n/packages/cli/src/foo.ts); they are normalised
* to repo-root-relative before merging with the E2E lcovs so MCR sees the
* same key across all three suites.
*/
import { execFileSync } from 'node:child_process';
import {
existsSync,
mkdirSync,
readFileSync,
writeFileSync,
readdirSync,
statSync,
rmSync,
} from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const JANITOR_CLI = path.resolve(__dirname, '..', '..', 'janitor', 'dist', 'cli.js');
// scripts/ → playwright/ → testing/ → packages/ → repo root
const REPO_ROOT = path.resolve(__dirname, '..', '..', '..', '..') + path.sep;
const arg = (name, fallback) =>
process.argv.find((a) => a.startsWith(`--${name}=`))?.slice(name.length + 3) ?? fallback;
const SHARDS = arg('shards', '/tmp/shards');
const UNIT_SHARDS = arg('unit-shards', null);
const OUT = arg('out', './coverage');
/** Recursively collect files under `dir` matching `predicate(filename)`. */
function findFiles(dir, predicate, acc = []) {
if (!existsSync(dir)) return acc;
for (const entry of readdirSync(dir)) {
const p = path.join(dir, entry);
if (statSync(p).isDirectory()) findFiles(p, predicate, acc);
else if (predicate(entry, p)) acc.push(p);
}
return acc;
}
/**
* Rewrite absolute SF: paths to repo-root-relative. Jest and vitest write
* absolute paths in CI; E2E lcovs are already repo-root-relative so the
* regex is a no-op for them. Merging everything through the same transform
* means MCR sees identical keys across all three suites.
*/
function normalizeLcov(content) {
return content.replace(/^SF:(.+)$/gm, (line, p) =>
p.startsWith(REPO_ROOT) ? `SF:${p.slice(REPO_ROOT.length)}` : line,
);
}
/** Stage files into a fresh temp dir with unique names, normalising SF: paths. */
function stage(label, files) {
const dir = path.join('/tmp', `agg-${label}`);
rmSync(dir, { recursive: true, force: true });
mkdirSync(dir, { recursive: true });
files.forEach((f, i) => {
writeFileSync(path.join(dir, `${label}-${i}.lcov`), normalizeLcov(readFileSync(f, 'utf8')));
});
return dir;
}
function merge(inputsDir, outLcov, outMap) {
execFileSync(
'node',
[
JANITOR_CLI,
'merge-coverage',
`--inputs-dir=${inputsDir}`,
`--out-lcov=${outLcov}`,
`--out-map=${outMap}`,
],
{ stdio: 'inherit' },
);
}
mkdirSync(OUT, { recursive: true });
const shardLcovs = findFiles(SHARDS, (name) => name === 'lcov.info');
const unitLcovs = UNIT_SHARDS ? findFiles(UNIT_SHARDS, (name) => name === 'lcov.info') : [];
const allReportLcovs = [...shardLcovs, ...unitLcovs];
// 1. Combined report → lcov.info (all layers: E2E + unit + integration + frontend).
// Uploaded to Codecov as a single `nightly-full` flag — one authoritative number
// rather than per-layer flags that require an approximated union at query time.
console.log(
`Report: ${shardLcovs.length} E2E shard lcov(s), ${unitLcovs.length} unit/integration/frontend lcov(s)`,
);
if (allReportLcovs.length) {
merge(stage('report', allReportLcovs), path.join(OUT, 'lcov.info'), '/tmp/agg-report-map.json');
} else {
console.warn(' ⚠ no lcovs found — skipping report');
}
// 2. Impact map: per-spec E2E lcovs (TN-tagged) → spec-keyed map for PR selection.
// Unit/integration lcovs are not TN-tagged per test so they stay out of the
// impact map for now — cross-layer per-test attribution is Phase 3 (DEVP-293).
const specLcovs = findFiles(
SHARDS,
(name, p) => name.endsWith('.lcov') && p.includes(`${path.sep}by-spec${path.sep}`),
);
console.log(`Impact map: ${specLcovs.length} per-spec lcov(s)`);
if (specLcovs.length) {
merge(stage('spec', specLcovs), '/tmp/agg-spec-fe.lcov', path.join(OUT, 'impact-map.json'));
} else {
console.warn(' ⚠ no per-spec lcovs found — impact map not built');
}