Skip to content

Commit 0e1d9c0

Browse files
committed
ci: shard integration and unit tests, run e2e groups in parallel, add a ci gate
The PR critical path was the PostgreSQL integration suite (~15 min), run in full on an 8 vCPU runner once per provisioning path. Its files run one at a time, so the runner sat mostly idle. - integration: each provisioning path (push, migrate) is split into 4 Vitest shards on 4 vCPU runners. Both paths keep the full suite: migrations add triggers, checks and NOT VALID constraints that db:push does not, so the schemas differ. - e2e: the four next-dev groups (scim, cli, stop-after, desktop-inbox) run as a matrix, each on its own database. The boot/wait/stop shell lives once in http-e2e.sh. - lint: lint, audits, type-check and schema sync split off from the tests. - test: apps/sim unit tests sharded 2 ways; shard 1 also runs root scripts and the other workspaces. Each shard has its own Turbo cache disk. - ci: one aggregate job that fails unless every check passed (skipped is allowed), so a ruleset can require a single stable check. Deploy gating is unchanged: migrate still requires the whole Test and Build workflow.
1 parent 8760ca7 commit 0e1d9c0

5 files changed

Lines changed: 276 additions & 306 deletions

File tree

‎.agents/skills/ship/SKILL.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ When the user runs `/ship`:
4747
- Run `/db-migrate` to review the migration for zero-downtime safety (expand/contract phasing, backward-compatibility with the deployed app version).
4848
- `(cd packages/db && bunx drizzle-kit generate && git status --porcelain ./migrations)` must print nothing (CI's schema/migration sync step).
4949
- `bun run check:migrations origin/staging` must pass (staging is the PR base). Do not silence a flagged statement with a `-- migration-safe:` annotation unless `/db-migrate` confirmed the old code no longer depends on it; otherwise split the destructive change into a later deploy.
50-
6. **Run pre-ship checks** from the repo root before staging. This has two phases: first **regenerate** every committed artifact so generated files never drift into a CI failure (this is what catches things like `agent-stream-docs` going stale after a `models.ts` edit), then run the **full audit suite** CI's `Lint and Test` job enforces. Both phases parallelize — but only across commands that write **disjoint** outputs — and a bare `wait` swallows child exit codes, so both phases below explicitly collect each job's status and abort ship if any failed.
50+
6. **Run pre-ship checks** from the repo root before staging. This has two phases: first **regenerate** every committed artifact so generated files never drift into a CI failure (this is what catches things like `agent-stream-docs` going stale after a `models.ts` edit), then run the **full audit suite** CI's `lint` job enforces. Both phases parallelize — but only across commands that write **disjoint** outputs — and a bare `wait` swallows child exit codes, so both phases below explicitly collect each job's status and abort ship if any failed.
5151
5252
**Phase A — regenerate the always-in-repo committed artifacts (parallel), then let step 7 stage whatever changed.** Regenerate only the generators whose inputs live entirely in this repo and that any ordinary code change can drift — `agent-stream-docs:generate` (derives from the provider model registry), `docs-manifest:generate` (derives from docs page paths), and `skills:sync` (derives from `.agents/skills/**`). They write disjoint outputs (`apps/docs/…/agent.mdx`, `apps/sim/lib/mothership/generated/docs-manifest.ts`, and `.claude/skills` links), so they parallelize safely, and each is idempotent (a no-op when already in sync):
5353
```bash

‎.claude/rules/sim-testing.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,11 @@ contracts, and demonstrated regressions.
3232

3333
| Suffix | Needs | Run with | In CI |
3434
|--------|-------|----------|-------|
35-
| `*.test.ts(x)` | nothing; global mocks from `vitest.setup.ts` | `vitest run` | Lint and Test job |
36-
| `*.integration.ts` | real PostgreSQL (`TEST_DATABASE_URL`), optionally Redis (`TEST_REDIS_URL`) | `vitest run --mode integration` | `PostgreSQL integration` job, by glob |
35+
| `*.test.ts(x)` | nothing; global mocks from `vitest.setup.ts` | `vitest run` | `test` jobs (sharded) |
36+
| `*.integration.ts` | real PostgreSQL (`TEST_DATABASE_URL`), optionally Redis (`TEST_REDIS_URL`) | `vitest run --mode integration` | `integration` jobs, by glob (sharded per provisioning path) |
3737
| `*.live.test.ts` | provider APIs, hosted sandboxes, local runtimes, or sibling checkouts | `vitest run --mode live <file>` (apps/sim) | never |
3838
| `apps/desktop/e2e/*.spec.ts` | the packaged Electron app | Playwright | desktop E2E workflow |
39-
| `apps/sim/scripts/test-*-e2e.ts` | a running app over HTTP | its `package.json` script when one exists (`bun run test:scim:e2e`; `test:workflow-version-compare:e2e` adds `--no-env-file`), else `bun scripts/test-<suite>-e2e.ts` from apps/sim | End-to-end over real HTTP job |
39+
| `apps/sim/scripts/test-*-e2e.ts` | a running app over HTTP | its `package.json` script when one exists (`bun run test:scim:e2e`; `test:workflow-version-compare:e2e` adds `--no-env-file`), else `bun scripts/test-<suite>-e2e.ts` from apps/sim | `e2e` jobs (`.github/scripts/http-e2e.sh`) |
4040

4141
- A unit test lives next to its source: `feature.ts` → `feature.test.ts`. No network, no database,
4242
no real timers.

‎.cursor/rules/sim-testing.mdc‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,11 @@ contracts, and demonstrated regressions.
3030

3131
| Suffix | Needs | Run with | In CI |
3232
|--------|-------|----------|-------|
33-
| `*.test.ts(x)` | nothing; global mocks from `vitest.setup.ts` | `vitest run` | Lint and Test job |
34-
| `*.integration.ts` | real PostgreSQL (`TEST_DATABASE_URL`), optionally Redis (`TEST_REDIS_URL`) | `vitest run --mode integration` | `PostgreSQL integration` job, by glob |
33+
| `*.test.ts(x)` | nothing; global mocks from `vitest.setup.ts` | `vitest run` | `test` jobs (sharded) |
34+
| `*.integration.ts` | real PostgreSQL (`TEST_DATABASE_URL`), optionally Redis (`TEST_REDIS_URL`) | `vitest run --mode integration` | `integration` jobs, by glob (sharded per provisioning path) |
3535
| `*.live.test.ts` | provider APIs, hosted sandboxes, local runtimes, or sibling checkouts | `vitest run --mode live <file>` (apps/sim) | never |
3636
| `apps/desktop/e2e/*.spec.ts` | the packaged Electron app | Playwright | desktop E2E workflow |
37-
| `apps/sim/scripts/test-*-e2e.ts` | a running app over HTTP | its `package.json` script when one exists (`bun run test:scim:e2e`; `test:workflow-version-compare:e2e` adds `--no-env-file`), else `bun scripts/test-<suite>-e2e.ts` from apps/sim | End-to-end over real HTTP job |
37+
| `apps/sim/scripts/test-*-e2e.ts` | a running app over HTTP | its `package.json` script when one exists (`bun run test:scim:e2e`; `test:workflow-version-compare:e2e` adds `--no-env-file`), else `bun scripts/test-<suite>-e2e.ts` from apps/sim | `e2e` jobs (`.github/scripts/http-e2e.sh`) |
3838

3939
- A unit test lives next to its source: `feature.ts` → `feature.test.ts`. No network, no database,
4040
no real timers.

‎.github/scripts/http-e2e.sh‎

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
#!/usr/bin/env bash
2+
# Runs one end-to-end suite group over real HTTP, each against its own `next dev` app.
3+
#
4+
# Usage: http-e2e.sh <scim|cli|stop-after|desktop-inbox> (run from apps/sim)
5+
#
6+
# The job provides DATABASE_URL, BETTER_AUTH_SECRET and ENCRYPTION_KEY; each group sets the rest of
7+
# its app's environment here. Reports and server logs land in $RUNNER_TEMP/e2e.
8+
#
9+
# The first request cold-compiles the app under Turbopack, which takes 42-150s on CI runners, so
10+
# the readiness deadline only has to catch a hung boot: an exited server fails immediately, and
11+
# either way the server log tail lands in the job log.
12+
#
13+
# Each app starts from an empty Turbopack dev cache: a cache written under other NEXT_PUBLIC_*
14+
# values, by a server that `next dev` SIGKILLs 100ms after SIGTERM, can panic Turbopack or wedge a
15+
# route compile on restore. It runs in its own session under an E2E_APP tag, and stop-session.sh
16+
# returns only once every process in that session or carrying that tag has exited (Next's
17+
# telemetry flush runs detached and still writes .next/dev).
18+
set -euo pipefail
19+
20+
group=${1:?usage: http-e2e.sh <scim|cli|stop-after|desktop-inbox>}
21+
report_dir="$RUNNER_TEMP/e2e"
22+
ready_timeout_seconds=300
23+
mkdir -p "$report_dir"
24+
25+
server_pid=''
26+
app_tag=''
27+
server_log=''
28+
status_log=''
29+
30+
finish() {
31+
local status=$?
32+
if [ -n "$server_pid" ]; then
33+
bash "$GITHUB_WORKSPACE/.github/scripts/stop-session.sh" "$server_pid" "$app_tag" || status=1
34+
wait "$server_pid" 2>/dev/null || true
35+
if [ -n "$status_log" ]; then
36+
awk '/^ (GET|POST|PUT|PATCH|DELETE|HEAD) \/api\// { print }' "$server_log" > "$status_log"
37+
fi
38+
if [ "$status" -ne 0 ]; then
39+
tail -n 200 "$server_log"
40+
fi
41+
fi
42+
exit "$status"
43+
}
44+
trap finish EXIT
45+
46+
# start_app <name> <port> <label> [record-http-status]
47+
start_app() {
48+
local name=$1 port=$2 label=$3
49+
server_log="$report_dir/$name-next.log"
50+
if [ "${4:-}" = record-http-status ]; then
51+
status_log="$report_dir/$name-http-status.log"
52+
fi
53+
app_tag="$name-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT-$$"
54+
export NEXT_PUBLIC_APP_URL="http://127.0.0.1:$port"
55+
export BETTER_AUTH_URL="$NEXT_PUBLIC_APP_URL"
56+
export DISABLE_TELEMETRY=true NEXT_TELEMETRY_DISABLED=1
57+
rm -rf .next/dev
58+
E2E_APP="$app_tag" setsid node ../../node_modules/next/dist/bin/next dev --hostname 127.0.0.1 \
59+
--port "$port" > "$server_log" 2>&1 &
60+
server_pid=$!
61+
62+
local started=$SECONDS
63+
until curl --fail --silent --max-time 10 "$NEXT_PUBLIC_APP_URL/api/health" > /dev/null; do
64+
if ! kill -0 "$server_pid" 2>/dev/null; then
65+
echo "::error::Local $label app exited during startup."
66+
exit 1
67+
fi
68+
if [ $((SECONDS - started)) -ge "$ready_timeout_seconds" ]; then
69+
echo "::error::Local $label app did not become ready within $ready_timeout_seconds seconds."
70+
exit 1
71+
fi
72+
sleep 2
73+
done
74+
echo "Local $label app ready after $((SECONDS - started))s"
75+
}
76+
77+
case "$group" in
78+
scim)
79+
export NEXT_PUBLIC_FORCE_HOSTED=true
80+
export BILLING_ENABLED=true NEXT_PUBLIC_BILLING_ENABLED=true
81+
export ENTERPRISE_ENABLED=true NEXT_PUBLIC_ENTERPRISE_ENABLED=true
82+
export SCIM_ENABLED=true NEXT_PUBLIC_SCIM_ENABLED=true
83+
export SSO_ENABLED=true NEXT_PUBLIC_SSO_ENABLED=true
84+
export ORGANIZATIONS_ENABLED=true NEXT_PUBLIC_ORGANIZATIONS_ENABLED=true
85+
export INTERNAL_API_SECRET=scim-http-ci-local-secret-at-least-32-characters
86+
export DB_TX_TRIPWIRE=throw
87+
export NEXT_PUBLIC_CHAT_DISABLED=true
88+
start_app scim 3017 SCIM record-http-status
89+
SCIM_E2E_BASE_URL="$NEXT_PUBLIC_APP_URL" \
90+
SCIM_E2E_DATABASE_URL="$DATABASE_URL" \
91+
SCIM_E2E_AUTH_SECRET="$BETTER_AUTH_SECRET" \
92+
SCIM_E2E_REPORT_PATH="$report_dir/scim-e2e-report.json" \
93+
bun run test:scim:e2e
94+
VERSION_COMPARE_E2E_BASE_URL="$NEXT_PUBLIC_APP_URL" \
95+
VERSION_COMPARE_E2E_DATABASE_URL="$DATABASE_URL" \
96+
VERSION_COMPARE_E2E_AUTH_SECRET="$BETTER_AUTH_SECRET" \
97+
VERSION_COMPARE_E2E_REPORT_PATH="$report_dir/version-compare-http-report.json" \
98+
bun run test:workflow-version-compare:e2e
99+
;;
100+
101+
# The search suites serve their own fixtures in-process and need no app. They share this group
102+
# because they finish in seconds. Self-hosted without billing: hosted billing admits runs through
103+
# Redis, which the CLI app is not given.
104+
cli)
105+
for search in google-content lucid zoom google-meet; do
106+
report_var="SEARCH_$(echo "$search" | tr 'a-z-' 'A-Z_')_REPORT_PATH"
107+
env NEXT_PUBLIC_APP_URL=http://127.0.0.1:3040 NEXT_PUBLIC_FORCE_HOSTED=false \
108+
"$report_var=$report_dir/search-$search.json" \
109+
bun "scripts/test-search-$search-e2e.ts"
110+
done
111+
export NEXT_PUBLIC_FORCE_HOSTED=false
112+
export INTERNAL_API_SECRET=cli-http-ci-local-secret-at-least-32-characters
113+
start_app cli 3018 CLI
114+
CLI_LATENCY_E2E_BASE_URL="$NEXT_PUBLIC_APP_URL" \
115+
CLI_LATENCY_E2E_DATABASE_URL="$DATABASE_URL" \
116+
CLI_LATENCY_E2E_RUNS=3 \
117+
CLI_LATENCY_E2E_WARMUP=1 \
118+
CLI_LATENCY_E2E_REPORT_PATH="$report_dir/cli-run-latency-report.json" \
119+
bun run test:cli-run-latency:e2e
120+
;;
121+
122+
# Self-hosted: hosted billing admits a run only through a Redis usage reservation.
123+
stop-after)
124+
export NEXT_PUBLIC_FORCE_HOSTED=false
125+
export INTERNAL_API_SECRET=stop-after-http-ci-local-secret-at-least-32-characters
126+
export DB_TX_TRIPWIRE=throw
127+
export NEXT_PUBLIC_CHAT_DISABLED=true
128+
start_app stop-after 3018 workflow record-http-status
129+
STOP_AFTER_E2E_BASE_URL="$NEXT_PUBLIC_APP_URL" \
130+
STOP_AFTER_E2E_DATABASE_URL="$DATABASE_URL" \
131+
STOP_AFTER_E2E_REPORT_PATH="$report_dir/stop-after-http-report.json" \
132+
bun run test:workflow-stop-after:e2e
133+
;;
134+
135+
# The desktop background executor's protocol: device registration, the SSE doorbell over Redis
136+
# pub/sub, presence, leased claims, Stop and isolation. The only group whose app gets Redis.
137+
desktop-inbox)
138+
export REDIS_URL=redis://127.0.0.1:6379
139+
export NEXT_PUBLIC_FORCE_HOSTED=false
140+
export MSHIP_DESKTOP_BACKGROUND_EXECUTOR=true
141+
export COPILOT_TOOL_PERMISSIONS_ENABLED=true
142+
export INTERNAL_API_SECRET=desktop-inbox-http-ci-local-secret-at-least-32-characters
143+
export DB_TX_TRIPWIRE=throw
144+
start_app desktop-inbox 3019 'desktop executor' record-http-status
145+
DESKTOP_INBOX_E2E_BASE_URL="$NEXT_PUBLIC_APP_URL" \
146+
DESKTOP_INBOX_E2E_DATABASE_URL="$DATABASE_URL" \
147+
DESKTOP_INBOX_E2E_REDIS_URL="$REDIS_URL" \
148+
DESKTOP_INBOX_E2E_AUTH_SECRET="$BETTER_AUTH_SECRET" \
149+
DESKTOP_INBOX_E2E_REPORT_PATH="$report_dir/desktop-inbox-http-report.json" \
150+
bun run test:desktop-inbox:e2e
151+
;;
152+
153+
*)
154+
echo "::error::Unknown end-to-end group: $group" >&2
155+
exit 2
156+
;;
157+
esac

0 commit comments

Comments
 (0)