Drop targeted placement to test isolate distribution (#1693) #4757
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| pull_request: | |
| push: | |
| branches: | |
| - main | |
| concurrency: | |
| group: ci-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| changes: | |
| name: Changed paths | |
| runs-on: blacksmith-4vcpu-ubuntu-2404 | |
| timeout-minutes: 10 | |
| outputs: | |
| desktop_smoke: ${{ github.event_name != 'pull_request' || steps.filter.outputs.desktop_smoke == 'true' }} | |
| selfhost_docker_smoke: ${{ github.event_name != 'pull_request' || steps.filter.outputs.selfhost_docker_smoke == 'true' }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Filter changed paths | |
| id: filter | |
| if: github.event_name == 'pull_request' | |
| uses: dorny/paths-filter@v3 | |
| with: | |
| filters: | | |
| desktop_smoke: | |
| - ".github/workflows/**" | |
| - "bun.lock" | |
| - "package.json" | |
| - "turbo.json" | |
| - "apps/desktop/**" | |
| - "apps/local/**" | |
| - "apps/cli/**" | |
| - "packages/app/**" | |
| - "packages/core/**" | |
| - "packages/hosts/mcp/**" | |
| - "packages/kernel/runtime-quickjs/**" | |
| - "packages/plugins/**" | |
| - "packages/react/**" | |
| selfhost_docker_smoke: | |
| - ".github/workflows/**" | |
| - ".dockerignore" | |
| - "bun.lock" | |
| - "package.json" | |
| - "turbo.json" | |
| - "apps/host-selfhost/**" | |
| - "packages/app/**" | |
| - "packages/core/**" | |
| - "packages/hosts/mcp/**" | |
| - "packages/kernel/runtime-quickjs/**" | |
| - "packages/plugins/**" | |
| - "packages/react/**" | |
| format: | |
| name: Format | |
| runs-on: blacksmith-4vcpu-ubuntu-2404 | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version: 1.3.11 | |
| - name: Cache Bun package cache | |
| uses: actions/cache@v4 | |
| with: | |
| path: ~/.bun/install/cache | |
| key: ${{ runner.os }}-bun-1.3.11-${{ hashFiles('bun.lock') }} | |
| restore-keys: | | |
| ${{ runner.os }}-bun-1.3.11- | |
| - run: bun install --frozen-lockfile --ignore-scripts | |
| - run: bun run format:check | |
| lint: | |
| name: Lint | |
| runs-on: blacksmith-4vcpu-ubuntu-2404 | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version: 1.3.11 | |
| - name: Cache Bun package cache | |
| uses: actions/cache@v4 | |
| with: | |
| path: ~/.bun/install/cache | |
| key: ${{ runner.os }}-bun-1.3.11-${{ hashFiles('bun.lock') }} | |
| restore-keys: | | |
| ${{ runner.os }}-bun-1.3.11- | |
| - run: bun install --frozen-lockfile --ignore-scripts | |
| - run: bun run lint | |
| typecheck: | |
| name: Typecheck | |
| runs-on: blacksmith-4vcpu-ubuntu-2404 | |
| timeout-minutes: 15 | |
| env: | |
| TURBO_API: ${{ vars.TURBO_API }} | |
| TURBO_TEAM: ${{ vars.TURBO_TEAM }} | |
| TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} | |
| TURBO_REMOTE_CACHE_SIGNATURE_KEY: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version: 1.3.11 | |
| - name: Cache Bun package cache | |
| uses: actions/cache@v4 | |
| with: | |
| path: ~/.bun/install/cache | |
| key: ${{ runner.os }}-bun-1.3.11-${{ hashFiles('bun.lock') }} | |
| restore-keys: | | |
| ${{ runner.os }}-bun-1.3.11- | |
| # No prebuilt better-sqlite3 binary matches this runner, so `bun install` | |
| # builds it from source via node-gyp, whose undici needs Node 22.10+ | |
| # (webidl.markAsUncloneable). Pin the same Node 24 runtime used by release. | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: 24 | |
| - run: bun install --frozen-lockfile | |
| - run: bun run typecheck | |
| # The committed routeTree.gen.ts files are generated but checked in, and | |
| # they carry @ts-nocheck — so the typecheck above structurally CANNOT see | |
| # a route that the contract declares and the tree lacks. That drift has | |
| # already shipped a live 404 once. Regenerate and diff instead. | |
| - run: bun run check:routes | |
| test: | |
| name: Test | |
| runs-on: blacksmith-16vcpu-ubuntu-2404 | |
| timeout-minutes: 15 | |
| # Run eight independent packages at once and cap each Vitest process at two | |
| # workers. This uses all 16 vCPUs without every nested runner seeing the | |
| # whole machine and oversubscribing it. | |
| env: | |
| TURBO_API: ${{ vars.TURBO_API }} | |
| TURBO_TEAM: ${{ vars.TURBO_TEAM }} | |
| TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} | |
| TURBO_REMOTE_CACHE_SIGNATURE_KEY: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }} | |
| TURBO_TEST_CONCURRENCY: 8 | |
| VITEST_MAX_WORKERS: 2 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version: 1.3.11 | |
| - name: Cache Bun package cache | |
| uses: actions/cache@v4 | |
| with: | |
| path: ~/.bun/install/cache | |
| key: ${{ runner.os }}-bun-1.3.11-${{ hashFiles('bun.lock') }} | |
| restore-keys: | | |
| ${{ runner.os }}-bun-1.3.11- | |
| # apps/cloud's test script invokes `node` directly; undici 8.x (pulled | |
| # in by @cloudflare/vitest-pool-workers) calls webidl.markAsUncloneable | |
| # which only exists in Node 22.10+. Pin the Node 24 CI runtime. | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: 24 | |
| - run: bun install --frozen-lockfile | |
| # Fail loudly if a load-bearing patched dependency (e.g. the agents MCP | |
| # transport hang fix) resolved to a stale, unpatched dist. Without this, | |
| # a bun cache edge case can leave the patch unapplied while everything | |
| # still builds and tests green, and prod ships the pre-fix transport with | |
| # no signal. This is just a few file reads plus greps, so ~zero cost. | |
| - run: bun run check:patches | |
| - run: bun run --cwd e2e test:ci-shard | |
| - run: bun run test | |
| e2e: | |
| name: E2E (${{ matrix.target }}${{ matrix['shard-name'] && format(' {0}', matrix['shard-name']) || '' }}) | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| # The planner assigns every file exactly once using recorded slow-file | |
| # durations plus a conservative weight for new tests. The cloud DB's | |
| # connection teardown and concurrent socket protocol have dedicated | |
| # regression tests; these shards balance wall clock, not hide retries. | |
| - { target: cloud, shard-index: 1, shard-name: 1of16 } | |
| - { target: cloud, shard-index: 2, shard-name: 2of16 } | |
| - { target: cloud, shard-index: 3, shard-name: 3of16 } | |
| - { target: cloud, shard-index: 4, shard-name: 4of16 } | |
| - { target: cloud, shard-index: 5, shard-name: 5of16 } | |
| - { target: cloud, shard-index: 6, shard-name: 6of16 } | |
| - { target: cloud, shard-index: 7, shard-name: 7of16 } | |
| - { target: cloud, shard-index: 8, shard-name: 8of16 } | |
| - { target: cloud, shard-index: 9, shard-name: 9of16 } | |
| - { target: cloud, shard-index: 10, shard-name: 10of16 } | |
| - { target: cloud, shard-index: 11, shard-name: 11of16 } | |
| - { target: cloud, shard-index: 12, shard-name: 12of16 } | |
| - { target: cloud, shard-index: 13, shard-name: 13of16 } | |
| - { target: cloud, shard-index: 14, shard-name: 14of16 } | |
| - { target: cloud, shard-index: 15, shard-name: 15of16 } | |
| - { target: cloud, shard-index: 16, shard-name: 16of16 } | |
| - { target: selfhost, shard-index: 1, shard-name: 1of10 } | |
| - { target: selfhost, shard-index: 2, shard-name: 2of10 } | |
| - { target: selfhost, shard-index: 3, shard-name: 3of10 } | |
| - { target: selfhost, shard-index: 4, shard-name: 4of10 } | |
| - { target: selfhost, shard-index: 5, shard-name: 5of10 } | |
| - { target: selfhost, shard-index: 6, shard-name: 6of10 } | |
| - { target: selfhost, shard-index: 7, shard-name: 7of10 } | |
| - { target: selfhost, shard-index: 8, shard-name: 8of10 } | |
| - { target: selfhost, shard-index: 9, shard-name: 9of10 } | |
| - { target: selfhost, shard-index: 10, shard-name: 10of10 } | |
| # Local files own their server, browser and data directory. Separate | |
| # runners preserve that isolation while removing its 69-second serial | |
| # lane from the two-minute critical path. | |
| - { target: local, shard-index: 1, shard-name: 1of2 } | |
| - { target: local, shard-index: 2, shard-name: 2of2 } | |
| runs-on: blacksmith-4vcpu-ubuntu-2404 | |
| timeout-minutes: 30 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version: 1.3.11 | |
| - name: Cache Bun package cache | |
| uses: actions/cache@v4 | |
| with: | |
| path: ~/.bun/install/cache | |
| key: ${{ runner.os }}-bun-1.3.11-${{ hashFiles('bun.lock') }} | |
| restore-keys: | | |
| ${{ runner.os }}-bun-1.3.11- | |
| # The dev stacks spawn Node sidecars (vite/workerd tooling); pin the | |
| # same Node 24 runtime the release and publish workflows use. | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: 24 | |
| - run: bun install --frozen-lockfile | |
| - name: Cache Playwright browsers | |
| uses: actions/cache@v4 | |
| with: | |
| path: ~/.cache/ms-playwright | |
| key: ${{ runner.os }}-playwright-1.60.0 | |
| restore-keys: | | |
| ${{ runner.os }}-playwright- | |
| # Install from e2e so bunx resolves ITS pinned playwright (the version | |
| # the tests run against) rather than floating to the latest. | |
| # Blacksmith's Ubuntu image carries the official GitHub runner system | |
| # dependencies. Restore the pinned browser binaries without apt-updating | |
| # every matrix machine. | |
| - name: Install Playwright Chromium | |
| run: bunx playwright install chromium chromium-headless-shell | |
| working-directory: e2e | |
| # Each target either boots its own shared dev server or lets each file own | |
| # its server. Ports and data paths are hermetic in both cases. | |
| # Do not retry scenarios: retries hide flakes and multiply slow timeout | |
| # failures. The fixtures and process lifecycle are deterministic enough | |
| # that the first result is the result. | |
| - name: Run cloud shard | |
| if: matrix.target == 'cloud' | |
| env: | |
| MCP_SESSION_TIMEOUT_MS: "3000" | |
| MCP_PAUSED_SESSION_IDLE_TIMEOUT_MS: "6000" | |
| run: bun scripts/run-ci-shard.ts cloud ${{ matrix['shard-index'] }} | |
| working-directory: e2e | |
| - name: Run scenarios | |
| if: matrix.target != 'cloud' | |
| run: bun scripts/run-ci-shard.ts ${{ matrix.target }} ${{ matrix['shard-index'] }} | |
| working-directory: e2e | |
| # Failed runs keep their trace.zip / session.mp4 / step screenshots in | |
| # runs/<target>/<slug>/ — surface them instead of a bare red X. | |
| - name: Upload run artifacts | |
| if: failure() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: e2e-runs-${{ matrix.target }}${{ matrix['shard-name'] && format('-{0}', matrix['shard-name']) || '' }} | |
| path: e2e/runs/ | |
| retention-days: 7 | |
| desktop-smoke: | |
| name: Desktop smoke build | |
| needs: changes | |
| if: needs.changes.outputs.desktop_smoke == 'true' | |
| runs-on: blacksmith-4vcpu-ubuntu-2404 | |
| timeout-minutes: 20 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version: 1.3.11 | |
| - name: Cache Bun package cache | |
| uses: actions/cache@v4 | |
| with: | |
| path: ~/.bun/install/cache | |
| key: ${{ runner.os }}-bun-1.3.11-${{ hashFiles('bun.lock') }} | |
| restore-keys: | | |
| ${{ runner.os }}-bun-1.3.11- | |
| # No prebuilt better-sqlite3 binary matches this runner, so `bun install` | |
| # builds it from source via node-gyp, whose undici needs Node 22.10+ | |
| # (webidl.markAsUncloneable). Pin the same Node 24 CI runtime. | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: 24 | |
| - run: bun install --frozen-lockfile | |
| - name: Build web app | |
| run: bun run --filter @executor-js/local build | |
| - name: Build bundled executor | |
| env: | |
| BUN_TARGET: bun-linux-x64 | |
| run: bun ./scripts/build-sidecar.ts | |
| working-directory: apps/desktop | |
| # Run the compiled sidecar in a clean container where the build | |
| # workspace does not exist. bun --compile bakes build-machine paths | |
| # into __dirname for runtime-loaded assets; running the binary on the | |
| # build machine hides that entire failure class (this exact gap | |
| # shipped a broken 1Password SDK: its sdk-core wasm was read from the | |
| # CI runner's node_modules path at runtime, ENOENT on every user | |
| # machine). Pass condition: the 1Password endpoint reaches real | |
| # credential validation, not a module-load or empty-namespace error. | |
| - name: Run sidecar outside the workspace | |
| working-directory: apps/desktop | |
| run: | | |
| docker run --rm -d --name sidecar-smoke -p 45841:45841 -e HOME=/tmp \ | |
| -v "$PWD/resources/executor:/opt/executor:ro" \ | |
| debian:bookworm-slim /opt/executor/executor daemon run --foreground \ | |
| --port 45841 --hostname 0.0.0.0 --auth-token=ci-smoke | |
| for i in $(seq 1 60); do | |
| code=$(curl -s -o /dev/null -w '%{http_code}' -H "Authorization: Bearer ci-smoke" http://127.0.0.1:45841/api/onepassword/vaults || true) | |
| [ "$code" != "000" ] && break | |
| docker ps -q -f name=sidecar-smoke | grep -q . || { docker logs sidecar-smoke; exit 1; } | |
| sleep 1 | |
| done | |
| body=$(curl -s -H "Authorization: Bearer ci-smoke" \ | |
| "http://127.0.0.1:45841/api/onepassword/vaults?authKind=service-account&account=ops_ci_fake_token") | |
| docker stop sidecar-smoke | |
| echo "$body" | |
| if echo "$body" | grep -qE "sdk module load|ENOENT|is not a function|not a constructor"; then | |
| echo "::error::1Password SDK failed to load inside the compiled binary (baked build path or missing sibling asset)" | |
| exit 1 | |
| fi | |
| if ! echo "$body" | grep -q "invalid service account token"; then | |
| echo "::error::sidecar did not reach 1Password credential validation; unexpected response above" | |
| exit 1 | |
| fi | |
| - name: Build Electron main/preload/renderer | |
| run: bunx --bun electron-vite build | |
| working-directory: apps/desktop | |
| selfhost-docker-smoke: | |
| name: Self-host Docker image | |
| needs: changes | |
| if: needs.changes.outputs.selfhost_docker_smoke == 'true' | |
| runs-on: blacksmith-4vcpu-ubuntu-2404 | |
| timeout-minutes: 20 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Setup Blacksmith Builder | |
| uses: useblacksmith/setup-docker-builder@v1 | |
| - name: Build self-host image | |
| uses: useblacksmith/build-push-action@v2 | |
| with: | |
| context: . | |
| file: apps/host-selfhost/Dockerfile | |
| push: false | |
| load: true | |
| tags: executor-selfhost:ci | |
| # Building alone doesn't catch startup crashes (e.g. a runtime-resolved | |
| # package missing from the packaged runtime), so boot the container and | |
| # require a healthy /api/health before calling the image good. | |
| - name: Boot image and check health | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| docker run -d --name selfhost-smoke -p 4788:4788 \ | |
| -e BETTER_AUTH_SECRET="$(openssl rand -hex 32)" \ | |
| -e EXECUTOR_BOOTSTRAP_ADMIN_EMAIL=smoke@example.com \ | |
| -e EXECUTOR_BOOTSTRAP_ADMIN_PASSWORD="$(openssl rand -hex 16)" \ | |
| -e EXECUTOR_WEB_BASE_URL=http://localhost:4788 \ | |
| executor-selfhost:ci | |
| for i in $(seq 1 30); do | |
| if [ "$(docker inspect -f '{{.State.Running}}' selfhost-smoke)" != "true" ]; then | |
| echo "container exited during startup" | |
| docker logs selfhost-smoke | |
| exit 1 | |
| fi | |
| if curl -fsS http://localhost:4788/api/health >/dev/null 2>&1; then | |
| echo "healthy after ${i}s" | |
| docker logs selfhost-smoke | |
| exit 0 | |
| fi | |
| sleep 1 | |
| done | |
| echo "health check never passed" | |
| docker logs selfhost-smoke | |
| exit 1 |