Skip to content

Commit 965c58f

Browse files
committed
improvement(ci): scope type checks and right-size hosted runners
1 parent 9499b73 commit 965c58f

6 files changed

Lines changed: 89 additions & 63 deletions

File tree

‎.github/CI.md‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ A PR compares its merge base with the checked-out merge commit. Pushes and norma
66

77
| Change | Expensive checks selected |
88
| --- | --- |
9-
| Independent SDK | That workspace's tests; no app build, services, or browsers |
9+
| Independent SDK | That workspace's tests and type-check; no app build, services, or browsers |
1010
| App unit test | App test shards and test type check |
1111
| Integration test | Both provision paths; no HTTP/browser job merely for the test edit |
1212
| Workspace UI | Mobile and live Electron, plus app tests/build |
@@ -18,7 +18,9 @@ The server boundary deliberately includes all API routes and backend domains. Sh
1818

1919
`affectedTasks` already uses task inputs. Do not enable `futureFlags.affectedUsingTaskInputs` just for this planner: that flag also changes pruning, which can pull CI-only cross-app task dependencies into production Docker contexts.
2020

21-
Lightweight HTTP suites and database integration run on free standard GitHub runners. Memory-heavy builds and browser/Electron jobs retain appropriately sized Blacksmith runners.
21+
Affected type-checks follow the package graph too; repository audits and lint always run. Workspace-only unit jobs use a free GitHub runner and do not start the app test shards.
22+
23+
Lightweight HTTP suites and database integration run on free standard GitHub runners. Memory-heavy builds and browser jobs retain appropriately sized Blacksmith runners. Electron uses 4-vCPU Blacksmith: both shards passed within 14 seconds of the 8-vCPU control; the free GitHub rest shard was slower and its lease run stalled.
2224

2325
Every HTTP and browser group consumes the same production build in an isolated job/database. Electron also uses that build and skips its development-route warm-up. Local HTTP and Electron development runs retain their dev-server behavior. Build inputs exclude test-only files so a harness edit can reuse an unchanged production build.
2426

‎.github/ci-suites.json‎

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@
1919
{
2020
"group": "seo",
2121
"task": "ci:e2e-seo"
22+
},
23+
{
24+
"group": "project-files",
25+
"task": "ci:e2e-project-files"
2226
}
2327
],
2428
"production": [
@@ -53,14 +57,6 @@
5357
"shard": "2/2",
5458
"artifact": "mobile-webkit-2",
5559
"task": "ci:e2e-mobile"
56-
},
57-
{
58-
"group": "project-files",
59-
"name": "project-files",
60-
"browser": "",
61-
"shard": "",
62-
"artifact": "project-files",
63-
"task": "ci:e2e-project-files"
6460
}
6561
]
6662
}

