ci.ts

5.4 KB · raw

1#!/usr/bin/env bun2// peach-browser/scripts/ci.ts3// Run at the end of a work session to verify everything is solid.4// Usage: bun run scripts/ci.ts56import { spawnSync } from 'bun';7import { existsSync } from 'node:fs';8import { resolveBin } from './release-lib';9// validateGateResult runs inside the JSON-report browser gate wrapper; chrome-release is mandatory.1011const RESET  = '\x1b[0m';12const GREEN  = '\x1b[32m';13const RED    = '\x1b[31m';14const BOLD   = '\x1b[1m';15const DIM    = '\x1b[2m';16// PB-26-210: the release browser gate is mandatory in CI and opt-in for17// local pre-release verification. When enabled, both Chrome and Firefox18// browser tests MUST run and pass — they do not silently skip.19const RUN_BROWSER_GATE = process.env.CI === 'true' || process.env.PEACH_RELEASE_BROWSER_GATE === '1';20const RUN_FIREFOX_SENDER_ONLY = process.env.PEACH_E2E_FIREFOX === '1' && !RUN_BROWSER_GATE;2122interface Step {23  name: string;24  cmd: string[] | (() => string[]);25  cwd?: string;26}2728const steps: Step[] = [29  {30    // LV-6: standardize on bun + a frozen lockfile. --frozen-lockfile exits31    // non-zero if bun.lock is stale relative to package.json, so a forgotten32    // lockfile update fails CI instead of drifting silently.33    name: 'Lockfile freshness',34    cmd: ['bun', 'install', '--frozen-lockfile'],35  },36  {37    name: 'TypeScript',38    cmd: () => [resolveBin('tsc'), '--noEmit'],39  },40  {41    name: 'Lint',42    cmd: ['npm', 'run', 'lint'],43  },44  {45    name: 'Tests',46    cmd: ['npm', 'test', '--', '--reporter=verbose'],47  },48  {49    // PB-26-003: rebuild the Noise WASM from crypto-core and fail if the50    // committed blob drifts — "reviewed source → shipped binary" enforced.51    name: 'Noise WASM provenance',52    cmd: ['node', 'scripts/check-noise-core.mjs'],53  },54  {55    name: 'Chrome build',56    cmd: ['npm', 'run', 'build:chrome'],57  },58  {59    name: 'Firefox build',60    cmd: ['npm', 'run', 'build:firefox'],61  },62  ...(RUN_FIREFOX_SENDER_ONLY63    ? [{64        name: 'Firefox sender context',65        cmd: ['npm', 'run', 'test:firefox:sender'],66      }]67    : []),68  // PB-26-210: one mandatory wrapper owns the Chrome security sentinels and69  // both real-Firefox harnesses, so no browser can be double-run or mislabeled.70  ...(RUN_BROWSER_GATE71    ? [{72        name: 'Release browser gate',73        cmd: ['bun', 'run', 'scripts/run-release-browser-gate.ts'],74      }]75    : []),76];7778function run(step: Step): { ok: boolean; output: string; duration: number } {79  const start = Date.now();80  const cmd = typeof step.cmd === 'function' ? step.cmd() : step.cmd;81  const result = spawnSync(cmd, {82    cwd: step.cwd ?? process.cwd(),83    env: process.env,84    stdout: 'pipe',85    stderr: 'pipe',86  });87  const duration = Date.now() - start;88  const ok = result.exitCode === 0;89  const output = [90    result.stdout?.toString().trim(),91    result.stderr?.toString().trim(),92  ].filter(Boolean).join('\n');9394  return { ok, output, duration };95}9697function formatDuration(ms: number): string {98  if (ms < 1000) return `${ms}ms`;99  return `${(ms / 1000).toFixed(1)}s`;100}101102function printStep(name: string, ok: boolean, duration: number) {103  const icon = ok ? `${GREEN}✓${RESET}` : `${RED}✗${RESET}`;104  const time = `${DIM}${formatDuration(duration)}${RESET}`;105  console.log(`  ${icon}  ${name.padEnd(20)} ${time}`);106}107108async function main() {109  console.log();110  console.log(`${BOLD}peach-browser CI${RESET}`);111  console.log(`${DIM}${new Date().toLocaleTimeString()}${RESET}`);112  console.log();113114  let failed = false;115  const failures: { name: string; output: string }[] = [];116117  // PB-26-210: report browser executables/versions before the browser gate118  // so release hosts can verify their runtime dependencies.119  if (RUN_BROWSER_GATE) {120    console.log(`${DIM}Browser gate enabled — reporting runtime versions:${RESET}`);121    const chromedriver = spawnSync({122      cmd: [resolveBin('playwright'), '--version'],123      stdout: 'pipe',124      stderr: 'pipe',125    });126    if (chromedriver.stdout) console.log(`${DIM}  Playwright: ${chromedriver.stdout.toString().trim()}${RESET}`);127    if (process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH) {128      console.log(`${DIM}  Chrome: ${process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH}${RESET}`);129    }130    console.log();131  }132133  for (const step of steps) {134    process.stdout.write(`  ${DIM}…${RESET}  ${step.name.padEnd(20)}\r`);135    const result = run(step);136    printStep(step.name, result.ok, result.duration);137138    if (!result.ok) {139      failed = true;140      failures.push({ name: step.name, output: result.output });141      break; // fail fast142    }143  }144145  console.log();146147  if (!failed) {148    console.log(`${GREEN}${BOLD}All checks passed.${RESET}`);149    console.log();150151    // Show build artifact sizes152    const chromeManifest = 'dist-chrome/manifest.json';153    if (existsSync(chromeManifest)) {154      const manifest = JSON.parse(await Bun.file(chromeManifest).text());155      console.log(`${DIM}Version: ${manifest.version}${RESET}`);156    }157  } else {158    console.log(`${RED}${BOLD}Failed: ${failures[0].name}${RESET}`);159    console.log();160161    // Print the relevant failure output162    const output = failures[0].output;163    if (output) {164      // Show last 30 lines of output — most errors are at the end165      const lines = output.split('\n');166      const relevant = lines.slice(-30).join('\n');167      console.log(`${DIM}${relevant}${RESET}`);168    }169170    console.log();171    process.exit(1);172  }173}174175main();