ci.ts

4.1 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 BOLD   = '\x1b[1m';14const DIM    = '\x1b[2m';15const RUN_FIREFOX_SENDER_ONLY = process.env.PEACH_E2E_FIREFOX === '1';1617interface Step {18  name: string;19  cmd: string[] | (() => string[]);20  cwd?: string;21}2223const steps: Step[] = [24  {25    // LV-6: standardize on bun + a frozen lockfile. --frozen-lockfile exits26    // non-zero if bun.lock is stale relative to package.json, so a forgotten27    // lockfile update fails CI instead of drifting silently.28    name: 'Lockfile freshness',29    cmd: ['bun', 'install', '--frozen-lockfile'],30  },31  {32    name: 'TypeScript',33    cmd: () => [resolveBin('tsc'), '--noEmit'],34  },35  {36    name: 'Lint',37    cmd: ['npm', 'run', 'lint'],38  },39  {40    // PB-26-003: rebuild the Noise WASM from crypto-core and fail if the41    // committed blob drifts — "reviewed source → shipped binary" enforced.42    name: 'Noise WASM provenance',43    cmd: ['node', 'scripts/check-noise-core.mjs'],44  },45  {46    name: 'Chrome build',47    cmd: ['npm', 'run', 'build:chrome'],48  },49  {50    name: 'Firefox build',51    cmd: ['npm', 'run', 'build:firefox'],52  },53  {54    // Runs after both builds: the browser gate exercises the shipped targets.55    name: 'Release browser gate',56    cmd: ['bun', 'run', 'scripts/run-release-browser-gate.ts'],57  },58  {59    name: 'Tests',60    cmd: ['npm', 'test', '--', '--reporter=verbose'],61  },62  ...(RUN_FIREFOX_SENDER_ONLY63    ? [{64        name: 'Firefox sender context',65        cmd: ['npm', 'run', 'test:firefox:sender'],66      }]67    : []),68];6970function run(step: Step): { ok: boolean; output: string; duration: number } {71  const start = Date.now();72  const cmd = typeof step.cmd === 'function' ? step.cmd() : step.cmd;73  const result = spawnSync(cmd, {74    cwd: step.cwd ?? process.cwd(),75    env: process.env,76    stdout: 'pipe',77    stderr: 'pipe',78  });79  const duration = Date.now() - start;80  const ok = result.exitCode === 0;81  const output = [82    result.stdout?.toString().trim(),83    result.stderr?.toString().trim(),84  ].filter(Boolean).join('\n');8586  return { ok, output, duration };87}8889function formatDuration(ms: number): string {90  if (ms < 1000) return `${ms}ms`;91  return `${(ms / 1000).toFixed(1)}s`;92}9394function printStep(name: string, ok: boolean, duration: number) {95  const icon = ok ? `${GREEN}✓${RESET}` : `${RED}✗${RESET}`;96  const time = `${DIM}${formatDuration(duration)}${RESET}`;97  console.log(`  ${icon}  ${name.padEnd(20)} ${time}`);98}99100async function main() {101  console.log();102  console.log(`${BOLD}peach-browser CI${RESET}`);103  console.log(`${DIM}${new Date().toLocaleTimeString()}${RESET}`);104  console.log();105106  let failed = false;107  const failures: { name: string; output: string }[] = [];108109  for (const step of steps) {110    process.stdout.write(`  ${DIM}…${RESET}  ${step.name.padEnd(20)}\r`);111    const result = run(step);112    printStep(step.name, result.ok, result.duration);113114    if (!result.ok) {115      failed = true;116      failures.push({ name: step.name, output: result.output });117      break; // fail fast118    }119  }120121  console.log();122123  if (!failed) {124    console.log(`${GREEN}${BOLD}All checks passed.${RESET}`);125    console.log();126127    // Show build artifact sizes128    const chromeManifest = 'dist-chrome/manifest.json';129    if (existsSync(chromeManifest)) {130      const manifest = JSON.parse(await Bun.file(chromeManifest).text());131      console.log(`${DIM}Version: ${manifest.version}${RESET}`);132    }133  } else {134    console.log(`${RED}${BOLD}Failed: ${failures[0].name}${RESET}`);135    console.log();136137    // Print the relevant failure output138    const output = failures[0].output;139    if (output) {140      // Show last 30 lines of output — most errors are at the end141      const lines = output.split('\n');142      const relevant = lines.slice(-30).join('\n');143      console.log(`${DIM}${relevant}${RESET}`);144    }145146    console.log();147    process.exit(1);148  }149}150151main();