# CI for the History simulator. Mirrors the local verification list in AGENTS.md # (typecheck → lint → unit → build) and then runs the deterministic Playwright e2e # suite (LLM_STUB=1) against the app's OWN dev webServer (npm run dev) on the runner. # No dind / docker-compose / static IP / NO_PROXY needed: history's e2e targets the dev # webServer, not a Docker image (unlike flight/orb's image-based e2e). CI never deploys: # production is updated manually on the host after a tag. name: CI on: push: branches: [master] pull_request: branches: [master] workflow_dispatch: jobs: ci: name: build + unit + e2e runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Node uses: actions/setup-node@v4 with: node-version: 22 # Job-level dep cache via the runner's built-in cache server. continue-on-error: # an unreachable/broken cache server must never break the run (sibling convention). # Caches node_modules and npm's tarball store ($HOME/.npm); npm ci wipes # node_modules, so the $HOME/.npm store is what actually makes a cache hit fast. - name: Restore deps cache uses: actions/cache@v4 continue-on-error: true with: path: | node_modules $HOME/.npm key: ${{ runner.os }}-deps-npm-${{ hashFiles('package-lock.json') }} # Retried: the runner's outbound network/DNS is flaky (transient EAI_AGAIN seen in CI). # npm ci is fully reproducible from the committed package-lock.json. - name: Install dependencies run: | set -u ok=0 for i in 1 2 3; do if npm ci; then ok=1; break; fi echo "::warning::dependency install attempt $i/3 failed (likely transient network) — retrying in 5s" >&2 sleep 5 done [ "$ok" = "1" ] || { echo "::error::dependency install failed after 3 attempts" >&2; exit 1; } - name: Typecheck run: npm run typecheck - name: Lint run: npm run lint # Deterministic: the LLM client uses the stub transport (LLM_STUB=1), so no model # or network is needed. vitest auto-appends its summary to $GITHUB_STEP_SUMMARY. - name: Unit tests run: npm test # Vite client build → dist/ + tsup server bundle → server/dist/. Verifies the # production build path (docker build does the same thing inside the image). - name: Build run: npm run build # Browsers run locally in CI (no remote browser server, no dind). Binaries # (~187 MiB) are cached across runs — the runner's uplink is flaky and # cdn.playwright.dev downloads time out mid-transfer; once seeded, installs skip. - name: Restore browser cache uses: actions/cache@v4 continue-on-error: true with: path: $HOME/.cache/ms-playwright key: ${{ runner.os }}-pw-browsers-${{ hashFiles('package.json') }} # Retried: cdn.playwright.dev is flaky from this runner — Playwright's internal # retry window is often not enough, but runs get through with a longer one. # Progress accumulates across attempts: playwright skips already-installed browsers. - name: Install browsers (local) run: | set -u ok=0 for i in 1 2 3 4 5 6; do if npx playwright install --with-deps chromium; then ok=1; break; fi echo "::warning::browser install attempt $i/6 failed (flaky cdn.playwright.dev) — retrying in 5s" >&2 sleep 5 done [ "$ok" = "1" ] || { echo "::error::browser install failed after 6 attempts" >&2; exit 1; } # E2E: fully deterministic. The Playwright config boots its own dev webServer # (npm run dev) with LLM_STUB=1 and DATA_DIR=data/e2e. VITE_PORT / HISTORY_API_PORT # are set to free ports so that webServer won't collide with anything else on the # runner (history's normal dev ports are 5173/4321). The config emits an HTML report # (playwright-report/) and a JSON report (test-results/e2e.json) used below. - name: E2E env: VITE_PORT: "5199" HISTORY_API_PORT: "4399" run: npm run e2e # Short machine-readable summary appended to the PR/job. Runs even on failure # (always()) and never breaks the job (|| true). Parses Playwright's JSON report: # stats.expected = passed, stats.unexpected = failed, stats.skipped = skipped. - name: E2E results summary if: always() run: | { echo "## E2E results" echo if [ -f test-results/e2e.json ]; then node -e ' const fs = require("fs"); const r = JSON.parse(fs.readFileSync("test-results/e2e.json", "utf8")); const s = r.stats || {}; console.log(`- **Passed:** ${s.expected ?? 0}`); console.log(`- **Failed:** ${s.unexpected ?? 0}` + (s.flaky ? ` (flaky: ${s.flaky})` : "")); console.log(`- **Skipped:** ${s.skipped ?? 0}`); console.log(`- **Duration:** ${((s.duration ?? 0) / 1000).toFixed(1)}s`); ' || echo "- could not parse test-results/e2e.json" else echo "- no test-results/e2e.json produced (e2e did not run)" fi } >> "$GITHUB_STEP_SUMMARY" || true # On failure, keep the HTML report + raw results for debugging. - name: Upload Playwright report if: failure() uses: actions/upload-artifact@v4 continue-on-error: true with: name: playwright-report path: | playwright-report/ test-results/ retention-days: 7