Improve release QA dashboard diagnostics

This commit is contained in:
2026-07-05 21:26:52 +09:00
parent 9da80e8c83
commit c8cf060790
2 changed files with 54 additions and 5 deletions

View File

@@ -82,7 +82,13 @@ function writeReleaseDashboard({ qaVersion, commit, shortCommit, qaReport, qaRep
const qa = readJsonFromDist(qaReport);
const repeat = qaRepeatReport ? readJsonFromDist(qaRepeatReport) : undefined;
const missedObjectives = repeat?.missedObjectives ?? qa?.missedObjectives ?? [];
const battleOutcomes = repeat?.battleOutcomes ?? battleOutcomesFromQaReport(qa);
const runRows = repeat?.runs ?? [];
const processFailureCount = repeat?.summary?.runsWithProcessFailures ?? 0;
const battleFailureCount = battleOutcomes
.filter((outcome) => outcome.outcome !== 'victory')
.reduce((total, outcome) => total + (outcome.count ?? 1), 0);
const totalMissedObjectives = repeat?.summary?.totalMissedObjectives ?? qa?.summary?.missedObjectiveCount ?? missedObjectives.length;
const html = `<!doctype html>
<html lang="ko">
@@ -98,7 +104,7 @@ function writeReleaseDashboard({ qaVersion, commit, shortCommit, qaReport, qaRep
h1 { font-size: 28px; }
h2 { font-size: 18px; margin-top: 28px; }
.meta, .grid, table { margin-top: 16px; }
.meta { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; }
.meta { display: grid; grid-template-columns: repeat(auto-fit, minmax(158px, 1fr)); gap: 10px; }
.tile { border: 1px solid #665941; background: #221f18; border-radius: 8px; padding: 12px; }
.label { color: #b9ad91; font-size: 12px; }
.value { margin-top: 5px; font-size: 18px; font-weight: 700; word-break: break-word; }
@@ -108,6 +114,7 @@ function writeReleaseDashboard({ qaVersion, commit, shortCommit, qaReport, qaRep
tr:last-child td { border-bottom: 0; }
.ok { color: #a9db8f; }
.warn { color: #f3c96b; }
.bad { color: #ff9b86; }
a { color: #9cc6ff; }
@media (max-width: 820px) { .meta { grid-template-columns: 1fr 1fr; } }
</style>
@@ -120,6 +127,9 @@ function writeReleaseDashboard({ qaVersion, commit, shortCommit, qaReport, qaRep
${metricTile('Commit', shortCommit)}
${metricTile('Early QA', `${qa?.summary?.victories ?? 0}/${qa?.battleCount ?? 0} victories`)}
${metricTile('Repeat QA', repeat ? `${repeat.summary.runs} runs` : 'not run')}
${metricTile('Battle Failures', battleFailureCount)}
${metricTile('Process Failures', processFailureCount)}
${metricTile('Objective Misses', totalMissedObjectives)}
</section>
<h2>Artifacts</h2>
@@ -136,6 +146,23 @@ function writeReleaseDashboard({ qaVersion, commit, shortCommit, qaReport, qaRep
</tbody>
</table>
<h2>Repeated Battle Outcomes</h2>
<table>
<thead><tr><th>Battle</th><th>Id</th><th>Outcome</th><th>Count</th></tr></thead>
<tbody>
${
battleOutcomes.length > 0
? battleOutcomes
.map((outcome) => {
const cssClass = outcome.outcome === 'victory' ? 'ok' : 'bad';
return `<tr><td>${escapeHtml(String(outcome.battleNo))}</td><td>${escapeHtml(outcome.battleId ?? '')}</td><td class="${cssClass}">${escapeHtml(outcome.outcome)}</td><td>${escapeHtml(String(outcome.count ?? 1))}</td></tr>`;
})
.join('')
: '<tr><td colspan="4">No battle outcome data recorded.</td></tr>'
}
</tbody>
</table>
<h2>Repeated Objective Misses</h2>
<table>
<thead><tr><th>Battle</th><th>Objective</th><th>Misses</th><th>Rate</th><th>Latest Reason</th></tr></thead>
@@ -155,17 +182,20 @@ function writeReleaseDashboard({ qaVersion, commit, shortCommit, qaReport, qaRep
<h2>Repeat Runs</h2>
<table>
<thead><tr><th>Run</th><th>Victories</th><th>Missed Objectives</th><th>Battles With Misses</th></tr></thead>
<thead><tr><th>Run</th><th>Status</th><th>Exit</th><th>Victories</th><th>Battle Failures</th><th>Missed Objectives</th><th>Battles With Misses</th><th>Report</th></tr></thead>
<tbody>
${
runRows.length > 0
? runRows
.map(
(run) =>
`<tr><td>${escapeHtml(String(run.run))}</td><td>${escapeHtml(String(run.victories))}</td><td>${escapeHtml(String(run.missedObjectiveCount))}</td><td>${escapeHtml((run.battlesWithMissedObjectives ?? []).join(', ') || '-')}</td></tr>`
(run) => {
const reportLink = distAssetPath(run.path);
const statusClass = run.exitCode === 0 && run.failures === 0 ? 'ok' : 'bad';
return `<tr><td>${escapeHtml(String(run.run))}</td><td class="${statusClass}">${escapeHtml(run.outcome ?? 'unknown')}</td><td>${escapeHtml(String(run.exitCode ?? 0))}</td><td>${escapeHtml(String(run.victories))}</td><td>${escapeHtml(String(run.failures ?? 0))}</td><td>${escapeHtml(String(run.missedObjectiveCount))}</td><td>${escapeHtml((run.battlesWithMissedObjectives ?? []).join(', ') || '-')}</td><td>${reportLink ? `<a href="${escapeAttribute(reportLink)}">${escapeHtml(reportLink)}</a>` : '-'}</td></tr>`;
}
)
.join('')
: '<tr><td colspan="4">Repeat QA was not run for this release.</td></tr>'
: '<tr><td colspan="8">Repeat QA was not run for this release.</td></tr>'
}
</tbody>
</table>
@@ -179,6 +209,15 @@ function writeReleaseDashboard({ qaVersion, commit, shortCommit, qaReport, qaRep
console.log(`Wrote dist/${releaseDashboardFile}`);
}
function battleOutcomesFromQaReport(report) {
return (report?.results ?? []).map((result) => ({
battleNo: result.no,
battleId: result.id,
outcome: result.outcome,
count: 1
}));
}
function readJsonFromDist(fileName) {
return JSON.parse(readFileSync(join('dist', fileName), 'utf8'));
}
@@ -191,6 +230,13 @@ function formatRate(value) {
return typeof value === 'number' ? `${Math.round(value * 100)}%` : '-';
}
function distAssetPath(filePath) {
if (!filePath) {
return '';
}
return String(filePath).replaceAll('\\', '/').replace(/^dist\//, '');
}
function escapeHtml(value) {
return String(value)
.replaceAll('&', '&amp;')

View File

@@ -138,6 +138,9 @@ try {
const dashboard = await readTextAsset(targetUrl, releaseManifest.releaseDashboard);
assert(dashboard.includes('Heros Web Release QA'), `Expected deployed release dashboard title: ${dashboard.slice(0, 200)}`);
assert(dashboard.includes(releaseManifest.shortCommit ?? releaseManifest.commit), `Expected deployed release dashboard commit: ${dashboard.slice(0, 400)}`);
assert(dashboard.includes('Repeated Battle Outcomes'), `Expected deployed release dashboard battle outcomes section: ${dashboard.slice(0, 800)}`);
assert(dashboard.includes('Process Failures'), `Expected deployed release dashboard process failure metric: ${dashboard.slice(0, 800)}`);
assert(dashboard.includes('Repeat Runs'), `Expected deployed release dashboard repeat runs section: ${dashboard.slice(0, 800)}`);
console.log(`Verified release dashboard ${releaseManifest.releaseDashboard}`);
}