ci.ts

5.5 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 'fs';89const RESET  = '\x1b[0m';10const GREEN  = '\x1b[32m';11const RED    = '\x1b[31m';12const YELLOW = '\x1b[33m';13const BOLD   = '\x1b[1m';14const DIM    = '\x1b[2m';15const RUN_FIREFOX_BROWSER_CHECKS = process.env.CI === 'true' || process.env.PEACH_E2E_FIREFOX === '1';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';2021interface Step {22  name: string;23  cmd: string[];24  cwd?: string;25}2627const steps: Step[] = [28  {29    // LV-6: standardize on bun + a frozen lockfile. --frozen-lockfile exits30    // non-zero if bun.lock is stale relative to package.json, so a forgotten31    // lockfile update fails CI instead of drifting silently.32    name: 'Lockfile freshness',33    cmd: ['bun', 'install', '--frozen-lockfile'],34  },35  {36    name: 'TypeScript',37    cmd: ['npx', 'tsc', '--noEmit'],38  },39  {40    name: 'Lint',41    cmd: ['npm', 'run', 'lint'],42  },43  {44    name: 'Tests',45    cmd: ['npm', 'test', '--', '--reporter=verbose'],46  },47  {48    // PB-26-003: rebuild the Noise WASM from crypto-core and fail if the49    // committed blob drifts — "reviewed source → shipped binary" enforced.50    name: 'Noise WASM provenance',51    cmd: ['node', 'scripts/check-noise-core.mjs'],52  },53  {54    name: 'Chrome build',55    cmd: ['npm', 'run', 'build:chrome'],56  },57  {58    name: 'Firefox build',59    cmd: ['npm', 'run', 'build:firefox'],60  },61  ...(RUN_FIREFOX_BROWSER_CHECKS62    ? [{63        name: 'Firefox sender context',64        cmd: ['npm', 'run', 'test:firefox:sender'],65      }]66    : []),67  // PB-26-210: mandatory release browser gate.68  // Chrome sentinels cover PB-26-201 (authorization) and PB-26-203 (storage isolation).69  ...(RUN_BROWSER_GATE70    ? [{71        name: 'Chrome release browser gate',72        cmd: ['npx', 'playwright', 'test', '--project=chrome-release'],73      }]74    : []),75  // Firefox passkey matrix covers PB-26-209 (exportFunction privileged bridge).76  // If Firefox/Marionette cannot launch, the script exits non-zero — the gate77  // fails rather than silently skipping.78  ...(RUN_BROWSER_GATE79    ? [{80        name: 'Firefox release browser gate',81        cmd: ['node', 'scripts/test-firefox-passkeys.mjs'],82      }]83    : []),84];8586function run(step: Step): { ok: boolean; output: string; duration: number } {87  const start = Date.now();88  const result = spawnSync(step.cmd, {89    cwd: step.cwd ?? process.cwd(),90    env: process.env,91    stdout: 'pipe',92    stderr: 'pipe',93  });94  const duration = Date.now() - start;95  const ok = result.exitCode === 0;96  const output = [97    result.stdout?.toString().trim(),98    result.stderr?.toString().trim(),99  ].filter(Boolean).join('\n');100101  return { ok, output, duration };102}103104function formatDuration(ms: number): string {105  if (ms < 1000) return `${ms}ms`;106  return `${(ms / 1000).toFixed(1)}s`;107}108109function printStep(name: string, ok: boolean, duration: number) {110  const icon = ok ? `${GREEN}✓${RESET}` : `${RED}✗${RESET}`;111  const time = `${DIM}${formatDuration(duration)}${RESET}`;112  console.log(`  ${icon}  ${name.padEnd(20)} ${time}`);113}114115async function main() {116  console.log();117  console.log(`${BOLD}peach-browser CI${RESET}`);118  console.log(`${DIM}${new Date().toLocaleTimeString()}${RESET}`);119  console.log();120121  let failed = false;122  const failures: { name: string; output: string }[] = [];123124  // PB-26-210: report browser executables/versions before the browser gate125  // so release hosts can verify their runtime dependencies.126  if (RUN_BROWSER_GATE) {127    console.log(`${DIM}Browser gate enabled — reporting runtime versions:${RESET}`);128    const chromedriver = spawnSync({129      cmd: ['npx', 'playwright', '--version'],130      stdout: 'pipe',131      stderr: 'pipe',132    });133    if (chromedriver.stdout) console.log(`${DIM}  Playwright: ${chromedriver.stdout.toString().trim()}${RESET}`);134    if (process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH) {135      console.log(`${DIM}  Chrome: ${process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH}${RESET}`);136    }137    console.log();138  }139140  for (const step of steps) {141    process.stdout.write(`  ${DIM}…${RESET}  ${step.name.padEnd(20)}\r`);142    const result = run(step);143    printStep(step.name, result.ok, result.duration);144145    if (!result.ok) {146      failed = true;147      failures.push({ name: step.name, output: result.output });148      break; // fail fast149    }150  }151152  console.log();153154  if (!failed) {155    console.log(`${GREEN}${BOLD}All checks passed.${RESET}`);156    console.log();157158    // Show build artifact sizes159    const chromeManifest = 'dist-chrome/manifest.json';160    if (existsSync(chromeManifest)) {161      const manifest = JSON.parse(await Bun.file(chromeManifest).text());162      console.log(`${DIM}Version: ${manifest.version}${RESET}`);163    }164  } else {165    console.log(`${RED}${BOLD}Failed: ${failures[0].name}${RESET}`);166    console.log();167168    // Print the relevant failure output169    const output = failures[0].output;170    if (output) {171      // Show last 30 lines of output — most errors are at the end172      const lines = output.split('\n');173      const relevant = lines.slice(-30).join('\n');174      console.log(`${DIM}${relevant}${RESET}`);175    }176177    console.log();178    process.exit(1);179  }180}181182main();