ci.ts

5.7 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 YELLOW = '\x1b[33m';15const BOLD   = '\x1b[1m';16const DIM    = '\x1b[2m';17const RUN_FIREFOX_BROWSER_CHECKS = process.env.CI === 'true' || process.env.PEACH_E2E_FIREFOX === '1';18// PB-26-210: the release browser gate is mandatory in CI and opt-in for19// local pre-release verification. When enabled, both Chrome and Firefox20// browser tests MUST run and pass — they do not silently skip.21const RUN_BROWSER_GATE = process.env.CI === 'true' || process.env.PEACH_RELEASE_BROWSER_GATE === '1';2223interface Step {24  name: string;25  cmd: string[] | (() => string[]);26  cwd?: string;27}2829const steps: Step[] = [30  {31    // LV-6: standardize on bun + a frozen lockfile. --frozen-lockfile exits32    // non-zero if bun.lock is stale relative to package.json, so a forgotten33    // lockfile update fails CI instead of drifting silently.34    name: 'Lockfile freshness',35    cmd: ['bun', 'install', '--frozen-lockfile'],36  },37  {38    name: 'TypeScript',39    cmd: () => [resolveBin('tsc'), '--noEmit'],40  },41  {42    name: 'Lint',43    cmd: ['npm', 'run', 'lint'],44  },45  {46    name: 'Tests',47    cmd: ['npm', 'test', '--', '--reporter=verbose'],48  },49  {50    // PB-26-003: rebuild the Noise WASM from crypto-core and fail if the51    // committed blob drifts — "reviewed source → shipped binary" enforced.52    name: 'Noise WASM provenance',53    cmd: ['node', 'scripts/check-noise-core.mjs'],54  },55  {56    name: 'Chrome build',57    cmd: ['npm', 'run', 'build:chrome'],58  },59  {60    name: 'Firefox build',61    cmd: ['npm', 'run', 'build:firefox'],62  },63  ...(RUN_FIREFOX_BROWSER_CHECKS64    ? [{65        name: 'Firefox sender context',66        cmd: ['npm', 'run', 'test:firefox:sender'],67      }]68    : []),69  // PB-26-210: mandatory release browser gate.70  // The wrapper runs Chrome and Firefox sentinels for PB-26-201/203.71  ...(RUN_BROWSER_GATE72    ? [{73        name: 'Chrome and Firefox security sentinel gate',74        cmd: ['bun', 'run', 'scripts/run-release-browser-gate.ts'],75      }]76    : []),77  // Firefox passkey matrix covers PB-26-209 (exportFunction privileged bridge).78  // If Firefox/Marionette cannot launch, the script exits non-zero — the gate79  // fails rather than silently skipping.80  ...(RUN_BROWSER_GATE81    ? [{82        name: 'Firefox release browser gate',83        cmd: ['node', 'scripts/test-firefox-passkeys.mjs'],84      }]85    : []),86];8788function run(step: Step): { ok: boolean; output: string; duration: number } {89  const start = Date.now();90  const cmd = typeof step.cmd === 'function' ? step.cmd() : step.cmd;91  const result = spawnSync(cmd, {92    cwd: step.cwd ?? process.cwd(),93    env: process.env,94    stdout: 'pipe',95    stderr: 'pipe',96  });97  const duration = Date.now() - start;98  const ok = result.exitCode === 0;99  const output = [100    result.stdout?.toString().trim(),101    result.stderr?.toString().trim(),102  ].filter(Boolean).join('\n');103104  return { ok, output, duration };105}106107function formatDuration(ms: number): string {108  if (ms < 1000) return `${ms}ms`;109  return `${(ms / 1000).toFixed(1)}s`;110}111112function printStep(name: string, ok: boolean, duration: number) {113  const icon = ok ? `${GREEN}✓${RESET}` : `${RED}✗${RESET}`;114  const time = `${DIM}${formatDuration(duration)}${RESET}`;115  console.log(`  ${icon}  ${name.padEnd(20)} ${time}`);116}117118async function main() {119  console.log();120  console.log(`${BOLD}peach-browser CI${RESET}`);121  console.log(`${DIM}${new Date().toLocaleTimeString()}${RESET}`);122  console.log();123124  let failed = false;125  const failures: { name: string; output: string }[] = [];126127  // PB-26-210: report browser executables/versions before the browser gate128  // so release hosts can verify their runtime dependencies.129  if (RUN_BROWSER_GATE) {130    console.log(`${DIM}Browser gate enabled — reporting runtime versions:${RESET}`);131    const chromedriver = spawnSync({132      cmd: [resolveBin('playwright'), '--version'],133      stdout: 'pipe',134      stderr: 'pipe',135    });136    if (chromedriver.stdout) console.log(`${DIM}  Playwright: ${chromedriver.stdout.toString().trim()}${RESET}`);137    if (process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH) {138      console.log(`${DIM}  Chrome: ${process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH}${RESET}`);139    }140    console.log();141  }142143  for (const step of steps) {144    process.stdout.write(`  ${DIM}…${RESET}  ${step.name.padEnd(20)}\r`);145    const result = run(step);146    printStep(step.name, result.ok, result.duration);147148    if (!result.ok) {149      failed = true;150      failures.push({ name: step.name, output: result.output });151      break; // fail fast152    }153  }154155  console.log();156157  if (!failed) {158    console.log(`${GREEN}${BOLD}All checks passed.${RESET}`);159    console.log();160161    // Show build artifact sizes162    const chromeManifest = 'dist-chrome/manifest.json';163    if (existsSync(chromeManifest)) {164      const manifest = JSON.parse(await Bun.file(chromeManifest).text());165      console.log(`${DIM}Version: ${manifest.version}${RESET}`);166    }167  } else {168    console.log(`${RED}${BOLD}Failed: ${failures[0].name}${RESET}`);169    console.log();170171    // Print the relevant failure output172    const output = failures[0].output;173    if (output) {174      // Show last 30 lines of output — most errors are at the end175      const lines = output.split('\n');176      const relevant = lines.slice(-30).join('\n');177      console.log(`${DIM}${relevant}${RESET}`);178    }179180    console.log();181    process.exit(1);182  }183}184185main();