ci.ts

5.6 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    // PB-26-003: rebuild the Noise WASM from crypto-core and fail if the46    // committed blob drifts — "reviewed source → shipped binary" enforced.47    name: 'Noise WASM provenance',48    cmd: ['node', 'scripts/check-noise-core.mjs'],49  },50  {51    name: 'Chrome build',52    cmd: ['npm', 'run', 'build:chrome'],53  },54  {55    name: 'Firefox build',56    cmd: ['npm', 'run', 'build:firefox'],57  },58  {59    // Runs after both builds: the web-accessible-resource assertions read the60    // shipped dist-chrome/ and dist-firefox/ manifests, so a Tests step ahead61    // of the builds would skip them on a clean checkout.62    name: 'Tests',63    cmd: ['npm', 'test', '--', '--reporter=verbose'],64  },65  ...(RUN_FIREFOX_SENDER_ONLY66    ? [{67        name: 'Firefox sender context',68        cmd: ['npm', 'run', 'test:firefox:sender'],69      }]70    : []),71  // PB-26-210: one mandatory wrapper owns the Chrome security sentinels and72  // both real-Firefox harnesses, so no browser can be double-run or mislabeled.73  ...(RUN_BROWSER_GATE74    ? [{75        name: 'Release browser gate',76        cmd: ['bun', 'run', 'scripts/run-release-browser-gate.ts'],77      }]78    : []),79];8081function run(step: Step): { ok: boolean; output: string; duration: number } {82  const start = Date.now();83  const cmd = typeof step.cmd === 'function' ? step.cmd() : step.cmd;84  const result = spawnSync(cmd, {85    cwd: step.cwd ?? process.cwd(),86    env: process.env,87    stdout: 'pipe',88    stderr: 'pipe',89  });90  const duration = Date.now() - start;91  const ok = result.exitCode === 0;92  const output = [93    result.stdout?.toString().trim(),94    result.stderr?.toString().trim(),95  ].filter(Boolean).join('\n');9697  return { ok, output, duration };98}99100function formatDuration(ms: number): string {101  if (ms < 1000) return `${ms}ms`;102  return `${(ms / 1000).toFixed(1)}s`;103}104105function printStep(name: string, ok: boolean, duration: number) {106  const icon = ok ? `${GREEN}✓${RESET}` : `${RED}✗${RESET}`;107  const time = `${DIM}${formatDuration(duration)}${RESET}`;108  console.log(`  ${icon}  ${name.padEnd(20)} ${time}`);109}110111async function main() {112  console.log();113  console.log(`${BOLD}peach-browser CI${RESET}`);114  console.log(`${DIM}${new Date().toLocaleTimeString()}${RESET}`);115  console.log();116117  let failed = false;118  const failures: { name: string; output: string }[] = [];119120  // PB-26-210: report browser executables/versions before the browser gate121  // so release hosts can verify their runtime dependencies.122  if (RUN_BROWSER_GATE) {123    console.log(`${DIM}Browser gate enabled — reporting runtime versions:${RESET}`);124    const chromedriver = spawnSync({125      cmd: [resolveBin('playwright'), '--version'],126      stdout: 'pipe',127      stderr: 'pipe',128    });129    if (chromedriver.stdout) console.log(`${DIM}  Playwright: ${chromedriver.stdout.toString().trim()}${RESET}`);130    if (process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH) {131      console.log(`${DIM}  Chrome: ${process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH}${RESET}`);132    }133    console.log();134  }135136  for (const step of steps) {137    process.stdout.write(`  ${DIM}…${RESET}  ${step.name.padEnd(20)}\r`);138    const result = run(step);139    printStep(step.name, result.ok, result.duration);140141    if (!result.ok) {142      failed = true;143      failures.push({ name: step.name, output: result.output });144      break; // fail fast145    }146  }147148  console.log();149150  if (!failed) {151    console.log(`${GREEN}${BOLD}All checks passed.${RESET}`);152    console.log();153154    // Show build artifact sizes155    const chromeManifest = 'dist-chrome/manifest.json';156    if (existsSync(chromeManifest)) {157      const manifest = JSON.parse(await Bun.file(chromeManifest).text());158      console.log(`${DIM}Version: ${manifest.version}${RESET}`);159    }160  } else {161    console.log(`${RED}${BOLD}Failed: ${failures[0].name}${RESET}`);162    console.log();163164    // Print the relevant failure output165    const output = failures[0].output;166    if (output) {167      // Show last 30 lines of output — most errors are at the end168      const lines = output.split('\n');169      const relevant = lines.slice(-30).join('\n');170      console.log(`${DIM}${relevant}${RESET}`);171    }172173    console.log();174    process.exit(1);175  }176}177178main();