#!/usr/bin/env node import { spawn, spawnSync } from "node:child_process"; import { createHash, randomBytes } from "node:crypto"; import { closeSync, existsSync, lstatSync, readlinkSync, realpathSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs"; import os from "node:os"; import path from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); const stagePlan = [ { id: "inputs", dependencies: [] }, { id: "tools", dependencies: [] }, { id: "server-unit", dependencies: ["tools"] }, { id: "typecheck", dependencies: [] }, { id: "client-build", dependencies: [] }, { id: "workflow-actions", dependencies: ["tools", "client-build"] }, { id: "server-regressions", dependencies: ["tools", "client-build"] }, { id: "browser", dependencies: ["inputs", "tools", "client-build"] }, ]; function readJson(file) { return JSON.parse(readFileSync(file, "utf8")); } function atomicJson(file, value) { writeFileSync(`${file}.tmp`, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 }); renameSync(`${file}.tmp`, file); } function currentEnvironment(requireGuest = false) { const result = spawnSync("node", [path.join(root, "scripts/qa/full-local-environment.mjs"), requireGuest ? "report-guest" : "report"], { encoding: "utf8", timeout: 30000 }); if (result.status !== 0) throw new Error("environment_global_precondition_failed"); return JSON.parse(result.stdout); } export function createDiagnosticScratch() { return mkdtempSync(path.join(os.tmpdir(), "hyb-qa-inventory-")); } export function sourceSnapshot(directory = root) { directory = realpathSync(directory); const git = (args) => { const result = spawnSync("git", args, { cwd: directory, encoding: "utf8", maxBuffer: 64 * 1024 * 1024 }); if (result.status !== 0) throw new Error("diagnostic_source_unavailable"); return result.stdout; }; const hash = value => createHash("sha256").update(value).digest("hex"); const snapshotFile = file => { const target = path.join(directory, file); let stat; try { stat = lstatSync(target); } catch (error) { if (error.code === "ENOENT") return { file, kind: "missing", sha256: null }; throw error; } if (realpathSync(path.dirname(target)) !== path.dirname(target)) return { file, kind: "unsupported", sha256: null }; const kind = stat.isSymbolicLink() ? "symlink" : stat.isFile() ? "file" : "unsupported"; return { file, kind, mode: stat.mode & 0o777, sha256: kind === "symlink" ? hash(readlinkSync(target)) : kind === "file" ? hash(readFileSync(target)) : null }; }; const files = git(["ls-files", "--cached", "--others", "--exclude-standard", "-z", "--", "scripts/qa", "server", "client", ".trellis/spec"]) .split("\0").filter(Boolean).sort().map(file => ({ file, sha256: snapshotFile(file).sha256 })); const trackedFiles = [...new Set(git(["ls-files", "--cached", "-z"]).split("\0").filter(Boolean))].sort().map(snapshotFile); const porcelainV1Z = git(["status", "--porcelain=v1", "--untracked-files=all", "-z"]); const indexEntriesZ = git(["ls-files", "--stage", "-z"]); const indexFlagsZ = git(["ls-files", "-v", "-z"]); const repository = { porcelainV1Z, statusShort: git(["status", "--short", "--untracked-files=all"]), trackedFiles, trackedDigest: hash(JSON.stringify(trackedFiles)), indexEntriesZ, indexFlagsZ, indexDigest: hash(JSON.stringify([indexEntriesZ, indexFlagsZ])), trackedCoverageComplete: trackedFiles.every(entry => entry.kind !== "unsupported"), }; return { commit: git(["rev-parse", "HEAD"]).trim(), tree: git(["rev-parse", "HEAD^{tree}"]).trim(), dirty: porcelainV1Z.length !== 0, repository, files, digest: createHash("sha256").update(JSON.stringify(files)).digest("hex"), }; } export function compareSnapshots(before, after) { const identityUnchanged = Boolean(before && after && before.commit === after.commit && before.tree === after.tree); const sourceUnchanged = identityUnchanged && before.digest === after.digest; const repositoryComparable = [before, after].every(snapshot => snapshot?.repository?.trackedCoverageComplete && /^[a-f0-9]{64}$/.test(snapshot.repository.trackedDigest ?? "") && /^[a-f0-9]{64}$/.test(snapshot.repository.indexDigest ?? "") && typeof snapshot.repository.porcelainV1Z === "string"); const repositoryUnchanged = Boolean(identityUnchanged && repositoryComparable && before.repository.trackedDigest === after.repository.trackedDigest && before.repository.indexDigest === after.repository.indexDigest && before.repository.porcelainV1Z === after.repository.porcelainV1Z); const candidateIntegrityStatus = !repositoryComparable ? "UNKNOWN" : sourceUnchanged && repositoryUnchanged ? "UNCHANGED" : "CHANGED"; return { sourceUnchanged, repositoryComparable, repositoryUnchanged, candidateIntegrityStatus, cleanAtStart: before?.dirty === false, cleanAtEnd: after?.dirty === false }; } export function browserInventory(discovery, execution) { const actual = new Map(); const key = (entry) => JSON.stringify([entry.id, entry.projectName ?? "", entry.repeatEachIndex ?? 0]); const issues = []; function visit(suites) { for (const suite of suites ?? []) { for (const spec of suite.specs ?? []) { for (const test of spec.tests ?? []) { const identity = { id: spec.id, projectName: test.projectName, repeatEachIndex: test.repeatEachIndex }; if (actual.has(key(identity))) issues.push("duplicate_case"); actual.set(key(identity), { reportedStatus: test.status, expectedStatus: test.expectedStatus, results: (test.results ?? []).map(result => ({ status: result.status, retry: result.retry, duration: result.duration })), }); } } visit(suite.suites); } } visit(execution?.suites); const cases = (discovery?.cases ?? []).map(entry => { const observed = actual.get(key(entry)); actual.delete(key(entry)); return { ...entry, status: observed?.results.at(-1)?.status ?? "NOT_RUN", ...observed }; }); if (actual.size) issues.push("unknown_cases"); return { cases, issues, executionErrors: execution?.errors?.length ?? 0, reportPresent: Boolean(execution) }; } async function main() { const approval = {}; for (const name of ["REASON", "OPERATOR", "APPROVAL_REF"]) { const value = process.env[`QA_DIAGNOSTIC_${name}`]?.trim(); if (!value || value.length > 500) throw new Error(`diagnostic_${name.toLowerCase()}_required`); approval[name.toLowerCase()] = value; } if ((process.env.QA_DB_HOST ?? "127.0.0.1") !== "127.0.0.1" || (process.env.QA_DB_CLIENT ?? "host") !== "host") throw new Error("diagnostic_requires_local_host_database"); if (process.env.QA_KEEP_DB === "1" || process.env.QA_SKIP_CLIENT_BUILD === "1") throw new Error("diagnostic_reuse_not_allowed"); for (const name of ["PLAYWRIGHT_MANUAL", "R70_TRIALRUN_ASSIST", "R70_TRIALRUN_DEEP_ASSIST", "PLAYWRIGHT_OCR_LIVE_BATCH"]) { if (process.env[name] === "1") throw new Error("diagnostic_scope_override_not_allowed"); } const runId = `${Date.now()}_${randomBytes(5).toString("hex")}`; const reportDir = path.resolve(process.env.QA_DIAGNOSTIC_REPORT_DIR ?? process.env.QA_FULL_REPORT_DIR ?? path.join(root, "artifacts/qa-flow/full-local-diagnostic", runId)); if (existsSync(reportDir) && readdirSync(reportDir).length) throw new Error("diagnostic_report_directory_not_empty"); mkdirSync(reportDir, { recursive: true, mode: 0o700 }); const scratch = createDiagnosticScratch(); const report = { schemaVersion: 1, kind: "FULL_LOCAL_DIAGNOSTIC_NOT_RELEASE_RECEIPT", profile: "full-local-diagnostic", status: "PARTIAL", runId, approval, startedAt: new Date().toISOString(), endedAt: null, nodeVersion: process.version, ci: process.env.CI ?? null, stages: stagePlan.map(stage => ({ ...stage, status: "NOT_RUN" })), limits: ["No release eligibility, even with zero observed failures.", "Nested fail-fast and serial dependencies remain observable coverage gaps.", "server-regressions includes a separate workflow-actions invocation.", "Diagnostic duration is not normal full-local green-path duration."], }; let interrupted = null; let activeChild = null; const save = () => atomicJson(path.join(reportDir, "diagnostic-results.json"), report); const interrupt = (signal) => { interrupted = signal; if (activeChild?.pid) { try { process.kill(-activeChild.pid, signal); } catch { report.signalDeliveryFailed = true; } } }; const onInt = () => interrupt("SIGINT"); const onTerm = () => interrupt("SIGTERM"); process.on("SIGINT", onInt); process.on("SIGTERM", onTerm); try { report.environmentBefore = currentEnvironment(); report.sourceBefore = sourceSnapshot(); save(); for (const stage of report.stages) { const blockedBy = stage.dependencies.filter(id => report.stages.find(item => item.id === id).status !== "PASS"); if (interrupted || blockedBy.length) { stage.reason = interrupted ? "interrupted" : "prerequisite_failed"; stage.blockedBy = blockedBy; save(); continue; } stage.environmentBefore = currentEnvironment(); const stageDir = path.join(reportDir, stage.id); const temporary = path.join(scratch, stage.id); mkdirSync(stageDir); mkdirSync(temporary); mkdirSync(path.join(temporary, "storage")); const environment = { ...process.env, QA_DB_HOST: "127.0.0.1", QA_DB_CLIENT: "host", QA_KEEP_DB: "0", QA_SKIP_CLIENT_BUILD: "0", QA_DB_NAME: `hyb_oa_diag_${runId}_${stage.id.replaceAll("-", "_")}`, QA_DIAGNOSTIC_STAGE: stage.id, QA_DIAGNOSTIC_STAGE_DIR: stageDir, QA_FULL_REPORT_DIR: path.join(reportDir, "full-local-report"), QA_SERVER_LOG: path.join(stageDir, "server.log"), QA_SERVER_PID_FILE: path.join(stageDir, "server.pid"), TMPDIR: temporary, STORAGE_DIR: path.join(temporary, "storage"), STORAGE_DRIVER: "local", QA_HOST: "127.0.0.1", JOBS_ENABLED: "false", E2E_MODE: "local", }; if (stage.id === "browser") { const fixture = readJson(path.join(reportDir, "inputs/pwa-fixture.json")); if (!fixture.root.startsWith(`${path.join(scratch, "inputs")}${path.sep}`)) throw new Error("diagnostic_fixture_ownership_invalid"); Object.assign(environment, { QA_DIAGNOSTIC_PWA_ROOT: fixture.root, PWA_BUILD_A_DIR: fixture.buildA, PWA_BUILD_B_DIR: fixture.buildB, PWA_BUILD_A_SHA: fixture.shaA, PWA_BUILD_B_SHA: fixture.shaB }); } stage.command = ["bash", "scripts/qa/run-web-gates.sh", "__full-local-diagnostic-stage", stage.id]; stage.database = environment.QA_DB_NAME; stage.startedAt = new Date().toISOString(); stage.status = "RUNNING"; save(); console.log(`[diagnostic] ${stage.id} started`); const descriptor = openSync(path.join(stageDir, "stage.log"), "w", 0o600); const outcome = await new Promise(resolve => { activeChild = spawn("bash", stage.command.slice(1), { cwd: root, env: environment, stdio: ["ignore", descriptor, descriptor], detached: true }); activeChild.once("error", () => resolve({ code: 1, signal: null, spawnFailed: true })); activeChild.once("close", (code, signal) => resolve({ code, signal })); }); activeChild = null; closeSync(descriptor); stage.endedAt = new Date().toISOString(); stage.durationMs = Date.parse(stage.endedAt) - Date.parse(stage.startedAt); stage.exitCode = outcome.code; stage.signal = outcome.signal; const completion = path.join(stageDir, "outcome.json"); stage.completion = existsSync(completion) ? readJson(completion) : null; stage.status = outcome.code === 0 && stage.completion?.cleanupExit === 0 ? "PASS" : "FAIL"; stage.failureKind = stage.status === "PASS" ? null : stage.completion?.cleanupExit ? "cleanup" : stage.completion?.phase === "test" ? "test" : "environment"; stage.environmentAfter = currentEnvironment(stage.id === "browser"); save(); console.log(`[diagnostic] ${stage.id} ${stage.status} exit=${outcome.code}`); } } catch (error) { report.runnerError = error.message; } finally { report.interrupted = interrupted; report.environmentQualified = Boolean(report.stages.find(stage => stage.id === "browser")?.environmentAfter?.bootstrapObservation?.valid) && !report.runnerError; for (const stage of report.stages) { if (stage.status === "NOT_RUN" && !stage.reason) stage.reason = interrupted ? "interrupted" : "runner_error"; } const discoveryFile = path.join(reportDir, "full-local-report/discovery-coverage.json"); const browserFile = path.join(reportDir, "full-local-report/playwright-results.json"); let discovery = null; try { discovery = existsSync(discoveryFile) ? readJson(discoveryFile) : null; const planFile = path.join(reportDir, "full-local-report/coverage-plan.json"); if (existsSync(planFile)) { const plan = readJson(planFile); if (plan.status !== "PASS") throw new Error("invalid_layered_plan"); discovery = { ...discovery, cases: plan.localCases }; report.coverageObligations = plan.obligations; const resultFile = path.join(reportDir, "full-local-report/local-coverage-result.json"); report.localCoverage = existsSync(resultFile) ? readJson(resultFile) : { status: "NOT_RUN" }; } report.browser = browserInventory(discovery, existsSync(browserFile) ? readJson(browserFile) : null); } catch { report.browser = browserInventory(discovery, null); report.browser.issues.push("report_unreadable"); } try { rmSync(scratch, { recursive: true }); report.scratchCleanup = "PASS"; } catch { report.scratchCleanup = "FAIL"; } try { report.sourceAfter = sourceSnapshot(); Object.assign(report, compareSnapshots(report.sourceBefore, report.sourceAfter)); } catch { report.repositorySnapshotError = "post_run_snapshot_unavailable"; report.repositoryUnchanged = false; report.candidateIntegrityStatus = "UNKNOWN"; } report.endedAt = new Date().toISOString(); report.durationMs = Date.parse(report.endedAt) - Date.parse(report.startedAt); report.exitCode = 2; save(); process.off("SIGINT", onInt); process.off("SIGTERM", onTerm); } console.log("[diagnostic] PARTIAL — no release eligibility"); process.exitCode = 2; } if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) { main().catch(error => { console.error(`[diagnostic] ${error.message}`); process.exitCode = 1; }); }