mirror of
https://github.com/multica-ai/multica.git
synced 2026-08-10 14:58:25 +02:00
perf(ci): split frontend build and test onto separate runners (MUL-5347)
The frontend job is the critical path of every CI run that touches web code: p50 576s, versus 267s for backend. 94% of it is a single `turbo build typecheck lint test` invocation. Profiling showed the cost is contention, not inefficient tasks. A standard runner has 4 vCPUs, and the job scheduled two CPU-saturating tasks onto them concurrently: `@multica/views:test` (259 jsdom files) and `@multica/web:build` (a webpack production build). Measured against the same suites running with the machine to themselves: @multica/views:test 104s owning 4 cores -> 500s sharing them @multica/web:build 26s owning 10 cores -> 342s sharing 4 Split the work across two runners so neither starves the other. The split is weighted rather than even, because the halves are not close: the test group costs ~575 CPU-seconds and everything else ~250, so tests get a runner alone. Also drop `^typecheck` from the `test` and `lint` task definitions. No package in this repo emits build artifacts -- core/ui/views export raw .ts that the consumer transpiles -- so that edge ordered tasks without producing anything they consumed, and left the heaviest task in the graph idle for ~38s while core/ui ran `tsc --noEmit`. Removing it also drops three redundant typecheck tasks from the test job, taking it from 7 tasks / 798 CPU-seconds to 4 / 575. Type errors still fail CI through the `typecheck` task, which frontend-build owns. `frontend` is retained as an aggregate gate so the existing status-check name keeps reporting, including the established behaviour that it goes green rather than pending on PRs the path filter excludes. Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
71
.github/workflows/ci.yml
vendored
71
.github/workflows/ci.yml
vendored
@@ -75,7 +75,19 @@ jobs:
|
||||
echo "frontend=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
frontend:
|
||||
# The frontend validation is split across two runners on purpose. Both
|
||||
# `@multica/web:build` (a webpack production build) and `@multica/views:test`
|
||||
# (259 jsdom files) are CPU-saturating, and a standard runner only has
|
||||
# 4 vCPUs. Running them in one job made them starve each other: the views
|
||||
# suite needs ~104s wall when it owns 4 cores but took ~500s sharing them,
|
||||
# and the identical webpack compile went from ~26s to ~342s. Splitting buys
|
||||
# a second 4-vCPU box rather than reducing the work; total runner-minutes go
|
||||
# up slightly, wall-clock feedback time goes down.
|
||||
#
|
||||
# The split is weighted, not even: the test group is by far the heavier half
|
||||
# (~575 CPU-seconds vs ~250 for build + typecheck + lint), so it gets a
|
||||
# runner to itself and everything else shares the other one.
|
||||
frontend-build:
|
||||
needs: changes
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -112,14 +124,67 @@ jobs:
|
||||
pnpm generate:reserved-slugs
|
||||
git diff --exit-code -- packages/core/paths/reserved-slugs.ts
|
||||
|
||||
- name: Build, type check, lint, and test
|
||||
- name: Build, type check, and lint
|
||||
if: ${{ needs.changes.outputs.frontend == 'true' }}
|
||||
# Mobile lives in a parallel mobile-verify workflow (path-filtered
|
||||
# to apps/mobile/** + packages/core/**) so it doesn't add
|
||||
# ~50s of expo-lint + tsc to every web/desktop PR. Keep this
|
||||
# filter in sync with the root package.json scripts, which also
|
||||
# exclude @multica/mobile.
|
||||
run: pnpm exec turbo build typecheck lint test --filter='!@multica/docs' --filter='!@multica/mobile'
|
||||
run: pnpm exec turbo build typecheck lint --filter='!@multica/docs' --filter='!@multica/mobile'
|
||||
|
||||
frontend-test:
|
||||
needs: changes
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
if: ${{ needs.changes.outputs.frontend == 'true' }}
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup pnpm
|
||||
if: ${{ needs.changes.outputs.frontend == 'true' }}
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
if: ${{ needs.changes.outputs.frontend == 'true' }}
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
cache: pnpm
|
||||
|
||||
- name: Install dependencies
|
||||
if: ${{ needs.changes.outputs.frontend == 'true' }}
|
||||
run: pnpm install
|
||||
|
||||
- name: Test
|
||||
if: ${{ needs.changes.outputs.frontend == 'true' }}
|
||||
# Same filter rationale as frontend-build. Type errors are not this
|
||||
# job's responsibility — the `typecheck` task in frontend-build owns
|
||||
# them, which is why `test` no longer depends on `^typecheck`.
|
||||
run: pnpm exec turbo test --filter='!@multica/docs' --filter='!@multica/mobile'
|
||||
|
||||
# Aggregate gate. `frontend` is the status-check name the repository's
|
||||
# branch rules refer to, so it has to survive the split above: this job
|
||||
# keeps reporting under that name and simply fails when either half fails.
|
||||
# It also inherits the old contract that the check goes green (rather than
|
||||
# staying pending) on PRs the path filter excluded — both halves succeed
|
||||
# trivially in that case because every step is gated off.
|
||||
frontend:
|
||||
needs: [frontend-build, frontend-test]
|
||||
if: ${{ always() }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check frontend job results
|
||||
env:
|
||||
BUILD_RESULT: ${{ needs.frontend-build.result }}
|
||||
TEST_RESULT: ${{ needs.frontend-test.result }}
|
||||
run: |
|
||||
echo "frontend-build: $BUILD_RESULT"
|
||||
echo "frontend-test: $TEST_RESULT"
|
||||
if [ "$BUILD_RESULT" != "success" ] || [ "$TEST_RESULT" != "success" ]; then
|
||||
echo "::error::frontend validation failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
backend:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
14
turbo.json
14
turbo.json
@@ -32,11 +32,13 @@
|
||||
"typecheck": {
|
||||
"dependsOn": ["^typecheck"]
|
||||
},
|
||||
"test": {
|
||||
"dependsOn": ["^typecheck"]
|
||||
},
|
||||
"lint": {
|
||||
"dependsOn": ["^typecheck"]
|
||||
}
|
||||
// `test` and `lint` deliberately have no `^typecheck` dependency. No
|
||||
// package emits build artifacts (core/ui/views export raw .ts that the
|
||||
// consumer transpiles), so depending on a dependency's `tsc --noEmit`
|
||||
// bought nothing but head-of-line blocking: the heaviest task in the
|
||||
// graph, `@multica/views:test`, sat idle until core/ui typechecked.
|
||||
// Type errors still fail CI through the `typecheck` task itself.
|
||||
"test": {},
|
||||
"lint": {}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user