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';910const RESET  = '\x1b[0m';11const GREEN  = '\x1b[32m';12const RED    = '\x1b[31m';13const YELLOW = '\x1b[33m';14const BOLD   = '\x1b[1m';15const DIM    = '\x1b[2m';16const RUN_FIREFOX_BROWSER_CHECKS = process.env.CI === 'true' || process.env.PEACH_E2E_FIREFOX === '1';17// PB-26-210: the release browser gate is mandatory in CI and opt-in for18// local pre-release verification. When enabled, both Chrome and Firefox19// browser tests MUST run and pass — they do not silently skip.20const RUN_BROWSER_GATE = process.env.CI === 'true' || process.env.PEACH_RELEASE_BROWSER_GATE === '1';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_BROWSER_CHECKS63    ? [{64        name: 'Firefox sender context',65        cmd: ['npm', 'run', 'test:firefox:sender'],66      }]67    : []),68  // PB-26-210: mandatory release browser gate.69  // Chrome sentinels cover PB-26-201 (authorization) and PB-26-203 (storage isolation).70  ...(RUN_BROWSER_GATE71    ? [{72        name: 'Chrome release browser gate',73        cmd: () => [resolveBin('playwright'), 'test', '--project=chrome-release'],74      }]75    : []),76  // Firefox passkey matrix covers PB-26-209 (exportFunction privileged bridge).77  // If Firefox/Marionette cannot launch, the script exits non-zero — the gate78  // fails rather than silently skipping.79  ...(RUN_BROWSER_GATE80    ? [{81        name: 'Firefox release browser gate',82        cmd: ['node', 'scripts/test-firefox-passkeys.mjs'],83      }]84    : []),85];8687function run(step: Step): { ok: boolean; output: string; duration: number } {88  const start = Date.now();89  const cmd = typeof step.cmd === 'function' ? step.cmd() : step.cmd;90  const result = spawnSync(cmd, {91    cwd: step.cwd ?? process.cwd(),92    env: process.env,93    stdout: 'pipe',94    stderr: 'pipe',95  });96  const duration = Date.now() - start;97  const ok = result.exitCode === 0;98  const output = [99    result.stdout?.toString().trim(),100    result.stderr?.toString().trim(),101  ].filter(Boolean).join('\n');102103  return { ok, output, duration };104}105106function formatDuration(ms: number): string {107  if (ms < 1000) return `${ms}ms`;108  return `${(ms / 1000).toFixed(1)}s`;109}110111function printStep(name: string, ok: boolean, duration: number) {112  const icon = ok ? `${GREEN}✓${RESET}` : `${RED}✗${RESET}`;113  const time = `${DIM}${formatDuration(duration)}${RESET}`;114  console.log(`  ${icon}  ${name.padEnd(20)} ${time}`);115}116117async function main() {118  console.log();119  console.log(`${BOLD}peach-browser CI${RESET}`);120  console.log(`${DIM}${new Date().toLocaleTimeString()}${RESET}`);121  console.log();122123  let failed = false;124  const failures: { name: string; output: string }[] = [];125126  // PB-26-210: report browser executables/versions before the browser gate127  // so release hosts can verify their runtime dependencies.128  if (RUN_BROWSER_GATE) {129    console.log(`${DIM}Browser gate enabled — reporting runtime versions:${RESET}`);130    const chromedriver = spawnSync({131      cmd: [resolveBin('playwright'), '--version'],132      stdout: 'pipe',133      stderr: 'pipe',134    });135    if (chromedriver.stdout) console.log(`${DIM}  Playwright: ${chromedriver.stdout.toString().trim()}${RESET}`);136    if (process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH) {137      console.log(`${DIM}  Chrome: ${process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH}${RESET}`);138    }139    console.log();140  }141142  for (const step of steps) {143    process.stdout.write(`  ${DIM}…${RESET}  ${step.name.padEnd(20)}\r`);144    const result = run(step);145    printStep(step.name, result.ok, result.duration);146147    if (!result.ok) {148      failed = true;149      failures.push({ name: step.name, output: result.output });150      break; // fail fast151    }152  }153154  console.log();155156  if (!failed) {157    console.log(`${GREEN}${BOLD}All checks passed.${RESET}`);158    console.log();159160    // Show build artifact sizes161    const chromeManifest = 'dist-chrome/manifest.json';162    if (existsSync(chromeManifest)) {163      const manifest = JSON.parse(await Bun.file(chromeManifest).text());164      console.log(`${DIM}Version: ${manifest.version}${RESET}`);165    }166  } else {167    console.log(`${RED}${BOLD}Failed: ${failures[0].name}${RESET}`);168    console.log();169170    // Print the relevant failure output171    const output = failures[0].output;172    if (output) {173      // Show last 30 lines of output — most errors are at the end174      const lines = output.split('\n');175      const relevant = lines.slice(-30).join('\n');176      console.log(`${DIM}${relevant}${RESET}`);177    }178179    console.log();180    process.exit(1);181  }182}183184main();