Publish early battle QA report

This commit is contained in:
2026-07-05 20:30:51 +09:00
parent 620c9c3eca
commit 5fbfada47a
3 changed files with 91 additions and 8 deletions

View File

@@ -1,6 +1,7 @@
import { spawn } from 'node:child_process'; import { spawn } from 'node:child_process';
import { existsSync } from 'node:fs'; import { existsSync } from 'node:fs';
import { readFile, writeFile } from 'node:fs/promises'; import { mkdir, readFile, writeFile } from 'node:fs/promises';
import { dirname } from 'node:path';
import { chromium } from 'playwright'; import { chromium } from 'playwright';
const cliOptions = parseCliOptions(process.argv.slice(2)); const cliOptions = parseCliOptions(process.argv.slice(2));
@@ -9,6 +10,7 @@ const targetUrl = process.env.QA_URL ?? `http://127.0.0.1:${defaultQaPort}/`;
const headless = process.env.QA_HEADLESS !== '0'; const headless = process.env.QA_HEADLESS !== '0';
const maxRounds = Number(process.env.QA_MAX_ROUNDS ?? 80); const maxRounds = Number(process.env.QA_MAX_ROUNDS ?? 80);
const cumulativeMode = process.env.QA_CUMULATIVE === '1'; const cumulativeMode = process.env.QA_CUMULATIVE === '1';
const qaReportPath = cliOptions.report ?? process.env.QA_REPORT_PATH ?? 'dist/qa-representative-battles.json';
const campaignStorageKey = 'heros-web:campaign-state'; const campaignStorageKey = 'heros-web:campaign-state';
const campaignSlotStorageKey = 'heros-web:campaign-state:slot-1'; const campaignSlotStorageKey = 'heros-web:campaign-state:slot-1';
const campaignSnapshotPath = process.env.QA_CAMPAIGN_SNAPSHOT_PATH; const campaignSnapshotPath = process.env.QA_CAMPAIGN_SNAPSHOT_PATH;
@@ -733,6 +735,8 @@ try {
); );
} }
await writeQaReport(qaReportPath, results, failed);
if (failed.length > 0) { if (failed.length > 0) {
console.dir( console.dir(
failed.map((result) => ({ failed.map((result) => ({
@@ -1570,3 +1574,69 @@ function parseCliOptions(args) {
return options; return options;
} }
async function writeQaReport(reportPath, results, failed) {
if (!reportPath) {
return;
}
await mkdir(dirname(reportPath), { recursive: true });
const report = buildQaReport(results, failed);
await writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8');
console.log(`Wrote QA report ${reportPath}`);
}
function buildQaReport(results, failed) {
const missedObjectives = results.flatMap((result) =>
(result.objectiveDetails ?? [])
.filter((objective) => !objective.achieved)
.map((objective) => ({
battleNo: result.no,
battleId: result.id,
objectiveId: objective.id,
category: objective.category,
summary: objective.summary,
detail: objective.detail,
failureReason: objective.failureReason ?? ''
}))
);
return {
app: 'heros_web',
generatedAt: new Date().toISOString(),
set: qaSetName,
targetUrl,
headless,
maxRounds,
cumulativeMode,
requestedBattles: [...requestedBattles],
battleCount: results.length,
summary: {
victories: results.filter((result) => result.outcome === 'victory').length,
failures: failed.length,
missedObjectiveCount: missedObjectives.length,
battlesWithMissedObjectives: [...new Set(missedObjectives.map((objective) => objective.battleNo))]
},
missedObjectives,
results: results.map((result) => ({
no: result.no,
id: result.id,
outcome: result.outcome,
finalTurn: result.finalTurn,
alliesAlive: result.alliesAlive,
enemiesAlive: result.enemiesAlive,
itemsUsed: result.itemsUsed,
supports: result.supports,
objectivesAchieved: result.objectivesAchieved,
objectiveCount: result.objectiveCount,
missedObjectives: (result.objectiveDetails ?? []).filter((objective) => !objective.achieved),
protectedStatus: result.protectedStatus,
targetStatus: result.targetStatus,
allyStatus: result.allyStatus,
enemyStatus: result.enemyStatus,
battleLogTail: result.battleLogTail,
campSuppliesUsed: result.campSuppliesUsed,
campaign: result.campaign
}))
};
}

View File

@@ -5,11 +5,12 @@ import { join } from 'node:path';
const qaVersion = process.env.PUBLIC_QA_VERSION ?? makeQaVersion(); const qaVersion = process.env.PUBLIC_QA_VERSION ?? makeQaVersion();
const releaseCommit = gitValue(['rev-parse', 'HEAD'], 'unknown'); const releaseCommit = gitValue(['rev-parse', 'HEAD'], 'unknown');
const releaseShortCommit = gitValue(['rev-parse', '--short', 'HEAD'], 'unknown'); const releaseShortCommit = gitValue(['rev-parse', '--short', 'HEAD'], 'unknown');
const qaReportFile = 'qa-early-report.json';
assertNoTrackedChanges(); assertNoTrackedChanges();
runPnpmScript('verify:local-release'); runPnpmScript('verify:local-release');
runPnpmScript('qa:early'); runPnpmScript('qa:early', { QA_REPORT_PATH: join('dist', qaReportFile) });
writeReleaseManifest(qaVersion, releaseCommit, releaseShortCommit); writeReleaseManifest(qaVersion, releaseCommit, releaseShortCommit, qaReportFile);
runPnpmScript('deploy:nas:dist'); runPnpmScript('deploy:nas:dist');
runPnpmScript('verify:public-deploy', { PUBLIC_QA_VERSION: qaVersion, PUBLIC_QA_COMMIT: releaseCommit }); runPnpmScript('verify:public-deploy', { PUBLIC_QA_VERSION: qaVersion, PUBLIC_QA_COMMIT: releaseCommit });
@@ -35,7 +36,7 @@ function assertNoTrackedChanges() {
} }
} }
function writeReleaseManifest(qaVersion, commit, shortCommit) { function writeReleaseManifest(qaVersion, commit, shortCommit, qaReport) {
const manifest = { const manifest = {
app: 'heros_web', app: 'heros_web',
qaVersion, qaVersion,
@@ -43,6 +44,7 @@ function writeReleaseManifest(qaVersion, commit, shortCommit) {
shortCommit, shortCommit,
branch: gitValue(['branch', '--show-current'], 'unknown'), branch: gitValue(['branch', '--show-current'], 'unknown'),
generatedAt: new Date().toISOString(), generatedAt: new Date().toISOString(),
qaReport,
verification: ['verify:local-release', 'qa:early', 'verify:public-deploy'] verification: ['verify:local-release', 'qa:early', 'verify:public-deploy']
}; };

View File

@@ -117,6 +117,13 @@ try {
`Expected deployed release manifest commit ${expectedCommit}: ${JSON.stringify(releaseManifest)}` `Expected deployed release manifest commit ${expectedCommit}: ${JSON.stringify(releaseManifest)}`
); );
} }
if (releaseManifest.qaReport) {
const qaReport = await readJsonAsset(targetUrl, releaseManifest.qaReport);
assert(qaReport.app === 'heros_web', `Expected deployed QA report app id: ${JSON.stringify(qaReport)}`);
assert(Array.isArray(qaReport.results) && qaReport.results.length > 0, `Expected deployed QA report results: ${JSON.stringify(qaReport)}`);
assert(typeof qaReport.generatedAt === 'string' && qaReport.generatedAt.length > 0, `Expected deployed QA report timestamp: ${JSON.stringify(qaReport)}`);
console.log(`Verified QA report ${releaseManifest.qaReport} with ${qaReport.results.length} battles`);
}
const relevantLogs = consoleMessages.filter( const relevantLogs = consoleMessages.filter(
(message) => message.type === 'error' || (isWarning(message.type) && relevantLogPattern.test(message.text)) (message) => message.type === 'error' || (isWarning(message.type) && relevantLogPattern.test(message.text))
@@ -145,10 +152,14 @@ function isWarning(type) {
} }
async function readReleaseManifest(pageUrl) { async function readReleaseManifest(pageUrl) {
const manifestUrl = new URL('release-manifest.json', pageUrl); return readJsonAsset(pageUrl, 'release-manifest.json');
manifestUrl.searchParams.set('v', expectedQaVersion ?? String(Date.now())); }
const response = await fetch(manifestUrl, { signal: AbortSignal.timeout(90000) });
assert(response.ok, `Expected deployed release manifest at ${manifestUrl.href}: ${response.status} ${response.statusText}`); async function readJsonAsset(pageUrl, assetPath) {
const assetUrl = new URL(assetPath, pageUrl);
assetUrl.searchParams.set('v', expectedQaVersion ?? String(Date.now()));
const response = await fetch(assetUrl, { signal: AbortSignal.timeout(90000) });
assert(response.ok, `Expected deployed JSON asset at ${assetUrl.href}: ${response.status} ${response.statusText}`);
return response.json(); return response.json();
} }