Skip to content

Instantly share code, notes, and snippets.

@hassankhan
Last active April 14, 2025 12:56
Show Gist options
  • Save hassankhan/2384916660e703f1cfda5a5d1dffa9d4 to your computer and use it in GitHub Desktop.
Save hassankhan/2384916660e703f1cfda5a5d1dffa9d4 to your computer and use it in GitHub Desktop.
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