Skip to content

CI

CI #2320

Workflow file for this run

name: CI
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
# Nightly pollution-detector run (see test-one-process below).
schedule:
- cron: "17 7 * * *"
concurrency:
group: ci-${{ github.workflow }}-${{ github.event_name == 'push' && github.sha || github.ref }}
cancel-in-progress: ${{ github.event_name != 'push' }}
jobs:
# oxfmt, oxlint, and typecheck share one runner: one checkout and one
# install instead of three of each. Dummy job names prettier and eslint
# stay for protect-main.
static-analysis:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: "1.3.14"
- name: Cache dependencies
uses: actions/cache@v4
with:
path: node_modules
# Exact-key-only: a restore-keys prefix of bun- would hydrate
# node_modules from a different lockfile. bun install then has to
# reconcile a stale tree; missing that step leaves wrong deps.
key: bun-${{ hashFiles('bun.lock') }}
- name: Install dependencies
run: bun install --frozen-lockfile
- name: oxfmt
run: bunx oxfmt --check .
- name: oxlint
run: bunx oxlint .
- name: Typecheck
run: bun run typecheck
# Build runs beside the suite instead of before it: tests import ./src
# directly and never read ./dist, so serializing build ahead of test put
# build time on the critical path for no dependency reason.
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "24"
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: "1.3.14"
- name: Cache dependencies
uses: actions/cache@v4
with:
path: node_modules
key: bun-${{ hashFiles('bun.lock') }}
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Build
run: bun run build
# The suite is sharded so the slowest slice, not the whole suite, sets the
# wall clock. Four time-balanced shards split the same
# ./src ./tests ./evals ./scripts union via bun's --shard=k/4, balanced by
# the checked-in per-file durations in scripts/ci-timings.json (--timings).
# Every shard still goes through check:projects-dir-guard: the guard
# forwards the path union plus the shard flags to the suite it wraps, so the
# gate covers the same tests as before, all of them sandboxed.
#
# Timings refresh policy: regenerate scripts/ci-timings.json by running the
# full union locally with --update-timings (same seeded flags as `test`):
# bun run check:projects-dir-guard ./src ./tests ./evals ./scripts \
# --timings=./scripts/ci-timings.json --update-timings
# Regen when the slowest shard's Test step skews more than ~20% above a
# quarter of the one-process suite time (shards drifting apart means the
# timings no longer describe the suite), or proactively whenever slow files
# land. A scheduled refresh artifact is a future option, not current setup.
test:
runs-on: ubuntu-latest
strategy:
# A red shard must not cancel the others; all results are the signal.
fail-fast: false
matrix:
shard: ["1/4", "2/4", "3/4", "4/4"]
name: test (${{ matrix.shard }})
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "24"
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: "1.3.14"
# The runner image has no ripgrep, so the grep plugin silently exercised
# its fallback walker and left the ripgrep path untested.
- name: Install ripgrep
run: sudo apt-get install -y ripgrep
- name: Cache dependencies
uses: actions/cache@v4
with:
path: node_modules
key: bun-${{ hashFiles('bun.lock') }}
- name: Install dependencies
run: bun install --frozen-lockfile
# The same script the local `bun run check` gate runs, with the full
# path union plus the shard's --shard/--timings flags forwarded through
# the guard to the suite. The guard routes a filtered run through
# test:paths, which carries the same seeded flags as the `test` script;
# bun test filters are additive, so appending filters to `bun run test`
# could not narrow it. --shard splits by file (balanced by --timings),
# so every shard covers the same union and the four shards together
# cover the whole suite. Randomized order catches tests that only pass
# in the default file order (shared module-level state, an unrestored
# global mock, a leaked env var).
# The seed stays 424242 in every shard rather than varying per shard:
# the shards already run disjoint file sets, and a fixed seed keeps
# any failure reproducible locally with the same
# `bun run test:paths <paths> --shard=k/4
# --timings=./scripts/ci-timings.json`.
- name: Test
run: bun run check:projects-dir-guard ./src ./tests ./evals ./scripts --shard=${{ matrix.shard }} --timings=./scripts/ci-timings.json
# Cross-shard pollution detector: the shards above split the path union,
# but the union is not the isolation domain — a mock.module leak across
# files fails in the one-process suite yet passes when the files land in
# different shards (CL-6967). This job reruns the whole union in one
# process with no filters, exactly like the local `bun run check` gate.
# Nightly (plus manual workflow_dispatch), not per-PR: the one-process
# suite takes ~2 minutes on CI and would put that back on the PR wall
# clock this sharding removes. Non-blocking (continue-on-error) so a slow
# or flaky full run cannot hold the gate; a real pollution failure still
# shows up red for triage.
test-one-process:
name: test (one-process pollution detector)
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
continue-on-error: true
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "24"
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: "1.3.14"
- name: Install ripgrep
run: sudo apt-get install -y ripgrep
- name: Cache dependencies
uses: actions/cache@v4
with:
path: node_modules
key: bun-${{ hashFiles('bun.lock') }}
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Test
run: bun run check:projects-dir-guard
# protect-main still requires the pre-restructure check names. These jobs
# exist only to publish those contexts after the real work succeeds.
prettier:
needs: static-analysis
runs-on: ubuntu-latest
steps:
- run: "true"
eslint:
needs: static-analysis
runs-on: ubuntu-latest
steps:
- run: "true"
typecheck:
needs: static-analysis
runs-on: ubuntu-latest
steps:
- run: "true"
build-and-test:
needs: [build, test]
runs-on: ubuntu-latest
steps:
- run: "true"
# The src-a/src-b/src-c matrix legs replaced the single test (src) leg, so
# this publishes that context name once all legs pass. needs: test waits for
# every matrix leg; a red leg skips this instead of greening it.
test-src:
name: test (src)
needs: test
runs-on: ubuntu-latest
steps:
- run: "true"