diff --git a/scripts/ship-release.mjs b/scripts/ship-release.mjs
index 3e911c4..70fb4e2 100644
--- a/scripts/ship-release.mjs
+++ b/scripts/ship-release.mjs
@@ -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 = `
@@ -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; } }
@@ -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)}
Artifacts
@@ -136,6 +146,23 @@ function writeReleaseDashboard({ qaVersion, commit, shortCommit, qaReport, qaRep
+ Repeated Battle Outcomes
+
+ | Battle | Id | Outcome | Count |
+
+ ${
+ battleOutcomes.length > 0
+ ? battleOutcomes
+ .map((outcome) => {
+ const cssClass = outcome.outcome === 'victory' ? 'ok' : 'bad';
+ return `| ${escapeHtml(String(outcome.battleNo))} | ${escapeHtml(outcome.battleId ?? '')} | ${escapeHtml(outcome.outcome)} | ${escapeHtml(String(outcome.count ?? 1))} |
`;
+ })
+ .join('')
+ : '| No battle outcome data recorded. |
'
+ }
+
+
+
Repeated Objective Misses
| Battle | Objective | Misses | Rate | Latest Reason |
@@ -155,17 +182,20 @@ function writeReleaseDashboard({ qaVersion, commit, shortCommit, qaReport, qaRep
Repeat Runs
- | Run | Victories | Missed Objectives | Battles With Misses |
+ | Run | Status | Exit | Victories | Battle Failures | Missed Objectives | Battles With Misses | Report |
${
runRows.length > 0
? runRows
.map(
- (run) =>
- `| ${escapeHtml(String(run.run))} | ${escapeHtml(String(run.victories))} | ${escapeHtml(String(run.missedObjectiveCount))} | ${escapeHtml((run.battlesWithMissedObjectives ?? []).join(', ') || '-')} |
`
+ (run) => {
+ const reportLink = distAssetPath(run.path);
+ const statusClass = run.exitCode === 0 && run.failures === 0 ? 'ok' : 'bad';
+ return `| ${escapeHtml(String(run.run))} | ${escapeHtml(run.outcome ?? 'unknown')} | ${escapeHtml(String(run.exitCode ?? 0))} | ${escapeHtml(String(run.victories))} | ${escapeHtml(String(run.failures ?? 0))} | ${escapeHtml(String(run.missedObjectiveCount))} | ${escapeHtml((run.battlesWithMissedObjectives ?? []).join(', ') || '-')} | ${reportLink ? `${escapeHtml(reportLink)}` : '-'} |
`;
+ }
)
.join('')
- : '| Repeat QA was not run for this release. |
'
+ : '| Repeat QA was not run for this release. |
'
}
@@ -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('&', '&')
diff --git a/scripts/verify-public-deploy.mjs b/scripts/verify-public-deploy.mjs
index caaab76..77a9765 100644
--- a/scripts/verify-public-deploy.mjs
+++ b/scripts/verify-public-deploy.mjs
@@ -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}`);
}