‎.github/scripts/ci-plan.gql‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
query($base: String!, $head: String!) {
22
packages { items { name path } }
33
affectedTasks(base: $base, head: $head, tasks: [
4-
"test", "build", "ci:integration", "ci:desktop-live",
4+
"test", "build", "type-check", "ci:integration", "ci:desktop-live",
55
"ci:e2e-scim", "ci:e2e-cli", "ci:e2e-stop-after", "ci:e2e-desktop-inbox",
66
"ci:e2e-project-files", "ci:e2e-mobile", "ci:e2e-seo"
77
]) {

‎.github/workflows/checks.yml‎

Lines changed: 54 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ jobs:
3636
production_matrix: ${{ steps.classify.outputs.production_matrix }}
3737
test_matrix: ${{ steps.classify.outputs.test_matrix }}
3838
test_packages: ${{ steps.classify.outputs.test_packages }}
39+
typecheck_packages: ${{ steps.classify.outputs.typecheck_packages }}
3940
steps:
4041
- name: Checkout code
4142
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6
@@ -214,9 +215,9 @@ jobs:
214215
--health-interval 5s
215216
--health-timeout 5s
216217
--health-retries 10
217-
# The desktop executor uses Redis.
218+
# Desktop inbox and Project Files use Redis.
218219
redis:
219-
image: redis:8.2-alpine
220+
image: ${{ (matrix.group == 'desktop-inbox' || matrix.group == 'project-files') && 'redis:8.2-alpine' || '' }}
220221
ports:
221222
- 6379:6379
222223
options: >-
@@ -228,6 +229,7 @@ jobs:
228229
DATABASE_URL: postgresql://postgres:postgres@127.0.0.1:5432/sim_test
229230
BETTER_AUTH_SECRET: http-e2e-ci-secret-at-least-32-characters
230231
E2E_APP_SERVER: start
232+
PROJECT_FILES_E2E_APP_SERVER: start
231233
ENCRYPTION_KEY: '0000000000000000000000000000000000000000000000000000000000000000'
232234

233235
steps:
@@ -241,6 +243,7 @@ jobs:
241243

242244
# Migrations create their own extensions, as on a fresh self-hosted install.
243245
- name: Provision the database through migrations
246+
if: matrix.group != 'project-files'
244247
working-directory: packages/db
245248
run: bun run db:migrate
246249

@@ -262,6 +265,17 @@ jobs:
262265
apps/sim/.next/prerender-manifest.json > /dev/null ||
263266
{ echo "::error::The build prerendered posts (a hosted build) or has no prerender manifest."; exit 1; }
264267
268+
# JOBS=4 caps node-gyp parallelism as docker/app.Dockerfile does (laverdet/isolated-vm#428).
269+
- name: Build native isolate addon for Project file rendering
270+
if: matrix.group == 'project-files'
271+
working-directory: apps/sim
272+
env:
273+
JOBS: 4
274+
run: |
275+
addon_dir="$(node -p 'require("node:path").dirname(require.resolve("isolated-vm/package.json", { paths: ["."] }))')"
276+
cd "$addon_dir"
277+
bun run install
278+
265279
# No step timeout: the job's bound covers a hang without cutting a slow but healthy suite
266280
# short of writing its report.
267281
- name: Run end-to-end suites
@@ -286,7 +300,7 @@ jobs:
286300
if-no-files-found: ignore
287301
retention-days: 7
288302

289-
# Browser and Project Files suites reuse the production build, avoiding dev-route compiles.
303+
# Browser suites reuse the production build, avoiding dev-route compiles.
290304
# Each job has its own database and runtime flags; the build reads public env at startup.
291305
# Each browser partitions the full viewport list across two shards, including future additions.
292306
#
@@ -316,15 +330,6 @@ jobs:
316330
--health-interval 5s
317331
--health-timeout 5s
318332
--health-retries 10
319-
redis:
320-
image: ${{ matrix.group == 'project-files' && 'redis:8.2-alpine' || '' }}
321-
ports:
322-
- 6379:6379
323-
options: >-
324-
--health-cmd "redis-cli ping"
325-
--health-interval 5s
326-
--health-timeout 5s
327-
--health-retries 10
328333
env:
329334
DATABASE_URL: postgresql://postgres:postgres@127.0.0.1:5432/sim_test
330335
BETTER_AUTH_SECRET: http-e2e-ci-secret-at-least-32-characters
@@ -338,11 +343,11 @@ jobs:
338343
# setup action's caches.
339344
- &checkout-mirror
340345
name: Checkout code (mirror)
341-
if: github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork
346+
if: (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork
342347
uses: useblacksmith/checkout@25227e61ff9dafe400e22fa487b673eac4e4409a # v1.8.1
343348
- &checkout-plain
344349
name: Checkout code
345-
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork
350+
if: (vars.CI_PROVIDER != '' && vars.CI_PROVIDER != 'blacksmith') || github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork
346351
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6
347352

348353

@@ -352,30 +357,17 @@ jobs:
352357
provider: ${{ vars.CI_PROVIDER }}
353358

354359
- name: Provision the database through migrations
355-
if: matrix.group != 'project-files'
356360
working-directory: packages/db
357361
run: bun run db:migrate
358362

359363
- *download-e2e-build
360364
- *extract-e2e-build
361365

362-
# JOBS=4 caps node-gyp parallelism as docker/app.Dockerfile does (laverdet/isolated-vm#428).
363-
- name: Build native isolate addon for Project file rendering
364-
if: matrix.group == 'project-files'
365-
working-directory: apps/sim
366-
env:
367-
JOBS: 4
368-
run: |
369-
addon_dir="$(node -p 'require("node:path").dirname(require.resolve("isolated-vm/package.json", { paths: ["."] }))')"
370-
cd "$addon_dir"
371-
bun run install
372-
373366
- name: Run end-to-end suites
374367
working-directory: apps/sim
375368
env:
376369
MOBILE_E2E_BROWSER: ${{ matrix.browser }}
377370
MOBILE_E2E_SHARD: ${{ matrix.shard }}
378-
PROJECT_FILES_E2E_APP_SERVER: start
379371
run: bash ../../.github/scripts/http-e2e.sh "${{ matrix.group }}"
380372

381373
- name: Upload end-to-end reports and server logs
@@ -397,13 +389,14 @@ jobs:
397389
retention-days: 7
398390

399391
# Desktop tools exercise the real Electron app against the shared production build.
392+
# Hosted 4-vCPU runs passed both shards within 14 seconds of 8-vCPU; production peak ~4 GB.
400393
# Two runners: the lease-lapse test sleeps through a long import and then a lease (~3 min), as
401394
# long as the rest of the spec together, so on one runner it set the time for every run.
402395
desktop-live:
403396
name: desktop-live (${{ matrix.tests }})
404397
needs: [changes, e2e-build]
405398
if: needs.changes.outputs.desktop_live == 'true'
406-
runs-on: ${{ (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && 'blacksmith-8vcpu-ubuntu-2404' || 'ubuntu-latest' }}
399+
runs-on: ${{ (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }}
407400
timeout-minutes: 30
408401
strategy:
409402
fail-fast: false
@@ -557,6 +550,7 @@ jobs:
557550
# kept off the test shards so neither waits on the other.
558551
lint:
559552
name: lint
553+
needs: changes
560554
runs-on: &runner-4vcpu ${{ (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }}
561555
timeout-minutes: 15
562556

@@ -569,16 +563,14 @@ jobs:
569563
# Worth stating because the failure was invisible for so long: the migration
570564
# audit read the resulting `git diff` failure as "no migrations changed" and
571565
# exited 0, so it had never actually run on a push build.
572-
# Same mirror/plain split as the integration job's checkout.
573-
- &checkout-mirror-depth2
574-
name: Checkout code (mirror)
575-
if: github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork
566+
# Same provider/trust split as the build jobs' checkout.
567+
- name: Checkout code (mirror)
568+
if: (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork
576569
uses: useblacksmith/checkout@25227e61ff9dafe400e22fa487b673eac4e4409a # v1.8.1
577570
with:
578571
fetch-depth: 2
579-
- &checkout-plain-depth2
580-
name: Checkout code
581-
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork
572+
- name: Checkout code
573+
if: (vars.CI_PROVIDER != '' && vars.CI_PROVIDER != 'blacksmith') || github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork
582574
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6
583575
with:
584576
fetch-depth: 2
@@ -689,14 +681,23 @@ jobs:
689681
- name: Migration safety (zero-downtime) audit
690682
run: bun run check:migrations "${{ steps.audit_base.outputs.ref }}"
691683

692-
# Every workspace, not just realtime. packages/emcn, packages/utils,
684+
# Every affected workspace, following the same real task graph as tests. Root/unknown
685+
# changes select all. packages/emcn, packages/utils,
693686
# apps/desktop and apps/docs had no type check in CI at all; apps/sim's
694687
# source was covered only as a side effect of `next build` in the separate
695688
# `build` job. This does NOT cover apps/sim's tests — its tsconfig
696689
# excludes *.test.ts(x), which carry ~2.2k errors; the `test-types` job
697690
# ratchets those per file instead.
698-
- name: Type-check all workspaces
699-
run: bunx turbo run type-check
691+
- name: Type-check affected workspaces
692+
env:
693+
TYPECHECK_PACKAGES: ${{ needs.changes.outputs.typecheck_packages }}
694+
run: |
695+
jq -e 'type == "array" and all(.[]; type == "string")' <<< "$TYPECHECK_PACKAGES" > /dev/null
696+
filters=()
697+
while IFS= read -r package; do filters+=("--filter=$package"); done < <(jq -r '.[]' <<< "$TYPECHECK_PACKAGES")
698+
if [ "${#filters[@]}" -gt 0 ]; then
699+
bunx turbo run type-check "${filters[@]}"
700+
fi
700701
701702
- name: Check schema and migrations are in sync
702703
working-directory: packages/db
@@ -754,21 +755,29 @@ jobs:
754755
name: test (${{ matrix.shard }}/${{ needs.changes.outputs.test_app == 'true' && '2' || '1' }})
755756
needs: changes
756757
if: needs.changes.outputs.test == 'true'
757-
runs-on: ${{ (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && 'blacksmith-8vcpu-ubuntu-2404' || 'ubuntu-latest' }}
758+
runs-on: ${{ needs.changes.outputs.test_app == 'true' && (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && 'blacksmith-8vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
758759
timeout-minutes: 15
759760
strategy:
760761
fail-fast: false
761762
matrix: ${{ fromJSON(needs.changes.outputs.test_matrix) }}
762763

763764

764765
steps:
765-
- *checkout-mirror-depth2
766-
- *checkout-plain-depth2
766+
- name: Checkout code (mirror)
767+
if: needs.changes.outputs.test_app == 'true' && (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork
768+
uses: useblacksmith/checkout@25227e61ff9dafe400e22fa487b673eac4e4409a # v1.8.1
769+
with:
770+
fetch-depth: 2
771+
- name: Checkout code
772+
if: needs.changes.outputs.test_app != 'true' || (vars.CI_PROVIDER != '' && vars.CI_PROVIDER != 'blacksmith') || github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork
773+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6
774+
with:
775+
fetch-depth: 2
767776

768777
- name: Setup workspace
769778
uses: ./.github/actions/setup
770779
with:
771-
provider: ${{ vars.CI_PROVIDER }}
780+
provider: ${{ needs.changes.outputs.test_app != 'true' && 'github' || vars.CI_PROVIDER }}
772781
turbo-cache-key: turbo-cache-test-${{ matrix.shard }}
773782

774783
# cloud-review-tools.test.ts runs the real helper on the runner, which shells
@@ -805,6 +814,7 @@ jobs:
805814
run: |
806815
if [ "$SHARD" = 1 ]; then
807816
bun run test:scripts
817+
jq -e 'type == "array" and all(.[]; type == "string")' <<< "$TEST_PACKAGES" > /dev/null
808818
filters=()
809819
while IFS= read -r package; do filters+=("--filter=$package"); done < <(jq -r '.[]' <<< "$TEST_PACKAGES")
810820
if [ "${#filters[@]}" -gt 0 ]; then
@@ -971,6 +981,8 @@ jobs:
971981
steps:
972982
- name: Checkout code
973983
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6
984+
with:
985+
sparse-checkout: .github/scripts
974986
- name: Require every check to pass
975987
env:
976988
NEEDS: ${{ toJSON(needs) }}

‎scripts/ci-changes.test.ts‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ interface Plan {
1313
fallback?: string
1414
http: { group: string }[]
1515
production: { group: string }[]
16+
typecheckPackages: string[]
1617
testPackages: string[]
1718
}
1819
function git(cwd: string, ...args: string[]) {
@@ -58,7 +59,11 @@ function fixture() {
5859
).scripts
5960
const workspaces: Record<string, unknown> = { '': { name: 'ci-fixture' } }
6061
for (const [path, name, dependencies] of packages) {
61-
const scripts: Record<string, string> = { test: 'echo test', build: 'echo build' }
62+
const scripts: Record<string, string> = {
63+
test: 'echo test',
64+
build: 'echo build',
65+
'type-check': 'echo types',
66+
}
6267
if (name === '@sim/app')
6368
for (const key of Object.keys(appScripts))
6469
if (key.startsWith('ci:')) scripts[key] = 'echo fixture'
@@ -124,6 +129,7 @@ describe('CI task attribution through real Turborepo', () => {
124129
expect(plan.fallback).toBeUndefined()
125130
expect(groups(plan)).toEqual([])
126131
expect(plan.testPackages).toEqual(['simstudio-ts-sdk'])
132+
expect(plan.typecheckPackages).toEqual(['simstudio-ts-sdk'])
127133
expect(plan.flags).toMatchObject({
128134
test: true,
129135
test_app: false,
@@ -194,6 +200,8 @@ describe('CI task attribution through real Turborepo', () => {
194200
const plan = classifyFiles([`apps/sim/scripts/${file}`])
195201
expect(plan.fallback).toBeUndefined()
196202
expect(groups(plan)).toEqual([group])
203+
expect(plan.flags.http).toBe(group !== 'mobile')
204+
expect(plan.flags.production).toBe(group === 'mobile')
197205
expect(plan.flags).toMatchObject({
198206
test: false,
199207
build: false,
@@ -213,6 +221,8 @@ describe('CI task attribution through real Turborepo', () => {
213221
'seo',
214222
'stop-after',
215223
])
224+
expect(plan.typecheckPackages).toContain('@sim/app')
225+
expect(plan.typecheckPackages).toContain('@sim/realtime')
216226
expect(plan.flags).toMatchObject({
217227
integration: true,
218228
desktop_live: true,

‎scripts/ci-plan.ts‎

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -98,15 +98,19 @@ const http = suites.http.filter((suite) => affected(suite.task))
9898
const production = suites.production.filter((suite) => affected(suite.task))
9999
const desktopLive = affected('ci:desktop-live')
100100
const appTest = affected('test')
101-
const packages = fallback
102-
? ['*']
103-
: [
104-
...new Set(
105-
tasks
106-
.filter((task) => task.name === 'test' && task.package.name !== '@sim/app')
107-
.map((task) => task.package.name)
108-
),
109-
].sort()
101+
function taskPackages(name: string, omitApp = false): string[] {
102+
return fallback
103+
? ['*']
104+
: [
105+
...new Set(
106+
tasks
107+
.filter((task) => task.name === name && (!omitApp || task.package.name !== '@sim/app'))
108+
.map((task) => task.package.name)
109+
),
110+
].sort()
111+
}
112+
const packages = taskPackages('test', true)
113+
const typecheckPackages = taskPackages('type-check')
110114
const flags = {
111115
integration: affected('ci:integration'),
112116
http: http.length > 0,
@@ -123,6 +127,7 @@ const outputs = {
123127
production_matrix: JSON.stringify({ include: production }),
124128
test_matrix: JSON.stringify({ shard: appTest ? [1, 2] : [1] }),
125129
test_packages: JSON.stringify(packages),
130+
typecheck_packages: JSON.stringify(typecheckPackages),
126131
}
127132
const report = {
128133
base,
@@ -134,6 +139,7 @@ const report = {
134139
http,
135140
production,
136141
testPackages: packages,
142+
typecheckPackages,
137143
}
138144
if (process.env.CI_PLAN_PATH)
139145
writeFileSync(process.env.CI_PLAN_PATH, `${JSON.stringify(report, null, 2)}\n`)

0 commit comments

Comments
 (0)