#!/usr/bin/env node import { execFileSync } from "node:child_process"; import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync, } from "node:fs"; import path from "node:path"; import process from "node:process"; import { fileURLToPath } from "node:url"; export const READINESS_STEP_NAMES = [ "environment", "publicReady", "publicBundle", "pendingOcr", "liveSmoke", "liveOcr", "privacy", ]; const CHILD_STEP_NAMES = ["environment", "pendingOcr", "liveSmoke", "liveOcr"]; const BLOCKING_STATUS_PATTERN = /FAIL|ERROR|BLOCKED/i; const MANUAL_STATUS_PATTERN = /SKIP|MISSING|PAUSED|PARTIAL|PASS_WITH_FINDINGS/i; export function toReportRef(rootDir, file) { const absolute = path.resolve(file); const relative = path.relative(path.resolve(rootDir), absolute); if (relative && !relative.startsWith("..") && !path.isAbsolute(relative)) { return relative.split(path.sep).join("/"); } return path.basename(absolute); } function safeBaseUrl(value) { try { const url = new URL(String(value || "")); url.username = ""; url.password = ""; return `${url.origin}${url.pathname.replace(/\/+$/, "")}`; } catch { return null; } } function normalizeFindingCount(status, value) { const parsed = Number(value || 0); const count = Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : 0; return status === "PASS" ? count : Math.max(1, count); } function normalizeStep(name, input = {}) { const status = String(input.status || "MISSING").toUpperCase(); return { name, status, kind: input.kind === "required" ? "required" : "finding_allowed", code: Number(input.code || 0), findingCount: normalizeFindingCount(status, input.findingCount), detail: String(input.detail || "").slice(0, 240), logRef: input.logRef || null, artifactRef: input.artifactRef || null, checkedAt: input.checkedAt || null, }; } export function evaluateReadinessSteps(stepInputs) { const steps = Object.fromEntries( READINESS_STEP_NAMES.map((name) => [name, normalizeStep(name, stepInputs?.[name])]), ); const findings = []; for (const [name, step] of Object.entries(steps)) { if (step.status === "PASS" && step.findingCount === 0) { continue; } const blocking = step.kind === "required" && BLOCKING_STATUS_PATTERN.test(step.status); findings.push({ source: name, status: step.status, severity: blocking ? "blocker" : MANUAL_STATUS_PATTERN.test(step.status) ? "manual_or_config" : "finding", findingCount: step.findingCount, reportRef: step.artifactRef || step.logRef, }); } const findingCount = findings.reduce((total, finding) => total + Math.max(1, finding.findingCount), 0); const status = findings.some((finding) => finding.severity === "blocker") ? "FAIL" : findingCount > 0 ? "PASS_WITH_FINDINGS" : "PASS"; return { status, findingCount, findings, steps }; } function readJson(file) { if (!existsSync(file)) { return null; } try { return JSON.parse(readFileSync(file, "utf8")); } catch { return null; } } function currentCommit(rootDir) { try { return execFileSync("git", ["rev-parse", "HEAD"], { cwd: rootDir, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], }).trim(); } catch { return "unknown"; } } export function buildReadinessSummary({ rootDir, reportDir, baseUrl, commit = currentCommit(rootDir), checkedAt = new Date().toISOString(), }) { const stepInputs = Object.fromEntries( READINESS_STEP_NAMES.map((name) => [ name, readJson(path.join(reportDir, `${name}.step.json`)) || { status: "MISSING", kind: "required", logRef: toReportRef(rootDir, path.join(reportDir, `${name}.log`)), }, ]), ); const evaluated = evaluateReadinessSteps(stepInputs); const environmentReport = readJson(path.join(reportDir, "environment.json")); const childReports = Object.fromEntries( CHILD_STEP_NAMES.map((name) => { const step = evaluated.steps[name]; return [name, { status: step.status, findingCount: step.findingCount, reportRef: step.artifactRef || step.logRef, }]; }), ); return { issueId: process.env.QA_ISSUE_ID?.trim() || null, mode: "live-smoke-readiness", status: evaluated.status, conclusion: evaluated.status, baseUrl: safeBaseUrl(baseUrl), commit, sourceCommit: commit, expectedBuildSha: environmentReport?.publicReady?.expectedBuildSha ?? null, observedBuildSha: environmentReport?.publicReady?.observedBuildSha ?? null, buildShaMatches: environmentReport?.publicReady?.buildShaMatches ?? null, checkedAt, reportRef: toReportRef(rootDir, reportDir), findingCount: evaluated.findingCount, findings: evaluated.findings, childReports, steps: evaluated.steps, boundaries: { readOnlyChecksOnly: true, noCredentialValues: true, noInvoiceSamplesEmbedded: true, noAbsoluteWorkspacePaths: true, }, }; } function writeStep({ rootDir, reportDir, name, status, kind, code, findingCount, detail, artifactFile }) { if (!READINESS_STEP_NAMES.includes(name)) { throw new Error(`unknown readiness step: ${name}`); } const step = normalizeStep(name, { status, kind, code, findingCount, detail, logRef: toReportRef(rootDir, path.join(reportDir, `${name}.log`)), artifactRef: artifactFile ? toReportRef(rootDir, artifactFile) : null, checkedAt: new Date().toISOString(), }); mkdirSync(reportDir, { recursive: true }); writeFileSync(path.join(reportDir, `${name}.step.json`), `${JSON.stringify(step, null, 2)}\n`, "utf8"); } function collectReportFiles(directory) { if (!existsSync(directory)) { return []; } const files = []; for (const entry of readdirSync(directory)) { const file = path.join(directory, entry); const stat = statSync(file); if (stat.isDirectory()) { files.push(...collectReportFiles(file)); } else if (/\.(json|log|txt)$/i.test(entry) && entry !== "privacy-findings.json") { files.push(file); } } return files; } function escapeRegExp(value) { return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } export function scanReadinessPrivacy({ rootDir, reportDir }) { const patterns = [ { category: "workspace_path", pattern: new RegExp(escapeRegExp(path.resolve(rootDir)), "g") }, { category: "secret_field", pattern: /\b[A-Z][A-Z0-9_]*(?:PASSWORD|SECRET|TOKEN|ENCODING_AES_KEY)[A-Z0-9_]*\b/g }, { category: "bearer_value", pattern: /Authorization:\s*Bearer\s+[A-Za-z0-9._-]{8,}/gi }, { category: "private_key", pattern: /-----BEGIN [A-Z ]*PRIVATE KEY-----/g }, { category: "email", pattern: /[\w.%+-]+@[\w.-]+\.[A-Za-z]{2,}/g }, { category: "phone", pattern: /(? { for (const { category, pattern } of patterns) { pattern.lastIndex = 0; if (pattern.test(line)) { findings.push({ fileRef: toReportRef(rootDir, file), line: index + 1, category, }); } } }); } const report = { status: findings.length ? "FAIL" : "PASS", findingCount: findings.length, findings, checkedAt: new Date().toISOString(), }; writeFileSync(path.join(reportDir, "privacy-findings.json"), `${JSON.stringify(report, null, 2)}\n`, "utf8"); return report; } function usage() { console.error("Usage: live-smoke-readiness-report.mjs ..."); } function main() { const [command, ...args] = process.argv.slice(2); if (command === "write-step") { const [rootDir, reportDir, name, status, kind, code, findingCount, detail = "", artifactFile = ""] = args; writeStep({ rootDir, reportDir, name, status, kind, code, findingCount, detail, artifactFile }); return; } if (command === "summarize") { const [rootDir, reportDir, baseUrl] = args; const summary = buildReadinessSummary({ rootDir, reportDir, baseUrl }); writeFileSync(path.join(reportDir, "summary.json"), `${JSON.stringify(summary, null, 2)}\n`, "utf8"); console.log(JSON.stringify({ status: summary.status, findingCount: summary.findingCount })); return; } if (command === "privacy") { const [rootDir, reportDir] = args; const report = scanReadinessPrivacy({ rootDir, reportDir }); console.log(JSON.stringify({ status: report.status, findingCount: report.findingCount })); if (report.findingCount > 0) { process.exitCode = 1; } return; } usage(); process.exitCode = 2; } const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url); if (isMain) { main(); }