ci: split PR and release certification gates (#161)
T3MP3ST CI / Pull request gate (push) Canceled after 0s
T3MP3ST CI / Release certification and ZIP archive (push) Canceled after 0s

This commit is contained in:
Joseph Magly authored and GitHub committed 2026-08-23 21:27:31 -04:00
1 parent 52ff30de91
commit f2eec3c48c
12 files changed
+210 -70

No files matched your search

+64
View File
@@ -0,0 +1,64 @@
#!/usr/bin/env node
import { readFileSync } from 'node:fs';
import { execFileSync } from 'node:child_process';
export function changedLines(diff) {
const files = new Map();
let current;
for (const line of String(diff).split('\n')) {
if (line.startsWith('+++ b/')) {
current = line.slice(6);
if (!files.has(current)) files.set(current, new Set());
continue;
}
const match = line.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/);
if (!match || !current) continue;
const start = Number(match[1]);
const count = match[2] === undefined ? 1 : Number(match[2]);
for (let n = start; n < start + count; n += 1) files.get(current).add(n);
}
return files;
}
export function assessChangedCoverage(coverage, changes) {
let coverable = 0;
let covered = 0;
const uncovered = [];
for (const [file, lines] of changes) {
if (!file.startsWith('src/') || file.endsWith('.test.ts') || file.includes('/__tests__/')) continue;
const entry = Object.values(coverage).find((item) => String(item.path || '').replaceAll('\\', '/').endsWith(`/${file}`));
if (!entry) continue;
for (const line of lines) {
const statementIds = Object.entries(entry.statementMap || {})
.filter(([, span]) => line >= span.start.line && line <= span.end.line)
.map(([id]) => id);
if (!statementIds.length) continue;
coverable += 1;
if (statementIds.some((id) => Number(entry.s?.[id] || 0) > 0)) covered += 1;
else uncovered.push(`${file}:${line}`);
}
}
return { coverable, covered, percent: coverable ? (covered / coverable) * 100 : 100, uncovered };
}
function selfTest() {
const changes = changedLines('+++ b/src/a.ts\n@@ -1 +1,3 @@\n+x\n+y\n+z');
const coverage = { '/repo/src/a.ts': { path: '/repo/src/a.ts', statementMap: { 0: { start: { line: 1 }, end: { line: 1 } }, 1: { start: { line: 2 }, end: { line: 3 } } }, s: { 0: 1, 1: 0 } } };
const result = assessChangedCoverage(coverage, changes);
if (result.coverable !== 3 || result.covered !== 1 || Math.round(result.percent) !== 33) throw new Error('changed-line coverage self-test failed');
console.log('changed-line coverage self-test passed');
}
if (process.argv.includes('--self-test')) {
selfTest();
} else {
const base = process.argv[2] || process.env.COVERAGE_BASE;
const minimum = Number(process.argv[3] || process.env.PR_CHANGED_LINE_COVERAGE || 50);
if (!base) throw new Error('base revision required as argv[2] or COVERAGE_BASE');
const diff = execFileSync('git', ['diff', '--unified=0', '--no-ext-diff', `${base}...HEAD`, '--', 'src'], { encoding: 'utf8' });
const coverage = JSON.parse(readFileSync('coverage-pr/coverage-final.json', 'utf8'));
const result = assessChangedCoverage(coverage, changedLines(diff));
console.log(`Changed executable lines: ${result.covered}/${result.coverable} (${result.percent.toFixed(1)}%; required ${minimum}%)`);
if (result.uncovered.length) console.log(`Uncovered changed lines:\n${result.uncovered.slice(0, 100).join('\n')}`);
if (result.percent < minimum) process.exitCode = 1;
}