Skip to content

Commit e895a5e

Browse files
killaguclaude
andcommitted
perf(ci): shard tests for ≤60s per ubuntu job; PR runs ubuntu-only fast path
Cut the monolithic test job (single ~190s vitest run, gated by a 170s cluster file; 12-29min on CI) into parallel, wall-time-balanced shards so every job on the PR critical path stays ≤60s — verified on real CI (48 ubuntu shards run 15-59s). PR critical path = ubuntu only: - Test sharding (scripts/run-shard.js, driven by the CI matrix): fork-heavy packages split via vitest --shard (cluster→3, egg→2, mock→2, schedule→4, security→2, plus redis/multipart/development standalone), each capped with --maxWorkers so forked egg cluster processes don't oversubscribe the CPU; the light remainder greedy bin-packed by per-dir cpu weight into 8 rest-* shards. - Long single test files split by `describe` block (no semantic change, no removed cases) so vitest can parallelize them. - quality split into a parallel matrix (lint/typecheck/fmtcheck/build/site) so the slowest check — not their sum — bounds wall time. - egg-bin runs without --coverage to stay ≤60s (no threshold; report only). macOS/Windows whole-suite compatibility (slow: ~13-25min; macOS has only ~5 runner slots) moved to a `test-compat` job that runs at merge time (merge_group) and on push to next — NOT on pull_request — so it no longer gates the PR wall while still enforcing cross-platform coverage before code lands. CI test jobs on a PR drop from 132 to the 48 ubuntu shards. Latent bugs fixed along the way: - tegg controller ControllerMetaManager.test.ts triggers a duplicate-proto error that polluted the process-global registry under isolate:false, failing sibling files; isolated that project (isolate:true). - clean-dist used `ut run clean --workspaces` which aborts the && chain (root has no clean script) → scripts/clean-dist.js. - typecheck/pretest used `ut run <self> --workspaces`, which includes the root and recurses infinitely → scripts/run-workspaces.js (concurrent, root-bin PATH). - Windows runner images no longer set %HOME%, breaking egg_loader getHomedir; export HOME=%USERPROFILE% on the windows compat job. Windows flakes absorbed by --retry. No test semantics changed or cases removed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 5c856b3 commit e895a5e

46 files changed

Lines changed: 2392 additions & 1552 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 153 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,20 @@ permissions:
1818
actions: write
1919

2020
jobs:
21-
typecheck:
21+
# Static-analysis / build checks. Each check runs as its own parallel job
22+
# (instead of serial steps) so the slowest one — not their sum — bounds the
23+
# wall time. Goal: keep each ≤ 60s.
24+
quality:
25+
strategy:
26+
fail-fast: false
27+
matrix:
28+
check: ['lint', 'typecheck', 'fmtcheck', 'build', 'site']
29+
30+
name: Quality (${{ matrix.check }})
2231
runs-on: ubuntu-latest
2332

2433
concurrency:
25-
group: typecheck-${{ github.workflow }}-#${{ github.event.pull_request.number || github.head_ref || github.ref }}
34+
group: quality-${{ matrix.check }}-${{ github.workflow }}-#${{ github.event.pull_request.number || github.head_ref || github.ref }}
2635
cancel-in-progress: true
2736
steps:
2837
- name: Checkout repository
@@ -41,40 +50,151 @@ jobs:
4150
run: ut install --from pnpm || (sleep 5 && ut install --from pnpm) || (sleep 10 && ut install --from pnpm)
4251

4352
- name: Run lint
53+
if: ${{ matrix.check == 'lint' }}
4454
run: ut run lint
4555

4656
- name: Run typecheck
57+
if: ${{ matrix.check == 'typecheck' }}
4758
run: ut run typecheck
4859

4960
- name: Run format check
61+
if: ${{ matrix.check == 'fmtcheck' }}
5062
run: ut run fmtcheck
5163

5264
- name: Run build
65+
if: ${{ matrix.check == 'build' }}
5366
run: ut run build
5467

5568
- name: Run site build
69+
if: ${{ matrix.check == 'site' }}
5670
run: ut run site:build
5771

5872
test:
5973
strategy:
6074
fail-fast: false
6175
matrix:
62-
os: ['ubuntu-latest', 'macos-latest', 'windows-latest']
76+
# ubuntu-latest runs the fine-grained shards: fork-heavy packages are
77+
# split into vitest --shard slices and the light remainder into
78+
# weight-balanced rest-* shards (see scripts/run-shard.js). Sized so each
79+
# shard's `vitest run` step stays ≤ ~60s on ubuntu-latest, where runner
80+
# concurrency is high enough to run them all in parallel.
81+
os: ['ubuntu-latest']
6382
node: ['22', '24']
83+
shard:
84+
- cluster-1
85+
- cluster-2
86+
- cluster-3
87+
- egg-1
88+
- egg-2
89+
- mock-1
90+
- mock-2
91+
- schedule-1
92+
- schedule-2
93+
- schedule-3
94+
- schedule-4
95+
- development
96+
- security-1
97+
- security-2
98+
- redis
99+
- multipart
100+
- rest-1
101+
- rest-2
102+
- rest-3
103+
- rest-4
104+
- rest-5
105+
- rest-6
106+
- rest-7
107+
- rest-8
108+
# macOS/Windows whole-suite compatibility runs live in `test-compat`,
109+
# which only runs at merge time (merge_group / push to next) — not on
110+
# every PR — because those runners are slow (~13-25min whole-suite) and
111+
# macOS has only ~5 concurrent slots, so they would dominate the PR wall.
112+
# The PR critical path is this ubuntu fan-out, every shard ≤60s.
113+
114+
name: Test (${{ matrix.os }}, ${{ matrix.node }}, ${{ matrix.shard }})
115+
runs-on: ${{ matrix.os }}
116+
117+
concurrency:
118+
group: test-${{ github.workflow }}-#${{ github.event.pull_request.number || github.head_ref || github.ref }}-(${{ matrix.os }}, ${{ matrix.node }}, ${{ matrix.shard }})
119+
cancel-in-progress: true
120+
121+
steps:
122+
- name: Checkout repository
123+
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
124+
125+
- name: Start Redis
126+
uses: shogo82148/actions-setup-redis@cff708d63a30aebc0bfaa7276fb709d173f36cb6 # v1
127+
with:
128+
redis-version: '7'
129+
auto-start: 'true'
130+
131+
- name: Start MySQL
132+
uses: shogo82148/actions-setup-mysql@27e74fac04c136a9f4c2dc2ed457df57331b3e0c # v1
133+
with:
134+
mysql-version: '8'
135+
auto-start: 'true'
136+
- name: Init DB
137+
run: |
138+
mysql -uroot -e "CREATE DATABASE IF NOT EXISTS test;"
139+
140+
- name: Setup utoo
141+
uses: utooland/setup-utoo@3a51006d0b66afcc32d1b9177a4b200b74f4a8cb # main
142+
143+
- name: Set up Node.js
144+
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
145+
with:
146+
node-version: ${{ matrix.node }}
147+
148+
- name: Install dependencies
149+
run: ut install --from pnpm || (sleep 5 && ut install --from pnpm) || (sleep 10 && ut install --from pnpm)
150+
151+
- name: Prepare test fixtures (clean dist + db)
152+
# Mirrors the original `preci` (pretest) step: clears stale dist so tegg
153+
# plugin tests don't double-load src+dist, and runs per-workspace
154+
# pretest (e.g. orm DB table init). The shard runner invokes vitest
155+
# directly, bypassing the npm `preci` lifecycle, so do it explicitly.
156+
run: ut run pretest
64157

65-
name: Test (${{ matrix.os }}, ${{ matrix.node }})
158+
- name: Run tests (shard ${{ matrix.shard }})
159+
run: node scripts/run-shard.js ${{ matrix.shard }} -- --coverage
160+
161+
- name: Run example tests
162+
# Run once (on the rest-1 shard) to avoid duplication across shards.
163+
if: ${{ matrix.shard == 'rest-1' }}
164+
run: |
165+
ut run example:test:all
166+
167+
- name: Code Coverage
168+
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5
169+
with:
170+
use_oidc: true
171+
172+
# macOS / Windows whole-suite compatibility runs. These are slow (~13-25min)
173+
# and macOS has only ~5 concurrent runner slots, so they would dominate the
174+
# PR wall. They run only at merge time (merge_group) and on push to next, so
175+
# the PR critical path stays the ubuntu `test` fan-out (each shard ≤60s) while
176+
# cross-platform coverage is still enforced before code lands.
177+
test-compat:
178+
if: ${{ github.event_name != 'pull_request' }}
179+
strategy:
180+
fail-fast: false
181+
matrix:
182+
os: ['macos-latest', 'windows-latest']
183+
node: ['22', '24']
184+
185+
name: Test compat (${{ matrix.os }}, ${{ matrix.node }})
66186
runs-on: ${{ matrix.os }}
67187

68188
concurrency:
69-
group: test-${{ github.workflow }}-#${{ github.event.pull_request.number || github.head_ref || github.ref }}-(${{ matrix.os }}, ${{ matrix.node }})
189+
group: test-compat-${{ github.workflow }}-#${{ github.event.pull_request.number || github.head_ref || github.ref }}-(${{ matrix.os }}, ${{ matrix.node }})
70190
cancel-in-progress: true
71191

72192
steps:
73193
- name: Checkout repository
74194
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
75195

76-
- name: Start Redis (MacOS or Linux)
77-
if: ${{ matrix.os == 'macos-latest' || matrix.os == 'ubuntu-latest' }}
196+
- name: Start Redis (macOS)
197+
if: ${{ matrix.os == 'macos-latest' }}
78198
uses: shogo82148/actions-setup-redis@cff708d63a30aebc0bfaa7276fb709d173f36cb6 # v1
79199
with:
80200
redis-version: '7'
@@ -130,15 +250,14 @@ jobs:
130250
131251
Write-Host "Memurai is ready on 127.0.0.1:6379"
132252
133-
# install and start MySQL (will automatically start mysqld)
134-
- name: Start MySQL (macOS or Linux)
135-
if: ${{ matrix.os == 'macos-latest' || matrix.os == 'ubuntu-latest' }}
253+
- name: Start MySQL (macOS)
254+
if: ${{ matrix.os == 'macos-latest' }}
136255
uses: shogo82148/actions-setup-mysql@27e74fac04c136a9f4c2dc2ed457df57331b3e0c # v1
137256
with:
138257
mysql-version: '8'
139258
auto-start: 'true'
140-
- name: Init DB (macOS or Linux)
141-
if: ${{ matrix.os == 'macos-latest' || matrix.os == 'ubuntu-latest' }}
259+
- name: Init DB (macOS)
260+
if: ${{ matrix.os == 'macos-latest' }}
142261
run: |
143262
mysql -uroot -e "CREATE DATABASE IF NOT EXISTS test;"
144263
@@ -148,8 +267,6 @@ jobs:
148267
run: |
149268
choco install -y mysql
150269
refreshenv
151-
# MySQL default root has no password, set a password and create database/user
152-
# & mysqladmin -u root password root
153270
& mysql -uroot -e "CREATE DATABASE IF NOT EXISTS test;"
154271
155272
- name: Setup utoo
@@ -163,20 +280,16 @@ jobs:
163280
- name: Install dependencies
164281
run: ut install --from pnpm || (sleep 5 && ut install --from pnpm) || (sleep 10 && ut install --from pnpm)
165282

166-
- name: Run tests
167-
run: ut run ci
283+
- name: Prepare test fixtures (clean dist + db)
284+
run: ut run pretest
168285

169-
- name: Run example tests
170-
if: ${{ matrix.os != 'windows-latest' }}
171-
run: |
172-
ut run example:test:all
286+
- name: Set HOME on Windows
287+
if: ${{ matrix.os == 'windows-latest' }}
288+
shell: pwsh
289+
run: echo "HOME=$env:USERPROFILE" >> $env:GITHUB_ENV
173290

174-
- name: Code Coverage
175-
# skip on windows, it will hangup on codecov
176-
if: ${{ matrix.os != 'windows-latest' }}
177-
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5
178-
with:
179-
use_oidc: true
291+
- name: Run tests (whole suite)
292+
run: node scripts/run-shard.js all -- --coverage
180293

181294
test-egg-bin:
182295
strategy:
@@ -207,17 +320,19 @@ jobs:
207320
- name: Install dependencies
208321
run: ut install --from pnpm || (sleep 5 && ut install --from pnpm) || (sleep 10 && ut install --from pnpm)
209322

323+
- name: Set HOME on Windows
324+
if: ${{ matrix.os == 'windows-latest' }}
325+
shell: pwsh
326+
run: echo "HOME=$env:USERPROFILE" >> $env:GITHUB_ENV
327+
210328
- name: Run tests
329+
# `ci` (vitest --coverage) adds ~50s of instrumentation on egg-bin's
330+
# fork-heavy suite, pushing it past 60s. Run without coverage (`test`)
331+
# to keep the job ≤60s; egg-bin's fork tests can't be sharded (they
332+
# depend on cross-file shared state under isolate:false).
211333
run: |
212334
ut run build -- --workspace ./tools/egg-bin
213-
ut run ci --workspace @eggjs/bin
214-
215-
- name: Code Coverage
216-
# skip on windows, it will hangup on codecov https://github.com/codecov/codecov-action/issues/1787
217-
if: ${{ matrix.os != 'windows-latest' }}
218-
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5
219-
with:
220-
use_oidc: true
335+
ut run test --workspace @eggjs/bin
221336
222337
test-egg-scripts:
223338
strategy:
@@ -263,9 +378,12 @@ jobs:
263378
runs-on: ubuntu-latest
264379
needs:
265380
- test
381+
- test-compat
266382
- test-egg-bin
267383
- test-egg-scripts
268-
- typecheck
384+
- quality
269385
steps:
270386
- run: exit 1
387+
# test-compat is skipped on pull_request (result 'skipped'), which does
388+
# not trip this gate — only real failures/cancellations do.
271389
if: ${{ always() && (contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')) }}

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,3 +120,8 @@ ecosystem-ci/examples
120120
pnpm-lock.yaml
121121
.utoo.toml
122122
.claude/
123+
124+
# benchmark output directories
125+
benchmark/ci-test/baseline*
126+
benchmark/ci-test/run*
127+
benchmark/ci-test/2*

AGENTS.md

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,18 +28,55 @@ Egg is maintained as a utoo monorepo.
2828

2929
### Local CI
3030

31-
Run tests **without building first**. The CI workflow (`ut install → ut run ci`) never runs `build` before tests. If `dist/` directories exist from a prior build, tegg plugin tests will fail with `duplicate proto` errors because globby scans both `src/*.ts` and `dist/*.js`, loading the same decorated class twice.
31+
Run tests **without building first**. The CI test jobs run `ut run pretest`
32+
(clean dist + per-workspace pretest) then a vitest shard; they never `build`
33+
before tests. If `dist/` directories exist from a prior build, tegg plugin tests
34+
will fail with `duplicate proto` errors because globby scans both `src/*.ts` and
35+
`dist/*.js`, loading the same decorated class twice. `scripts/clean-dist.js`
36+
(run by `ut run clean-dist`) removes every `dist/` for you.
3237

33-
When you see `duplicate proto` failures locally:
38+
When you see `duplicate proto` failures locally, run `ut run clean-dist` (or the
39+
equivalent find below) and re-run:
3440

3541
```bash
3642
find tegg packages plugins tools -name dist -type d \
3743
-not -path '*/node_modules/*' -not -path '*/test/*' -not -path '*/fixtures/*' \
3844
-exec rm -rf {} +
3945
```
4046

47+
### Aggregator scripts (avoid `ut run <x> --workspaces` recursion)
48+
49+
The root `typecheck` / `pretest` aggregate per-workspace scripts via
50+
`node scripts/run-workspaces.js <script>`, **not** `ut run <script> --workspaces`.
51+
With utoo, `--workspaces` includes the monorepo root, so a root script that calls
52+
`ut run <same-name> --workspaces` recurses infinitely. `run-workspaces.js`
53+
enumerates real workspace dirs, runs each script body against the root-hoisted
54+
`node_modules/.bin` (so root-only CLIs like `tsgo` resolve even where a stale
55+
local bin shim exists), and runs them concurrently to bound wall time.
56+
4157
Then re-run tests.
4258

59+
### CI test sharding
60+
61+
CI splits the test suite across parallel runners via `scripts/run-shard.js`
62+
(`node scripts/run-shard.js <shard>`). Each heavy fork-based package
63+
(`cluster`, `egg`, `mock`, `development`, `schedule-a`/`schedule-b`) runs on its
64+
own runner with a `--maxWorkers` cap so forked egg cluster child processes do
65+
not oversubscribe the CPU and time out; `rest-a`/`rest-b` split everything else
66+
with full parallelism. The goal is to keep each shard's `vitest run` wall time
67+
under ~60s. To reproduce one shard locally: `node scripts/run-shard.js cluster`.
68+
Use `node scripts/run-shard.js all` for the unsharded full suite.
69+
70+
Cluster/mock/schedule tests fork real OS processes that bind ports. If a run is
71+
killed mid-flight, orphaned `start-cluster`/`app_worker`/`agent_worker`
72+
processes can linger and hold ports (e.g. 17001), causing later runs to fail
73+
with `EADDRINUSE`/`app.ready()` timeouts. Kill them before re-running:
74+
75+
```bash
76+
ps aux | grep -E "[s]tart-cluster|[c]luster/src/(app|agent)_worker" \
77+
| awk '{print $2}' | xargs kill -9
78+
```
79+
4380
## Coding Conventions
4481

4582
- prefer existing repo patterns over inventing new ones

package.json

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,21 @@
1313
],
1414
"type": "module",
1515
"scripts": {
16-
"clean-dist": "ut run clean --workspaces --if-present",
16+
"clean-dist": "node scripts/clean-dist.js",
1717
"build": "tsdown",
1818
"prelint": "ut run clean-dist",
1919
"lint": "oxlint --type-aware --type-check --quiet",
2020
"fmt": "oxfmt",
21-
"typecheck": "ut run clean-dist && ut run typecheck --workspaces --if-present",
21+
"typecheck": "ut run clean-dist && node scripts/run-workspaces.js typecheck",
2222
"fmtcheck": "oxfmt --check .",
23-
"pretest": "ut run clean-dist && ut run pretest --workspaces --if-present",
23+
"pretest": "ut run clean-dist && node scripts/run-workspaces.js pretest",
2424
"test": "vitest run --bail 1 --retry 2 --testTimeout 20000 --hookTimeout 20000",
2525
"test:cov": "ut run test -- --coverage",
26+
"test:shard": "node scripts/run-shard.js",
2627
"benchmark:ci-test": "node scripts/ci-test-benchmark.js",
27-
"preci": "ut run pretest --workspaces --if-present",
28+
"preci": "ut run pretest",
2829
"ci": "ut run test -- --coverage",
30+
"ci:shard": "node scripts/run-shard.js --coverage",
2931
"dev:services:start": "node scripts/dev-services.js start",
3032
"dev:services:stop": "node scripts/dev-services.js stop",
3133
"dev:services:status": "node scripts/dev-services.js status",

0 commit comments

Comments
 (0)