release-gate.ts

3.3 KB · raw

1/**2 * PB-26-210: Release browser-gate validation.3 *4 * Pure functions that verify a gate result has no silent failures.5 * A release cannot report success with zero tests, unexpected skips,6 * flaky retries, or missing required projects.7 */89export interface ProjectResult {10  name: string;11  totalTests: number;12  passed: number;13  skipped: number;14  flaky: number;15}1617export interface GateResult {18  projects: ProjectResult[];19}2021export const REQUIRED_BROWSER_PROJECTS = ["chrome-release"] as const;2223export interface PlaywrightJsonReport {24  suites?: Array<{25    title?: string;26    specs?: Array<{ tests?: Array<{ status?: string; results?: Array<{ status?: string }> }> }>;27    suites?: PlaywrightJsonReport["suites"];28  }>;29}3031export function gateResultFromPlaywright(report: PlaywrightJsonReport): GateResult {32  const projects = new Map<string, ProjectResult>();33  const visit = (suites: PlaywrightJsonReport["suites"] = []) => {34    for (const suite of suites) {35      if (suite.title && REQUIRED_BROWSER_PROJECTS.includes(suite.title as (typeof REQUIRED_BROWSER_PROJECTS)[number])) {36        const project = projects.get(suite.title) ?? { name: suite.title, totalTests: 0, passed: 0, skipped: 0, flaky: 0 };37        for (const spec of suite.specs ?? []) for (const test of spec.tests ?? []) {38          project.totalTests++;39          const results = test.results ?? [];40          const status = results.at(-1)?.status ?? test.status;41          if (status === "skipped") project.skipped++;42          else if (status === "passed") project.passed++;43          if (results.length > 1 && results.some((result) => result.status === "passed")) project.flaky++;44        }45        projects.set(suite.title, project);46      }47      visit(suite.suites);48    }49  };50  visit(report.suites);51  return { projects: [...projects.values()] };52}5354export function validatePlaywrightJsonReport(report: PlaywrightJsonReport): string[] {55  return validateGateResult(gateResultFromPlaywright(report));56}5758export function formatGateErrors(errors: string[]): never {59  throw new Error(`Release browser gate validation failed:\n  ${errors.join("\n  ")}`);60}6162export function assertValidPlaywrightJsonReport(report: PlaywrightJsonReport): void {63  const errors = validatePlaywrightJsonReport(report);64  if (errors.length > 0) formatGateErrors(errors);65}666768/**69 * Validate a gate result. Returns a list of error strings — empty means pass.70 * The release gate fails (non-zero) on any error.71 */72export function validateGateResult(73  result: GateResult,74  requiredProjects: readonly string[] = REQUIRED_BROWSER_PROJECTS,75): string[] {76  const errors: string[] = [];7778  const totalTests = result.projects.reduce((sum, p) => sum + p.totalTests, 0);79  if (totalTests === 0) {80    errors.push("zero tests discovered across all projects");81  }8283  for (const name of requiredProjects) {84    const project = result.projects.find((p) => p.name === name);85    if (!project) {86      errors.push(`required project "${name}" is missing`);87      continue;88    }89    if (project.totalTests === 0) {90      errors.push(`project "${name}" has zero tests`);91    }92    if (project.skipped > 0) {93      errors.push(`project "${name}" has ${project.skipped} skipped test(s)`);94    }95    if (project.flaky > 0) {96      errors.push(`project "${name}" has ${project.flaky} flaky retry/retr(y)ies`);97    }98  }99100  return errors;101}