Skip to content

Instantly share code, notes, and snippets.

@hassankhan
Last active April 14, 2025 12:56
Script that merges Cobertura coverage reports generated by Jest in an Nx monorepo and improves them for use in Azure DevOps
import fs from 'fs/promises';
import { readdirSync, statSync } from 'fs';
import path from 'path';
import { promisify } from 'util';
import { exec as callbackExec } from 'child_process';
const exec = promisify(callbackExec);
const COVERAGE_DIR = path.resolve(process.cwd(), 'coverage');
const OUTPUT_DIR_RELATIVE = 'coverage';
const COBERTURA_FILENAME = 'cobertura-coverage.xml';
const COBERTURA_MERGED_FILENAME = 'cobertura-coverage-complete.xml';
const COBERTURA_TARGET_PATH = path.join(
OUTPUT_DIR_RELATIVE,
COBERTURA_MERGED_FILENAME,
);
// Recursive function to find all cobertura-coverage.xml files
// Using sync methods here as it simplifies the recursive structure slightly,
// and typically runs quickly enough during build processes.
function findCoberturaFilesRecursive(dir, files = []) {
const list = readdirSync(dir);
for (const file of list) {
const absolutePath = path.join(dir, file);
if (statSync(absolutePath).isDirectory()) {
// Exclude the potential output directory itself to avoid self-inclusion
if (absolutePath !== path.resolve(process.cwd(), OUTPUT_DIR_RELATIVE)) {
findCoberturaFilesRecursive(absolutePath, files);
}
} else if (path.basename(absolutePath) === COBERTURA_FILENAME) {
// Store path relative to CWD for the command
files.push(path.relative(process.cwd(), absolutePath));
}
}
return files;
}
async function mergeCoberturaFiles() {
console.log(
`Searching for ${COBERTURA_FILENAME} files in ${COVERAGE_DIR}...`,
);
let allCoberturaFiles = [];
try {
await fs.access(COVERAGE_DIR);
allCoberturaFiles = findCoberturaFilesRecursive(COVERAGE_DIR);
} catch (error) {
if (error.code === 'ENOENT') {
console.log('Coverage directory not found. Skipping merge.');
return;
} else {
console.error('Error accessing coverage directory:', error);
throw error;
}
}
if (allCoberturaFiles.length === 0) {
console.log('No Cobertura coverage files found to merge.');
return;
}
console.log(`Found ${allCoberturaFiles.length} files to merge:`);
allCoberturaFiles.forEach((file) => console.log(` - ${file}`));
const targetDir = path.dirname(COBERTURA_TARGET_PATH);
try {
await fs.mkdir(targetDir, { recursive: true });
} catch (err) {
console.error(`Error creating target directory ${targetDir}:`, err);
return;
}
const commandPrefix = `npx cobertura-merge -o ${COBERTURA_TARGET_PATH}`;
const allPackagesCommand = allCoberturaFiles
.map((file) => {
const reportDir = path.dirname(file);
const relativeProjectPath = path.relative(OUTPUT_DIR_RELATIVE, reportDir);
const packageName =
relativeProjectPath === '.'
? 'root'
: relativeProjectPath.replace(/[/]/g, '.');
const safePackageName = packageName || 'unknown';
return `${safePackageName}=${file}`;
})
.join(' ');
const completeCommand = `${commandPrefix} ${allPackagesCommand}`;
console.log('\nRunning merge command:');
console.log(completeCommand);
try {
const { stdout, stderr } = await exec(completeCommand);
if (stderr) {
console.error('Merge command stderr:', stderr);
}
console.log('Merge command stdout:', stdout);
console.log(
`\nSuccessfully merged Cobertura reports into ${COBERTURA_TARGET_PATH}`,
);
} catch (err) {
console.error('\nError executing Cobertura merge command:', err);
process.exit(1);
}
}
mergeCoberturaFiles();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment