Skip to content

CI: fast PR gate (<2 min), full suite on main + nightly #12

CI: fast PR gate (<2 min), full suite on main + nightly

CI: fast PR gate (<2 min), full suite on main + nightly #12

Workflow file for this run

name: CI
on:
pull_request:
push:
branches: [master, main]
schedule:
# Nightly full pipeline: complete unit test suite, Docker e2e, and host
# behavior e2e for both harnesses.
- cron: '37 2 * * *'
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
# Two lanes:
#
# PR FAST LANE (every pull_request) — one consolidated job, target <2 min.
# The repo has a single self-hosted runner, so separate jobs would queue
# behind each other and multiply the wall time; everything runs inside one
# job instead:
#
# pr-fast-gate:
# 1. biome lint on all workspace packages (plugin, pi-plugin, cli)
# 2. plugin fast unit subset (src/config + src/agents — schema guards,
# permission model, project-security, removed-config-keys drift)
# 3. pi-plugin full unit tests (fast, ~40s measured)
# 4. cli unit tests
# 5. dashboard typecheck + lint + test + frontend build
# (config-parity gate: schema/manifest drift fails here)
# 6. plugin + pi-plugin + cli builds
#
# Typecheck on all packages (~2-4 min on this box) and the full plugin unit
# suite (~570s) move OFF the PR path — they run in the full lane. Lint and
# the builds stay: they are the cheap, high-signal packaging gates.
#
# FULL LANE (push to master/main + nightly) — everything that used to gate
# every PR, with the same gating structure as before:
#
# check-plugin, check-pi-plugin (full unit + lint + typecheck, parallel)
# ↓
# e2e-opencode, e2e-pi (Docker install + smoke, parallel — gated by unit)
# ↓
# e2e-host-opencode, e2e-host-pi (host behavior suite from packages/e2e-tests, parallel — gated by Docker)
#
# Two e2e layers cover different concerns:
# - Docker e2e: fresh-install smoke (plugin loads, doctor clean, one mock turn writes DB rows
# under cortexkit path with right harness). Catches packaging / install-flow regressions.
# - Host e2e: behavior suite with byte-level wire assertions, multi-turn cache stability,
# historian publish behavior, tag-owner collision, synthetic todowrite, cross-harness memory,
# etc. Spawns real `opencode serve` / Pi subprocesses against an embedded mock provider.
# Catches cache-stability + correctness regressions that the smoke layer cannot see.
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
pr-fast-gate:
name: PR fast gate
runs-on: self-hosted
if: github.event_name == 'pull_request'
timeout-minutes: 10
steps:
- uses: actions/checkout@v5
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install dependencies
run: bun install
# Lint every workspace package first — cheapest high-signal gate.
- name: Lint (all packages)
run: bun run lint
# Fast unit subset: src/config + src/agents carry the config-schema
# and permission-model guards (incl. project-security and
# removed-config-keys drift checks) — the regressions a PR can most
# plausibly introduce. Full unit suite runs in the full lane.
- name: Plugin unit tests (fast subset)
run: bun run --cwd packages/plugin test src/config src/agents
- name: Pi-plugin unit tests
run: bun run --cwd packages/pi-plugin test
- name: CLI unit tests
run: bun run --cwd packages/cli test
# Dashboard: config-parity test fails the PR if the plugin config
# schema drifts from the dashboard ConfigEditor coverage manifest.
- name: Dashboard typecheck
run: bun run --cwd packages/dashboard typecheck
- name: Dashboard lint
run: bun run --cwd packages/dashboard lint
- name: Dashboard tests (config parity)
run: bun run --cwd packages/dashboard test
- name: Dashboard build (frontend)
run: bun run --cwd packages/dashboard build
# Packaging gate: all three shippable packages must build.
- name: Build (plugin + pi-plugin + cli)
run: bun run build
check-plugin:
name: Check (plugin)
runs-on: self-hosted
if: github.event_name != 'pull_request'
timeout-minutes: 25
steps:
- uses: actions/checkout@v5
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install dependencies
run: bun install
- name: TypeScript typecheck
run: bun run typecheck
- name: Lint
run: bun run lint
- name: Build
run: bun run build
# Full workspace unit suite (plugin + pi-plugin + cli) — the plugin
# suite alone measured ~570s, which is why it lives here and not on
# the PR fast lane.
- name: Test
run: bun run test
check-pi-plugin:
name: Check (pi-plugin)
runs-on: self-hosted
if: github.event_name != 'pull_request'
timeout-minutes: 25
steps:
- uses: actions/checkout@v5
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install dependencies
run: bun install
- name: TypeScript typecheck
run: bun run --cwd packages/pi-plugin typecheck
- name: Lint
run: bun run --cwd packages/pi-plugin lint
- name: Build
run: bun run --cwd packages/pi-plugin build
- name: Test
run: bun run --cwd packages/pi-plugin test
e2e-opencode:
name: E2E (OpenCode, Docker)
runs-on: self-hosted
needs: [check-plugin]
if: github.event_name != 'pull_request'
timeout-minutes: 25
steps:
- uses: actions/checkout@v5
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install workspace deps
run: bun install --frozen-lockfile
- name: Build OpenCode plugin
run: bun run --cwd packages/plugin build
- name: Build CLI
# The CLI is its own package (@cortexkit/magic-context) since
# v0.16.1; Dockerfile.opencode COPYs packages/cli/dist/ in.
run: bun run --cwd packages/cli build
- name: Build E2E image
run: |
docker build \
--platform linux/amd64 \
-f tests/docker/Dockerfile.opencode \
-t mc-e2e-opencode \
.
- name: Run E2E
run: docker run --rm --platform linux/amd64 mc-e2e-opencode
e2e-pi:
name: E2E (Pi, Docker)
runs-on: self-hosted
needs: [check-pi-plugin]
if: github.event_name != 'pull_request'
timeout-minutes: 25
steps:
- uses: actions/checkout@v5
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install workspace deps
run: bun install --frozen-lockfile
- name: Build Pi plugin
run: bun run --cwd packages/pi-plugin build
- name: Build CLI
# The CLI moved to its own package (@cortexkit/magic-context) in
# v0.16.1. Dockerfile.pi COPYs packages/cli/dist/ in for the
# `magic-context doctor --harness pi` test invocation.
run: bun run --cwd packages/cli build
- name: Build E2E image
# The Pi Dockerfile installs runtime deps fresh inside the image
# (better-sqlite3 builds against linux/amd64), so no host-side
# `npm install` is needed.
run: |
docker build \
--platform linux/amd64 \
-f tests/docker/Dockerfile.pi \
-t mc-e2e-pi \
.
- name: Run E2E
run: docker run --rm --platform linux/amd64 mc-e2e-pi
e2e-host-opencode:
name: E2E (OpenCode, host behavior)
runs-on: self-hosted
# Gated on Docker e2e: no point exercising the deep behavior suite if
# the simpler install+smoke path is broken.
needs: [e2e-opencode]
if: github.event_name != 'pull_request'
# 90m: on the shared self-hosted box the SDK's per-call latency is
# amplified ~300x under co-tenant load (measured: session.create 100ms
# quiet vs 31s loaded). Test-level timeouts are 300s; the job budget
# must cover the full suite at that pace.
timeout-minutes: 90
steps:
- uses: actions/checkout@v5
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install workspace deps
run: bun install --frozen-lockfile
# Install opencode the same way the Docker image does — the host
# suite spawns `opencode serve` from PATH.
- name: Install opencode
# Float to LATEST (no --version) so CI exercises the opencode version
# users actually run and catches upstream breakage as it ships — not at
# our release time. (A fixed pin previously hid opencode 1.16's
# post-overflow change, commit 7e09660c3, until release; the
# overflow-recovery host test is now version-agnostic.) Trade-off: a
# known-bad upstream release can turn CI red for unrelated PRs — if that
# happens, temporarily re-pin with `--version X.Y.Z` here until it's
# resolved upstream.
run: |
curl -fsSL https://opencode.ai/install | bash
echo "$HOME/.opencode/bin" >> "$GITHUB_PATH"
- name: Verify opencode on PATH
run: opencode --version
- name: Build OpenCode plugin
# Host tests spawn `opencode serve` with a file:// plugin
# specifier pointing at packages/plugin/, so dist must exist.
run: bun run --cwd packages/plugin build
# Strip inherited NODE_ENV=test so the spawned opencode subprocess
# gets the same logging + runtime behavior as a normal local run
# (documented in CONTRIBUTING / project memory).
#
# Per-test (and per-hook) timeout bumped to 300s: the first test file
# Bun loads on a cold GitHub-hosted runner pays the dependency-resolution
# + opencode-binary cold-start cost in its `beforeAll(TestHarness.create)`,
# which can exceed Bun's 120s default. Subsequent files run in 5-10s.
- name: Run host e2e suite (OpenCode tests only)
env:
NODE_ENV: ""
run: |
# Match every test file except pi-*.test.ts. The shell glob is
# not great at "all .test.ts except pi-*", so list them explicitly.
cd packages/e2e-tests
# OpenCode host behavior tests, plus the cache-bust oracle unit test
# (src/cache-analysis.test.ts) — the oracle backs the cache-invariant
# suite, so a regression in it would silently neuter those guards.
files=$(ls tests/*.test.ts | grep -v "/pi-" | tr '\n' ' ')
files="$files src/cache-analysis.test.ts"
echo "Running OpenCode host tests: $files"
bun test --timeout 600000 $files
e2e-host-pi:
name: E2E (Pi, host behavior)
runs-on: self-hosted
needs: [e2e-pi]
if: github.event_name != 'pull_request'
timeout-minutes: 40
steps:
- uses: actions/checkout@v5
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
# Pi tests resolve the Pi binary via createRequire against
# @earendil-works/pi-coding-agent, which is a workspace dep of
# packages/pi-plugin. `bun install` brings it in.
- name: Install workspace deps
run: bun install --frozen-lockfile
# pi-cross-harness.test.ts spawns BOTH a Pi runner and an OpenCode
# serve to verify cross-harness memory sharing, so this job needs
# opencode on PATH too. Float to LATEST like the OpenCode host job
# (see that step for the rationale + re-pin escape hatch).
- name: Install opencode
run: |
curl -fsSL https://opencode.ai/install | bash
echo "$HOME/.opencode/bin" >> "$GITHUB_PATH"
- name: Verify opencode on PATH
run: opencode --version
- name: Build Pi plugin
run: bun run --cwd packages/pi-plugin build
# pi-cross-harness also instantiates the OpenCode harness, which
# spawns `opencode serve` with a file:// plugin specifier pointing
# at packages/plugin/. That dist must exist.
- name: Build OpenCode plugin
run: bun run --cwd packages/plugin build
# Per-test timeout bumped to 300s for the same cold-start reason as
# the OpenCode host job. Pi historian publish path also crosses an
# HTTP boundary into the mock provider, which is slower on shared
# runners than on local hardware.
- name: Run host e2e suite (Pi tests only)
env:
NODE_ENV: ""
run: |
cd packages/e2e-tests
bun test --timeout 600000 tests/pi-*.test.ts