mirror of
https://github.com/lexmount/moli.git
synced 2026-09-23 00:01:25 +00:00
749 lines
29 KiB
JavaScript
749 lines
29 KiB
JavaScript
'use strict';
|
||
|
||
const fs = require('node:fs');
|
||
const path = require('node:path');
|
||
|
||
const COMMENT_MARKER = '<!-- moli-ci-regression-report -->';
|
||
const MAX_JSON_BYTES = 8 * 1024 * 1024;
|
||
const MAX_COMMENT_BYTES = 32 * 1024;
|
||
const MAX_RESULT_ROWS = 2_000;
|
||
const MAX_DETAIL_ROWS = 10;
|
||
const MAX_MATRIX_ROWS = 5_000;
|
||
const MAX_CDP_GROUPS = 100;
|
||
const MIB = 1024 * 1024;
|
||
|
||
function isObject(value) {
|
||
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
||
}
|
||
|
||
function finiteNumber(value) {
|
||
return typeof value === 'number' && Number.isFinite(value) && Math.abs(value) <= Number.MAX_SAFE_INTEGER
|
||
? value
|
||
: null;
|
||
}
|
||
|
||
function nonNegativeNumber(value) {
|
||
const number = finiteNumber(value);
|
||
return number !== null && number >= 0 ? number : null;
|
||
}
|
||
|
||
function count(value) {
|
||
const number = nonNegativeNumber(value);
|
||
return number !== null && Number.isInteger(number) ? number : null;
|
||
}
|
||
|
||
function objectAt(value, key) {
|
||
return isObject(value) && isObject(value[key]) ? value[key] : {};
|
||
}
|
||
|
||
function safeText(value, maximumLength = 120) {
|
||
if (typeof value !== 'string') {
|
||
return 'unknown';
|
||
}
|
||
return value
|
||
.replace(/[\u0000-\u001f\u007f]/g, ' ')
|
||
.replace(/\\/g, '\\\\')
|
||
.replace(/\|/g, '\\|')
|
||
.replace(/`/g, "'")
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.slice(0, maximumLength) || 'unknown';
|
||
}
|
||
|
||
function code(value, maximumLength = 120) {
|
||
return `\`${safeText(value, maximumLength)}\``;
|
||
}
|
||
|
||
function readJson(root, relativePath) {
|
||
if (typeof root !== 'string' || root.length === 0) {
|
||
throw new Error('artifact root is unavailable');
|
||
}
|
||
const filePath = path.join(root, relativePath);
|
||
const metadata = fs.lstatSync(filePath);
|
||
if (!metadata.isFile() || metadata.isSymbolicLink()) {
|
||
throw new Error('artifact JSON is not a regular file');
|
||
}
|
||
if (metadata.size > MAX_JSON_BYTES) {
|
||
throw new Error('artifact JSON exceeds the trusted renderer limit');
|
||
}
|
||
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
||
}
|
||
|
||
function loadSection(loader) {
|
||
try {
|
||
return { available: true, data: loader() };
|
||
} catch {
|
||
return { available: false, data: null };
|
||
}
|
||
}
|
||
|
||
function median(values) {
|
||
const numbers = values.filter((value) => finiteNumber(value) !== null).sort((left, right) => left - right);
|
||
if (numbers.length === 0) {
|
||
return null;
|
||
}
|
||
const middle = Math.floor(numbers.length / 2);
|
||
return numbers.length % 2 === 0
|
||
? (numbers[middle - 1] + numbers[middle]) / 2
|
||
: numbers[middle];
|
||
}
|
||
|
||
function sum(values) {
|
||
const numbers = values.map(finiteNumber).filter((value) => value !== null);
|
||
return numbers.length === 0 ? null : numbers.reduce((total, value) => total + value, 0);
|
||
}
|
||
|
||
function formatInteger(value) {
|
||
const number = count(value);
|
||
return number === null ? '—' : number.toLocaleString('en-US');
|
||
}
|
||
|
||
function formatDecimal(value, digits = 2) {
|
||
const number = finiteNumber(value);
|
||
return number === null ? '—' : number.toFixed(digits);
|
||
}
|
||
|
||
function formatMilliseconds(value) {
|
||
const number = finiteNumber(value);
|
||
return number === null ? '—' : `${number.toFixed(2)} ms`;
|
||
}
|
||
|
||
function formatDurationMilliseconds(value) {
|
||
const number = nonNegativeNumber(value);
|
||
if (number === null) {
|
||
return '—';
|
||
}
|
||
if (number < 1_000) {
|
||
return formatMilliseconds(number);
|
||
}
|
||
const totalSeconds = number / 1_000;
|
||
if (totalSeconds < 60) {
|
||
return `${totalSeconds.toFixed(2)} s`;
|
||
}
|
||
const minutes = Math.floor(totalSeconds / 60);
|
||
return `${minutes}m ${(totalSeconds - minutes * 60).toFixed(2)}s`;
|
||
}
|
||
|
||
function formatSeconds(value) {
|
||
const number = finiteNumber(value);
|
||
return number === null ? '—' : `${number.toFixed(2)} s`;
|
||
}
|
||
|
||
function formatPercentValue(value) {
|
||
const number = finiteNumber(value);
|
||
return number === null ? '—' : `${number.toFixed(2)}%`;
|
||
}
|
||
|
||
function formatBytes(value) {
|
||
const number = finiteNumber(value);
|
||
if (number === null) {
|
||
return '—';
|
||
}
|
||
const absolute = Math.abs(number);
|
||
if (absolute >= MIB) {
|
||
return `${(absolute / MIB).toFixed(2)} MiB`;
|
||
}
|
||
if (absolute >= 1024) {
|
||
return `${(absolute / 1024).toFixed(2)} KiB`;
|
||
}
|
||
return `${Math.round(absolute)} B`;
|
||
}
|
||
|
||
function signed(value, formatter) {
|
||
const number = finiteNumber(value);
|
||
if (number === null) {
|
||
return '—';
|
||
}
|
||
if (number === 0) {
|
||
return formatter(0);
|
||
}
|
||
return `${number > 0 ? '+' : '−'}${formatter(Math.abs(number))}`;
|
||
}
|
||
|
||
function percentDelta(base, head, digits = 2) {
|
||
const baseline = finiteNumber(base);
|
||
const current = finiteNumber(head);
|
||
if (baseline === null || current === null || baseline === 0) {
|
||
return '—';
|
||
}
|
||
const value = ((current - baseline) / Math.abs(baseline)) * 100;
|
||
if (value === 0) {
|
||
return '0.00%';
|
||
}
|
||
return `${value > 0 ? '+' : '−'}${Math.abs(value).toFixed(digits)}%`;
|
||
}
|
||
|
||
function comparisonCells(base, head, formatter, percentDigits = 2) {
|
||
const baseline = finiteNumber(base);
|
||
const current = finiteNumber(head);
|
||
return [
|
||
formatter(baseline),
|
||
formatter(current),
|
||
baseline === null || current === null ? '—' : signed(current - baseline, formatter),
|
||
percentDelta(baseline, current, percentDigits),
|
||
];
|
||
}
|
||
|
||
function metric(summary, caseName, ...pathParts) {
|
||
let value = objectAt(objectAt(summary, 'cases'), caseName);
|
||
for (const part of pathParts) {
|
||
value = isObject(value) ? value[part] : null;
|
||
}
|
||
return finiteNumber(value);
|
||
}
|
||
|
||
function failures(summary) {
|
||
return count(summary?.gate_failures) ?? count(summary?.total_failures) ?? null;
|
||
}
|
||
|
||
function statusIcon(failureCount) {
|
||
if (failureCount === null) {
|
||
return '⚪';
|
||
}
|
||
return failureCount === 0 ? '✅' : '❌';
|
||
}
|
||
|
||
function loadRelease(root) {
|
||
const baseStartup = readJson(root, 'base-startup/startup/summary.json');
|
||
const headStartup = readJson(root, 'head-startup/startup/summary.json');
|
||
const baseMatrix = readJson(root, 'base-matrix/synthetic-matrix/summary.json');
|
||
const headMatrix = readJson(root, 'head-matrix/synthetic-matrix/summary.json');
|
||
const baseMatrixRows = readJson(root, 'base-matrix/synthetic-matrix/matrix.json');
|
||
const headMatrixRows = readJson(root, 'head-matrix/synthetic-matrix/matrix.json');
|
||
if (
|
||
!isObject(baseStartup) ||
|
||
!isObject(headStartup) ||
|
||
!isObject(baseMatrix) ||
|
||
!isObject(headMatrix) ||
|
||
!Array.isArray(baseMatrixRows) ||
|
||
!Array.isArray(headMatrixRows) ||
|
||
baseMatrixRows.length > MAX_MATRIX_ROWS ||
|
||
headMatrixRows.length > MAX_MATRIX_ROWS
|
||
) {
|
||
throw new Error('invalid release regression artifact');
|
||
}
|
||
return { baseStartup, headStartup, baseMatrix, headMatrix, baseMatrixRows, headMatrixRows };
|
||
}
|
||
|
||
function matrixAggregate(summary, rows, concurrency) {
|
||
const selected = rows.filter(
|
||
(row) => isObject(row) && finiteNumber(row.concurrency) === concurrency
|
||
);
|
||
const cases = objectAt(summary, 'cases');
|
||
let unstable = 0;
|
||
for (const caseValue of Object.values(cases)) {
|
||
const cell = isObject(caseValue) ? caseValue[String(concurrency)] : null;
|
||
if (isObject(cell) && cell.stable === false) {
|
||
unstable += 1;
|
||
}
|
||
}
|
||
return {
|
||
latency: median(selected.map((row) => row.elapsed_p50_ms)),
|
||
pss: median(selected.map((row) => row.peak_pss_p50_bytes)),
|
||
failures: sum(selected.map((row) => row.failures)) ?? 0,
|
||
unstable,
|
||
};
|
||
}
|
||
|
||
function releaseOverview(section) {
|
||
if (!section.available) {
|
||
return { status: '⚪', signal: 'artifact unavailable or invalid' };
|
||
}
|
||
const { baseStartup, headStartup, baseMatrix, headMatrix } = section.data;
|
||
const baseFailures = (failures(baseStartup) ?? 0) + (failures(baseMatrix) ?? 0);
|
||
const headFailures = (failures(headStartup) ?? 0) + (failures(headMatrix) ?? 0);
|
||
const rawBase = metric(section.data.baseStartup, 'binary-size', 'binary_bytes');
|
||
const rawHead = metric(headStartup, 'binary-size', 'binary_bytes');
|
||
return {
|
||
status: headFailures > 0 ? '❌' : baseFailures > 0 ? '⚠️' : '✅',
|
||
signal: `HEAD/base failures ${formatInteger(headFailures)}/${formatInteger(baseFailures)}; raw binary ${percentDelta(rawBase, rawHead, 6)}`,
|
||
};
|
||
}
|
||
|
||
function renderRelease(section) {
|
||
const overview = releaseOverview(section);
|
||
const lines = [`<details open><summary><strong>Release regression</strong> — ${overview.status} ${overview.signal}</summary>`, ''];
|
||
if (!section.available) {
|
||
lines.push('Artifact unavailable or invalid. See the source CI run for infrastructure details.', '', '</details>');
|
||
return lines;
|
||
}
|
||
|
||
const { baseStartup, headStartup, baseMatrix, headMatrix, baseMatrixRows, headMatrixRows } = section.data;
|
||
const sizeRows = [
|
||
['Raw binary', metric(baseStartup, 'binary-size', 'binary_bytes'), metric(headStartup, 'binary-size', 'binary_bytes')],
|
||
['Stripped binary', metric(baseStartup, 'binary-size', 'stripped_binary_bytes'), metric(headStartup, 'binary-size', 'stripped_binary_bytes')],
|
||
['gzip binary', metric(baseStartup, 'binary-size', 'tar_gz_bytes'), metric(headStartup, 'binary-size', 'tar_gz_bytes')],
|
||
['Rootfs', metric(baseStartup, 'image-size', 'image_uncompressed_bytes'), metric(headStartup, 'image-size', 'image_uncompressed_bytes')],
|
||
['gzip rootfs', metric(baseStartup, 'image-size', 'image_compressed_bytes'), metric(headStartup, 'image-size', 'image_compressed_bytes')],
|
||
];
|
||
lines.push(
|
||
'#### Package and image size',
|
||
'',
|
||
'| Metric | Base | HEAD | Delta | Delta % |',
|
||
'| --- | ---: | ---: | ---: | ---: |'
|
||
);
|
||
for (const [label, base, head] of sizeRows) {
|
||
lines.push(`| ${label} | ${comparisonCells(base, head, formatBytes, 6).join(' | ')} |`);
|
||
}
|
||
|
||
const startupRows = [
|
||
['Serve ready', 'serve-ready', ['elapsed_ms', 'p50']],
|
||
['CDP first page', 'cdp-first-page', ['elapsed_ms', 'p50']],
|
||
['CDP warm page', 'cdp-warm-pages', ['cdp_page_elapsed_p50_ms']],
|
||
['CLI about:blank', 'cli-fetch-aboutblank', ['elapsed_ms', 'p50']],
|
||
['CLI local JS', 'cli-fetch-local-js', ['elapsed_ms', 'p50']],
|
||
];
|
||
lines.push(
|
||
'',
|
||
'#### Startup latency and PSS',
|
||
'',
|
||
'| Case | Base p50 | HEAD p50 | Delta | Base PSS | HEAD PSS | Delta |',
|
||
'| --- | ---: | ---: | ---: | ---: | ---: | ---: |'
|
||
);
|
||
for (const [label, caseName, latencyPath] of startupRows) {
|
||
const baseLatency = metric(baseStartup, caseName, ...latencyPath);
|
||
const headLatency = metric(headStartup, caseName, ...latencyPath);
|
||
const basePss = metric(baseStartup, caseName, 'pss_bytes', 'p50');
|
||
const headPss = metric(headStartup, caseName, 'pss_bytes', 'p50');
|
||
const latencyCells = comparisonCells(baseLatency, headLatency, formatMilliseconds).slice(0, 3);
|
||
const pssCells = comparisonCells(basePss, headPss, formatBytes).slice(0, 3);
|
||
lines.push(`| ${label} | ${[...latencyCells, ...pssCells].join(' | ')} |`);
|
||
}
|
||
|
||
const levels = [...new Set([
|
||
...(Array.isArray(baseMatrix.concurrency_levels) ? baseMatrix.concurrency_levels : []),
|
||
...(Array.isArray(headMatrix.concurrency_levels) ? headMatrix.concurrency_levels : []),
|
||
])]
|
||
.map(finiteNumber)
|
||
.filter((value) => value !== null && Number.isInteger(value) && value > 0)
|
||
.sort((left, right) => left - right)
|
||
.slice(0, 10);
|
||
lines.push(
|
||
'',
|
||
'#### Concurrency matrix',
|
||
'',
|
||
'| Concurrency | Base p50 | HEAD p50 | Delta | Base PSS | HEAD PSS | Delta | HEAD failures | Unstable B/H |',
|
||
'| ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |'
|
||
);
|
||
for (const level of levels) {
|
||
const base = matrixAggregate(baseMatrix, baseMatrixRows, level);
|
||
const head = matrixAggregate(headMatrix, headMatrixRows, level);
|
||
const latency = comparisonCells(base.latency, head.latency, formatMilliseconds).slice(0, 3);
|
||
const pss = comparisonCells(base.pss, head.pss, formatBytes).slice(0, 3);
|
||
lines.push(
|
||
`| ${level} | ${[...latency, ...pss, formatInteger(head.failures), `${base.unstable}/${head.unstable}`].join(' | ')} |`
|
||
);
|
||
}
|
||
lines.push('', '</details>');
|
||
return lines;
|
||
}
|
||
|
||
function loadFrontend(root) {
|
||
const summary = readJson(root, 'summary.json');
|
||
if (!isObject(summary) || !isObject(summary.counts) || !Array.isArray(summary.results) || summary.results.length > MAX_RESULT_ROWS) {
|
||
throw new Error('invalid frontend differential artifact');
|
||
}
|
||
return summary;
|
||
}
|
||
|
||
function frontendOverview(section) {
|
||
if (!section.available) {
|
||
return { status: '⚪', signal: 'artifact unavailable or invalid' };
|
||
}
|
||
const summary = section.data;
|
||
const total = summary.results.length;
|
||
const matches = (count(summary.counts.match) ?? 0) + (count(summary.counts.reference_ok) ?? 0);
|
||
const problems = Math.max(0, total - matches);
|
||
const recoveries = count(objectAt(summary, 'referenceGate').recoveredCases) ?? 0;
|
||
return {
|
||
status: summary.ok === true && problems === 0 ? '✅' : '❌',
|
||
signal: `${formatInteger(matches)}/${formatInteger(total)} cases matched; ${formatInteger(problems)} issues${recoveries ? `; ${formatInteger(recoveries)} Chromium reference recoveries` : ''}`,
|
||
};
|
||
}
|
||
|
||
function renderFrontend(section) {
|
||
const overview = frontendOverview(section);
|
||
const lines = [`<details><summary><strong>Frontend differential</strong> — ${overview.status} ${overview.signal}</summary>`, ''];
|
||
if (!section.available) {
|
||
lines.push('Artifact unavailable or invalid. See the source CI run for infrastructure details.', '', '</details>');
|
||
return lines;
|
||
}
|
||
const summary = section.data;
|
||
const timeline = objectAt(summary, 'timeline');
|
||
lines.push(
|
||
'| Match | DOM mismatch | Diagnostic mismatch | Moli error | Reference error | Infrastructure error | Mismatched frames | Duration |',
|
||
'| ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |',
|
||
`| ${formatInteger(count(summary.counts.match) ?? 0)} | ${formatInteger(count(summary.counts.dom_mismatch) ?? 0)} | ${formatInteger(count(summary.counts.diagnostic_mismatch) ?? 0)} | ${formatInteger(count(summary.counts.moli_error) ?? 0)} | ${formatInteger(count(summary.counts.reference_error) ?? 0)} | ${formatInteger(count(summary.counts.infrastructure_error) ?? 0)} | ${formatInteger(timeline.mismatchedFrames)} | ${formatDurationMilliseconds(summary.durationMs)} |`
|
||
);
|
||
const recoveredReferences = summary.results
|
||
.filter((result) => {
|
||
const chromium = objectAt(result, 'chromium');
|
||
return Array.isArray(chromium.previous_failures) && chromium.previous_failures.length !== 0;
|
||
})
|
||
.slice(0, MAX_DETAIL_ROWS);
|
||
if (recoveredReferences.length !== 0) {
|
||
lines.push('', '**Recovered Chromium reference infrastructure failures**', '');
|
||
for (const result of recoveredReferences) {
|
||
const chromium = objectAt(result, 'chromium');
|
||
const failures = chromium.previous_failures;
|
||
const first = isObject(failures[0]) ? failures[0] : {};
|
||
const error = typeof first.error === 'string' ? first.error : 'unknown';
|
||
lines.push(
|
||
`- ${code(result.id, 160)} — ${formatInteger(failures.length)} failed attempt(s) before recovery · ${code(error, 160)}`
|
||
);
|
||
}
|
||
}
|
||
const issues = summary.results
|
||
.filter((result) => isObject(result) && !['match', 'reference_ok'].includes(result.status))
|
||
.slice(0, MAX_DETAIL_ROWS);
|
||
if (issues.length !== 0) {
|
||
lines.push('', '**First failing cases**', '');
|
||
for (const issue of issues) {
|
||
const details = [];
|
||
if (typeof issue.firstDifference === 'string' && issue.firstDifference.length !== 0) {
|
||
details.push(`first difference ${code(issue.firstDifference, 160)}`);
|
||
}
|
||
if (Array.isArray(issue.mismatchedFrames) && issue.mismatchedFrames.length !== 0) {
|
||
const frames = issue.mismatchedFrames.slice(0, 3).map((frame) => code(frame, 80));
|
||
const remainder = issue.mismatchedFrames.length - frames.length;
|
||
details.push(`frames ${frames.join(', ')}${remainder > 0 ? ` +${remainder} more` : ''}`);
|
||
}
|
||
const moli = objectAt(issue, 'moli');
|
||
const chromium = objectAt(issue, 'chromium');
|
||
const error = typeof moli.error === 'string'
|
||
? `Moli error ${code(moli.error, 160)}`
|
||
: typeof chromium.error === 'string'
|
||
? `Chromium error ${code(chromium.error, 160)}`
|
||
: null;
|
||
if (error !== null) {
|
||
details.push(error);
|
||
}
|
||
lines.push(`- ${code(issue.id)} — ${code(issue.status)}${details.length ? ` · ${details.join(' · ')}` : ''}`);
|
||
}
|
||
}
|
||
lines.push('', '</details>');
|
||
return lines;
|
||
}
|
||
|
||
function loadAgent(root) {
|
||
const fast = readJson(root, 'agent-fast/agent-episode/summary.json');
|
||
const resource = readJson(root, 'agent-resource/agent-episode/summary.json');
|
||
if (!isObject(fast) || !isObject(resource) || !isObject(fast.targets) || !isObject(resource.targets)) {
|
||
throw new Error('invalid agent episode artifact');
|
||
}
|
||
return { fast, resource };
|
||
}
|
||
|
||
function target(summary, name) {
|
||
return objectAt(objectAt(summary, 'targets'), name);
|
||
}
|
||
|
||
function agentOverview(section) {
|
||
if (!section.available) {
|
||
return { status: '⚪', signal: 'artifact unavailable or invalid' };
|
||
}
|
||
const totalFailures = (failures(section.data.fast) ?? 0) + (failures(section.data.resource) ?? 0);
|
||
const moli = target(section.data.fast, 'moli-cdp');
|
||
return {
|
||
status: statusIcon(totalFailures),
|
||
signal: `${formatInteger(moli.passed)}/${formatInteger(moli.episodes)} Moli episodes passed; ${formatInteger(totalFailures)} failures`,
|
||
};
|
||
}
|
||
|
||
function renderAgent(section) {
|
||
const overview = agentOverview(section);
|
||
const lines = [`<details><summary><strong>Agent episodes · Moli vs Chromium</strong> — ${overview.status} ${overview.signal}</summary>`, ''];
|
||
if (!section.available) {
|
||
lines.push('Artifact unavailable or invalid. See the source CI run for infrastructure details.', '', '</details>');
|
||
return lines;
|
||
}
|
||
const { fast, resource } = section.data;
|
||
lines.push(
|
||
'| Fast contract | Episodes | Passed | Assertions | Ready p50 | Episode p50 |',
|
||
'| --- | ---: | ---: | ---: | ---: | ---: |'
|
||
);
|
||
for (const name of ['moli-cdp', 'chrome-cdp']) {
|
||
const item = target(fast, name);
|
||
lines.push(
|
||
`| ${name === 'moli-cdp' ? 'Moli' : 'Chromium'} | ${formatInteger(item.episodes)} | ${formatInteger(item.passed)} | ${formatInteger(item.assertions_passed)}/${formatInteger(item.assertions_total)} | ${formatMilliseconds(objectAt(item, 'ready_ms').p50)} | ${formatMilliseconds(objectAt(item, 'elapsed_ms').p50)} |`
|
||
);
|
||
}
|
||
lines.push(
|
||
'',
|
||
'| Operation p50 | Chromium | Moli | Delta | Delta % |',
|
||
'| --- | ---: | ---: | ---: | ---: |'
|
||
);
|
||
for (const operation of ['navigate', 'observe', 'fill', 'click']) {
|
||
const chromeValue = finiteNumber(objectAt(objectAt(target(fast, 'chrome-cdp'), 'operations'), operation).p50);
|
||
const moliValue = finiteNumber(objectAt(objectAt(target(fast, 'moli-cdp'), 'operations'), operation).p50);
|
||
lines.push(`| ${operation} | ${comparisonCells(chromeValue, moliValue, formatMilliseconds).join(' | ')} |`);
|
||
}
|
||
lines.push(
|
||
'',
|
||
'| Idle-resource episode | Peak RSS | Peak PSS | Average CPU | Peak CPU | Peak processes | Sampler |',
|
||
'| --- | ---: | ---: | ---: | ---: | ---: | --- |'
|
||
);
|
||
for (const name of ['moli-cdp', 'chrome-cdp']) {
|
||
const resources = objectAt(target(resource, name), 'resources');
|
||
const sampler = objectAt(resources, 'sampler_health');
|
||
const samplerStatus = sampler.healthy !== true
|
||
? '❌'
|
||
: sampler.pss_complete === false
|
||
? '⚠️ PSS partial'
|
||
: '✅';
|
||
lines.push(
|
||
`| ${name === 'moli-cdp' ? 'Moli' : 'Chromium'} | ${formatBytes(resources.peak_rss_bytes)} | ${formatBytes(resources.peak_pss_bytes)} | ${formatPercentValue(resources.average_cpu_percent)} | ${formatPercentValue(resources.peak_cpu_percent)} | ${formatDecimal(resources.peak_process_count, 0)} | ${samplerStatus} |`
|
||
);
|
||
}
|
||
const failingCases = [];
|
||
for (const [profile, summary] of [['fast', fast], ['resource', resource]]) {
|
||
for (const name of ['moli-cdp', 'chrome-cdp']) {
|
||
for (const [caseName, caseValue] of Object.entries(objectAt(target(summary, name), 'cases'))) {
|
||
if ((count(isObject(caseValue) ? caseValue.failures : null) ?? 0) > 0) {
|
||
failingCases.push({ profile, target: name, caseName });
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if (failingCases.length !== 0) {
|
||
lines.push('', '**First failing episodes**', '');
|
||
for (const failure of failingCases.slice(0, MAX_DETAIL_ROWS)) {
|
||
lines.push(`- ${code(failure.caseName)} — ${failure.profile} / ${failure.target}`);
|
||
}
|
||
}
|
||
lines.push('', '</details>');
|
||
return lines;
|
||
}
|
||
|
||
function loadRuntime(root) {
|
||
const synthetic = readJson(root, 'runtime-synthetic/synthetic/summary.json');
|
||
const cdpSession = readJson(root, 'cdp-session/cdp-session/summary.json');
|
||
if (!isObject(synthetic) || !isObject(cdpSession)) {
|
||
throw new Error('invalid runtime contract artifact');
|
||
}
|
||
return { synthetic, cdpSession };
|
||
}
|
||
|
||
function summarizeCases(cases, includePss) {
|
||
const entries = Object.entries(isObject(cases) ? cases : {}).slice(0, MAX_RESULT_ROWS);
|
||
return {
|
||
cases: entries.length,
|
||
failures: sum(entries.map(([, value]) => (isObject(value) ? value.failures : null))) ?? 0,
|
||
latencyP50: median(entries.map(([, value]) => objectAt(value, 'elapsed_ms').p50)),
|
||
latencyP95: median(entries.map(([, value]) => objectAt(value, 'elapsed_ms').p95)),
|
||
pssP50: includePss
|
||
? median(entries.map(([, value]) => objectAt(value, 'peak_pss_bytes').p50))
|
||
: null,
|
||
failing: entries
|
||
.filter(([, value]) => (count(isObject(value) ? value.failures : null) ?? 0) > 0)
|
||
.map(([name]) => name)
|
||
.slice(0, MAX_DETAIL_ROWS),
|
||
};
|
||
}
|
||
|
||
function runtimeOverview(section) {
|
||
if (!section.available) {
|
||
return { status: '⚪', signal: 'artifact unavailable or invalid' };
|
||
}
|
||
const synthetic = summarizeCases(section.data.synthetic.cases, true);
|
||
const cdp = summarizeCases(target(section.data.cdpSession, 'moli-cdp').cases, false);
|
||
const caseFailures = synthetic.failures + cdp.failures;
|
||
const reportedFailures = (failures(section.data.synthetic) ?? 0) + (failures(section.data.cdpSession) ?? 0);
|
||
const totalFailures = Math.max(caseFailures, reportedFailures);
|
||
return {
|
||
status: statusIcon(totalFailures),
|
||
signal: `${formatInteger(synthetic.cases + cdp.cases)} contract cases; ${formatInteger(totalFailures)} failures`,
|
||
};
|
||
}
|
||
|
||
function renderRuntime(section) {
|
||
const overview = runtimeOverview(section);
|
||
const lines = [`<details><summary><strong>Runtime and CDP session contracts</strong> — ${overview.status} ${overview.signal}</summary>`, ''];
|
||
if (!section.available) {
|
||
lines.push('Artifact unavailable or invalid. See the source CI run for infrastructure details.', '', '</details>');
|
||
return lines;
|
||
}
|
||
const synthetic = summarizeCases(section.data.synthetic.cases, true);
|
||
const cdp = summarizeCases(target(section.data.cdpSession, 'moli-cdp').cases, false);
|
||
lines.push(
|
||
'| Suite | Cases | Failures | Median case p50 | Median case p95 | Median peak PSS p50 |',
|
||
'| --- | ---: | ---: | ---: | ---: | ---: |',
|
||
`| Synthetic fetch | ${formatInteger(synthetic.cases)} | ${formatInteger(synthetic.failures)} | ${formatMilliseconds(synthetic.latencyP50)} | ${formatMilliseconds(synthetic.latencyP95)} | ${formatBytes(synthetic.pssP50)} |`,
|
||
`| Long-lived CDP session | ${formatInteger(cdp.cases)} | ${formatInteger(cdp.failures)} | ${formatMilliseconds(cdp.latencyP50)} | ${formatMilliseconds(cdp.latencyP95)} | — |`
|
||
);
|
||
const failing = [...synthetic.failing, ...cdp.failing].slice(0, MAX_DETAIL_ROWS);
|
||
if (failing.length !== 0) {
|
||
lines.push('', `**Failing cases:** ${failing.map((name) => code(name)).join(', ')}`);
|
||
}
|
||
lines.push('', '</details>');
|
||
return lines;
|
||
}
|
||
|
||
function loadCdp(root) {
|
||
const summary = readJson(root, 'summary.json');
|
||
if (!isObject(summary) || !Array.isArray(summary.groups) || summary.groups.length > MAX_CDP_GROUPS) {
|
||
throw new Error('invalid CDP smoke artifact');
|
||
}
|
||
return summary;
|
||
}
|
||
|
||
function cdpStats(summary) {
|
||
const groups = summary.groups.filter(isObject);
|
||
const passed = groups.filter((group) => group.status === 'passed').length;
|
||
const scenarios = sum(groups.map((group) => group.scenarioCount)) ?? 0;
|
||
const cumulativeSeconds = sum(groups.map((group) => group.durationSeconds));
|
||
const failures = groups.filter((group) => group.status !== 'passed');
|
||
const slowest = [...groups]
|
||
.filter((group) => nonNegativeNumber(group.durationSeconds) !== null)
|
||
.sort((left, right) => right.durationSeconds - left.durationSeconds)
|
||
.slice(0, 5);
|
||
return { groups, passed, scenarios, cumulativeSeconds, failures, slowest };
|
||
}
|
||
|
||
function cdpOverview(section) {
|
||
if (!section.available) {
|
||
return { status: '⚪', signal: 'artifact unavailable or invalid' };
|
||
}
|
||
const stats = cdpStats(section.data);
|
||
return {
|
||
status: section.data.ok === true && stats.failures.length === 0 ? '✅' : '❌',
|
||
signal: `${formatInteger(stats.passed)}/${formatInteger(stats.groups.length)} groups passed; ${formatInteger(stats.scenarios)} scenarios`,
|
||
};
|
||
}
|
||
|
||
function renderCdp(section) {
|
||
const overview = cdpOverview(section);
|
||
const lines = [`<details><summary><strong>CDP smoke</strong> — ${overview.status} ${overview.signal}</summary>`, ''];
|
||
if (!section.available) {
|
||
lines.push('Artifact unavailable or invalid. See the source CI run for infrastructure details.', '', '</details>');
|
||
return lines;
|
||
}
|
||
const stats = cdpStats(section.data);
|
||
lines.push(
|
||
`Workers: **${formatInteger(section.data.jobs)}** · cumulative group time: **${formatSeconds(stats.cumulativeSeconds)}** · failed groups: **${formatInteger(stats.failures.length)}**`,
|
||
'',
|
||
'| Slowest group | Scenarios | Duration | Status |',
|
||
'| --- | ---: | ---: | --- |'
|
||
);
|
||
for (const group of stats.slowest) {
|
||
lines.push(`| ${code(group.group)} | ${formatInteger(group.scenarioCount)} | ${formatSeconds(group.durationSeconds)} | ${group.status === 'passed' ? '✅' : '❌'} |`);
|
||
}
|
||
if (stats.failures.length !== 0) {
|
||
lines.push('', '**Failed groups**', '');
|
||
for (const group of stats.failures.slice(0, MAX_DETAIL_ROWS)) {
|
||
lines.push(`- ${code(group.group)} — ${code(group.status)}${typeof group.error === 'string' ? ` · ${code(group.error, 160)}` : ''}`);
|
||
}
|
||
}
|
||
lines.push('', '</details>');
|
||
return lines;
|
||
}
|
||
|
||
function trustedRunUrl(value) {
|
||
try {
|
||
const url = new URL(value);
|
||
if (url.protocol === 'https:' && url.hostname === 'github.com') {
|
||
return url.toString();
|
||
}
|
||
} catch {
|
||
// Fall through to a non-link label.
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function renderReport({ releaseRoot, frontendRoot, agentRoot, runtimeRoot, cdpRoot, runUrl, conclusion }) {
|
||
const sections = {
|
||
release: loadSection(() => loadRelease(releaseRoot)),
|
||
frontend: loadSection(() => loadFrontend(frontendRoot)),
|
||
agent: loadSection(() => loadAgent(agentRoot)),
|
||
runtime: loadSection(() => loadRuntime(runtimeRoot)),
|
||
cdp: loadSection(() => loadCdp(cdpRoot)),
|
||
};
|
||
const overviews = [
|
||
['Release regression', releaseOverview(sections.release)],
|
||
['Frontend differential', frontendOverview(sections.frontend)],
|
||
['Agent episodes', agentOverview(sections.agent)],
|
||
['Runtime/CDP contracts', runtimeOverview(sections.runtime)],
|
||
['CDP smoke', cdpOverview(sections.cdp)],
|
||
];
|
||
const available = Object.values(sections).filter((section) => section.available).length;
|
||
const sourceConclusion = ['success', 'failure', 'cancelled', 'timed_out', 'in_progress'].includes(conclusion)
|
||
? conclusion
|
||
: 'unknown';
|
||
const link = trustedRunUrl(runUrl);
|
||
const lines = [
|
||
COMMENT_MARKER,
|
||
'## CI Regression Report',
|
||
'',
|
||
`${link ? `[Source CI run](${link})` : 'Source CI run'} · source state at render: \`${sourceConclusion}\` · artifacts: \`${available}/5\``,
|
||
'',
|
||
'| Check | Status | Signal |',
|
||
'| --- | :---: | --- |',
|
||
];
|
||
for (const [label, overview] of overviews) {
|
||
lines.push(`| ${label} | ${overview.status} | ${overview.signal} |`);
|
||
}
|
||
lines.push(
|
||
'',
|
||
...renderRelease(sections.release),
|
||
'',
|
||
...renderFrontend(sections.frontend),
|
||
'',
|
||
...renderAgent(sections.agent),
|
||
'',
|
||
...renderRuntime(sections.runtime),
|
||
'',
|
||
...renderCdp(sections.cdp),
|
||
'',
|
||
'_All artifact fields are parsed by the trusted default-branch renderer; missing or invalid inputs remain visible as unavailable._',
|
||
''
|
||
);
|
||
const rendered = lines.join('\n');
|
||
if (Buffer.byteLength(rendered, 'utf8') > MAX_COMMENT_BYTES) {
|
||
throw new Error('rendered CI regression report exceeds 32 KiB');
|
||
}
|
||
return rendered;
|
||
}
|
||
|
||
function parseArgs(argv) {
|
||
const values = {};
|
||
for (let index = 0; index < argv.length; index += 2) {
|
||
const key = argv[index];
|
||
const value = argv[index + 1];
|
||
if (!key?.startsWith('--') || value === undefined) {
|
||
throw new Error('arguments must be --name value pairs');
|
||
}
|
||
values[key.slice(2)] = value;
|
||
}
|
||
if (!values.output) {
|
||
throw new Error('--output is required');
|
||
}
|
||
return values;
|
||
}
|
||
|
||
function main(argv = process.argv.slice(2)) {
|
||
const args = parseArgs(argv);
|
||
const rendered = renderReport({
|
||
releaseRoot: args.release,
|
||
frontendRoot: args.frontend,
|
||
agentRoot: args.agent,
|
||
runtimeRoot: args.runtime,
|
||
cdpRoot: args.cdp,
|
||
runUrl: args['run-url'],
|
||
conclusion: args.conclusion,
|
||
});
|
||
fs.writeFileSync(args.output, rendered, 'utf8');
|
||
}
|
||
|
||
if (require.main === module) {
|
||
main();
|
||
}
|
||
|
||
module.exports = {
|
||
COMMENT_MARKER,
|
||
loadCdp,
|
||
loadFrontend,
|
||
loadRelease,
|
||
renderReport,
|
||
};
|