From 2b4d90edaa143c8c39451c0aee487632ad2dd83c Mon Sep 17 00:00:00 2001 From: Eric Date: Thu, 28 May 2026 23:55:32 -0700 Subject: [PATCH] [eric] ci: parallelize + cache the e2e gate to cut push wall-clock under ~10 min - split the single windows e2e job into parallel jobs: gate (pure-node selftests, no build), verify (build + verify-all), playwright (build + renderer suite). verify and playwright run concurrently so wall-clock is max(verify, playwright), not their sum - build the UNPACKED app on the gate path (electron-builder --dir via a new -DirOnly switch in build-app-win.ps1), skipping the ~2min NSIS LZMA compression. verify-all drives win-unpacked\OpenSwarm.exe; afterPack + locale paks still run in the pack phase, and verify-update-feed skips cleanly when no installer feed is present - cache the heavy build inputs (bundled Python env, uv, MCP bundles) + npm across runs, keyed on their source manifests; the build script already skips any input already on disk, so warm builds are fast - move the destructive installer cycle to its own job gated off routine pushes (runs on PR-to-main / dispatch; release-windows.yml covers v* tags) - apply the same caching to dogfood (shares cache keys, so the two warm each other) --- .github/workflows/dogfood.yml | 25 +++- .github/workflows/e2e.yml | 236 +++++++++++++++++++++------------- scripts/build-app-win.ps1 | 12 +- 3 files changed, 182 insertions(+), 91 deletions(-) diff --git a/.github/workflows/dogfood.yml b/.github/workflows/dogfood.yml index 66f7a125..585855ca 100644 --- a/.github/workflows/dogfood.yml +++ b/.github/workflows/dogfood.yml @@ -49,10 +49,33 @@ jobs: steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 - with: { node-version: '20.18.1' } + with: + node-version: '20.18.1' + cache: npm + cache-dependency-path: | + electron/package-lock.json + frontend/package-lock.json - uses: actions/setup-python@v5 with: { python-version: '3.13' } + # Reuse the heavy build inputs (shares keys with e2e.yml so the two warm + # each other's caches); the build script skips any input already on disk. + - name: Cache bundled Python env + uses: actions/cache@v4 + with: + path: electron/python-env + key: pyenv-win-${{ hashFiles('scripts/build-python-env-win.ps1', 'backend/requirements.txt') }} + - name: Cache uv binaries + uses: actions/cache@v4 + with: + path: backend/uv-bin + key: uvbin-win-${{ hashFiles('scripts/build-app-win.ps1') }} + - name: Cache MCP bundles + uses: actions/cache@v4 + with: + path: backend/mcp-bundles + key: mcpbundles-win-${{ hashFiles('scripts/build-app-win.ps1') }} + - name: Build packaged app (Windows) shell: pwsh run: pwsh -NoProfile -File scripts/build-app-win.ps1 diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index a28ec83c..298e6a47 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -1,38 +1,26 @@ name: E2E (packaged app, Windows) -# Builds the UNSIGNED packaged app on each OS, then runs the FULL deterministic -# gate against the real artifact plus the Playwright smoke. Signing/notarization + -# release upload are handled separately by release-windows.yml / release-macos.yml. +# Fast-feedback packaged-app gate, Windows-only. Split into parallel jobs so the +# push wall-clock stays well under ~10 min: +# gate - cheap pure-node selftests (no build): mutation gate + preflight +# Layers 1-5. Fails the "tests that test the tests" fast. +# verify - build the UNPACKED app (electron-builder --dir, which skips the +# slow ~2min NSIS LZMA compression) and run the deterministic +# verify-all gate against win-unpacked\OpenSwarm.exe. +# playwright - build the unpacked app and run the renderer-level Playwright e2e. +# installer - full NSIS build + destructive install->verify->uninstall. The +# heaviest leg, so it runs only on PR-to-main / dispatch (NOT on +# routine pushes); release-windows.yml covers it on v* tags. +# verify + playwright run concurrently; both reuse cached heavy build inputs +# (bundled Python env, uv, MCP bundles, npm) so warm builds are fast - the build +# script skips any input already on disk. # -# The gate, per leg: -# 1. selftest-gate.js - "test the tests": mutation-checks the gate's own logic -# so a weakened guard fails loudly (runs first, no build needed). -# 2. build the packaged app. -# 3. verify-all.js - boot/paint/serve/provenance + signature state + -# resilience (locked-port, double-launch) + network/auth/9router. Provenance -# goes GREEN here because the build is AT the checked-out commit. -# 4. mcp/selftest.js - proves the Playwright "GUI hand" can launch + drive + -# read the real app. -# 5. playwright npm test - the renderer-level smoke. -# 6. (windows) verify-installer --destructive - the runner is a CLEAN box with no -# prior install, so the real install -> verify -> uninstall cycle is safe here -# (it refuses to run where an install already exists). -# -# Matrix: windows-latest (Win Server 2022) only. The macOS legs were removed - -# GitHub's scarce mac runners left them perpetually queued/starved and surfaced -# mac-only failures we cannot triage from the Windows dev box, so the whole -# matrix read red. Re-add macos-14/macos-13 (matrix edit + workflow_dispatch) -# when a Mac maintainer can own them. GitHub retired windows-2019 and ships no -# Win10 client image, so true Win10 coverage needs the SELF-HOSTED e2e-win10 job -# (activates when a runner labeled [self-hosted, windows, win10] exists and repo -# var WIN10_SELF_HOSTED=true). +# macOS legs were removed (runner starvation + untriageable mac-only failures); +# re-add when a Mac maintainer can own them. Real Win10 coverage still needs the +# SELF-HOSTED e2e-win10 job (repo var WIN10_SELF_HOSTED=true + a +# [self-hosted, windows, win10] runner). on: - # This is the EXPENSIVE leg (packaged-app build + e2e matrix). Gate it so it only - # runs when code that actually affects the built artifact changes - not docs, - # .gitignore, or unrelated CI meta - plus on PRs into main and on demand. The - # cheap gates (hermetic phase-tests, gitleaks) keep running on every push via - # their own workflows, so every push still gets fast signal. push: branches: [eric/lock] paths: &build-paths @@ -41,8 +29,8 @@ on: - 'backend/**' - 'e2e/**' - 'scripts/build-app**' - - 'scripts/fetch-router**' - 'scripts/build-python-env**' + - 'scripts/fetch-router**' - 'scripts/ci/**' - '.github/workflows/e2e.yml' pull_request: @@ -53,37 +41,24 @@ on: permissions: contents: read +env: + CSC_IDENTITY_AUTO_DISCOVERY: 'false' + GOOGLE_OAUTH_CLIENT_ID: 'e2e-placeholder.apps.googleusercontent.com' + GOOGLE_OAUTH_CLIENT_SECRET: 'e2e-placeholder-secret' + jobs: - e2e: - strategy: - fail-fast: false - matrix: - # Windows-only. macOS legs removed: runner starvation + untriageable - # mac-only failures kept the matrix red. Re-add macos-14/macos-13 here - # (with workflow_dispatch) when a Mac maintainer can own them. - os: [windows-latest] - runs-on: ${{ matrix.os }} - timeout-minutes: 90 - env: - CSC_IDENTITY_AUTO_DISCOVERY: 'false' - GOOGLE_OAUTH_CLIENT_ID: 'e2e-placeholder.apps.googleusercontent.com' - GOOGLE_OAUTH_CLIENT_SECRET: 'e2e-placeholder-secret' + # Pure-node, no build: cheap enough to always run and fail fast. + gate: + runs-on: windows-latest + timeout-minutes: 15 steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: '20.18.1' - - uses: actions/setup-python@v5 - with: - python-version: '3.13' - - # Test the tests first: cheap, pure, no build. If a guard stopped - # discriminating good from broken, fail before spending a build. - name: Gate selftest (mutation) shell: bash run: node scripts/ci/selftest-gate.js - - # Preflight Layer 1+2+3+4 (unit, hang fuzz, failure rigs, race) - pure node, ~5s. - name: Preflight selftest (Layers 1+2) shell: bash run: node scripts/ci/selftest-preflight.js @@ -96,77 +71,160 @@ jobs: - name: Pairwise generator selftest (covering-array math) shell: bash run: node scripts/ci/selftest-pairwise.js + - name: Preflight matrix (normal + hostile-env scenarios) + shell: bash + run: | + node scripts/ci/verify-preflight.js + OPENSWARM_TEST_NETWORK=blocked node scripts/ci/verify-preflight.js + OPENSWARM_TEST_APPDATA=readonly node scripts/ci/verify-preflight.js + OPENSWARM_TEST_LANG=de-DE node scripts/ci/verify-preflight.js - # Preflight Layer 5: hostile-env scenarios. Each leg asserts the verdict - # matches the expected envelope for that scenario; network=blocked - # producing 'fail' instead of 'warn' is the false-positive bug class. - - name: Preflight matrix (normal) - shell: bash - run: node scripts/ci/verify-preflight.js - - name: Preflight matrix (network=blocked must -> warn) - shell: bash - env: { OPENSWARM_TEST_NETWORK: blocked } - run: node scripts/ci/verify-preflight.js - - name: Preflight matrix (appdata=readonly must -> fail) - shell: bash - env: { OPENSWARM_TEST_APPDATA: readonly } - run: node scripts/ci/verify-preflight.js - - name: Preflight matrix (lang=de-DE must stay -> ok) - shell: bash - env: { OPENSWARM_TEST_LANG: de-DE } - run: node scripts/ci/verify-preflight.js - - - name: Build packaged app (Windows) + # Build the unpacked app + run the deterministic gate. + verify: + runs-on: windows-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '20.18.1' + cache: npm + cache-dependency-path: | + electron/package-lock.json + frontend/package-lock.json + - uses: actions/setup-python@v5 + with: + python-version: '3.13' + - name: Cache bundled Python env + uses: actions/cache@v4 + with: + path: electron/python-env + key: pyenv-win-${{ hashFiles('scripts/build-python-env-win.ps1', 'backend/requirements.txt') }} + - name: Cache uv binaries + uses: actions/cache@v4 + with: + path: backend/uv-bin + key: uvbin-win-${{ hashFiles('scripts/build-app-win.ps1') }} + - name: Cache MCP bundles + uses: actions/cache@v4 + with: + path: backend/mcp-bundles + key: mcpbundles-win-${{ hashFiles('scripts/build-app-win.ps1') }} + - name: Build packaged app (unpacked, no installer) shell: pwsh - run: pwsh -NoProfile -File scripts/build-app-win.ps1 - + run: pwsh -NoProfile -File scripts/build-app-win.ps1 -DirOnly - name: Deterministic gate (verify-all) shell: bash run: node scripts/ci/verify-all.js + # Build the unpacked app + run the Playwright renderer suite, concurrently with verify. + playwright: + runs-on: windows-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '20.18.1' + cache: npm + cache-dependency-path: | + electron/package-lock.json + frontend/package-lock.json + e2e/package-lock.json + - uses: actions/setup-python@v5 + with: + python-version: '3.13' + - name: Cache bundled Python env + uses: actions/cache@v4 + with: + path: electron/python-env + key: pyenv-win-${{ hashFiles('scripts/build-python-env-win.ps1', 'backend/requirements.txt') }} + - name: Cache uv binaries + uses: actions/cache@v4 + with: + path: backend/uv-bin + key: uvbin-win-${{ hashFiles('scripts/build-app-win.ps1') }} + - name: Cache MCP bundles + uses: actions/cache@v4 + with: + path: backend/mcp-bundles + key: mcpbundles-win-${{ hashFiles('scripts/build-app-win.ps1') }} + - name: Build packaged app (unpacked, no installer) + shell: pwsh + run: pwsh -NoProfile -File scripts/build-app-win.ps1 -DirOnly - name: Install e2e deps shell: bash working-directory: e2e env: PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1' run: npm ci - - name: GUI hand selftest (MCP) shell: bash working-directory: e2e run: node mcp/selftest.js - - name: Run E2E (Playwright) shell: bash working-directory: e2e run: npm test - - - name: Installer cycle (Windows, clean runner) - if: startsWith(matrix.os, 'windows') - shell: pwsh - run: node scripts/ci/verify-installer.js --destructive - - name: Upload E2E results if: always() uses: actions/upload-artifact@v4 with: - name: e2e-results-${{ matrix.os }} + name: e2e-results path: e2e/results.json if-no-files-found: ignore retention-days: 14 - - # Per-test traces: playwright-trace.zip (open via `npx playwright show-trace`), - # events.jsonl (unified timestamped stream), mousepath.jsonl, backend.log.tail. - # Always-upload so a failed run is debuggable without re-running. + # Per-test traces: playwright-trace.zip, events.jsonl, mousepath.jsonl, + # backend.log.tail - always uploaded so a failed run is debuggable. - name: Upload E2E visibility traces if: always() uses: actions/upload-artifact@v4 with: - name: e2e-traces-${{ matrix.os }} + name: e2e-traces path: e2e/traces/ if-no-files-found: ignore retention-days: 14 + # Heaviest leg: full NSIS installer + destructive install->verify->uninstall on + # a clean runner. Skip on routine pushes; run on PRs into main + manual dispatch. + installer: + if: github.event_name != 'push' + runs-on: windows-latest + timeout-minutes: 45 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '20.18.1' + cache: npm + cache-dependency-path: | + electron/package-lock.json + frontend/package-lock.json + - uses: actions/setup-python@v5 + with: + python-version: '3.13' + - name: Cache bundled Python env + uses: actions/cache@v4 + with: + path: electron/python-env + key: pyenv-win-${{ hashFiles('scripts/build-python-env-win.ps1', 'backend/requirements.txt') }} + - name: Cache uv binaries + uses: actions/cache@v4 + with: + path: backend/uv-bin + key: uvbin-win-${{ hashFiles('scripts/build-app-win.ps1') }} + - name: Cache MCP bundles + uses: actions/cache@v4 + with: + path: backend/mcp-bundles + key: mcpbundles-win-${{ hashFiles('scripts/build-app-win.ps1') }} + - name: Build packaged app (full NSIS installer) + shell: pwsh + run: pwsh -NoProfile -File scripts/build-app-win.ps1 + - name: Installer cycle (clean runner) + shell: pwsh + run: node scripts/ci/verify-installer.js --destructive + # Real Windows 10 coverage. Skipped unless a self-hosted Win10 runner exists # and WIN10_SELF_HOSTED=true (repo variable). Mirrors the windows steps above. e2e-win10: diff --git a/scripts/build-app-win.ps1 b/scripts/build-app-win.ps1 index 6fb3f27d..9ffd721e 100644 --- a/scripts/build-app-win.ps1 +++ b/scripts/build-app-win.ps1 @@ -11,6 +11,10 @@ param( [switch]$Sign, [switch]$Publish, + # Fast CI gate path: build only the unpacked win-unpacked\ dir (no NSIS + # installer, no LZMA compression of the ~1GB tree - the slowest packaging + # phase). verify-all + Playwright drive the unpacked OpenSwarm.exe directly. + [switch]$DirOnly, # Phase 7 A/B: build a Squirrel.Windows installer instead of the default # NSIS one, from the SAME staged tree / SAME commit. Opt-in only; NSIS stays # the default and shipped target until Squirrel is proven faster AND its @@ -434,7 +438,13 @@ try { $env:CSC_IDENTITY_AUTO_DISCOVERY = 'false' } - if ($Publish) { + if ($DirOnly) { + # Unpacked-only build for the fast CI gate. afterPack (router node_modules) + # and locale-pak filtering still run during the pack phase, so the produced + # win-unpacked\OpenSwarm.exe is fully functional; only the NSIS installer + + # update feed are skipped (verify-update-feed skips cleanly when absent). + & npx electron-builder --win --x64 --dir @TargetOverride --publish never + } elseif ($Publish) { # Safety check: warn if the matching Mac release isn't on GitHub yet. # Mac and Windows publishes don't conflict (different asset names, # different latest*.yml manifests), but a Windows-only release means