Skip to content

Commit fd8616a

Browse files
committed
improvement(ci): use free integration runners and shard browser viewports
1 parent 4924e4c commit fd8616a

3 files changed

Lines changed: 325 additions & 209 deletions

File tree

‎.github/workflows/checks.yml‎

Lines changed: 48 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,12 @@ jobs:
4747
# The two paths build different schemas (migrations add triggers, checks and NOT VALID
4848
# constraints that `db:push` does not), so each runs the whole suite. Vitest splits each
4949
# suite's files across eight shards by measured duration (DurationBalancedSequencer in vitest.shared.ts); a
50-
# shard runs its files one at a time against its own database. Files run serially, so a shard
51-
# barely uses more than one core: two cores leave room for Postgres and Redis.
50+
# shard runs its files one at a time against its own database. Standard GitHub runners are
51+
# free for this public repository and have enough memory for the app, Postgres and Redis.
5252
name: integration (${{ matrix.provision }}, ${{ matrix.shard }}/8)
5353
needs: changes
5454
if: needs.changes.outputs.full == 'true'
55-
runs-on: *runner-2vcpu
55+
runs-on: ubuntu-24.04
5656
timeout-minutes: 20
5757
strategy:
5858
fail-fast: false
@@ -105,24 +105,13 @@ jobs:
105105
ENCRYPTION_KEY: '0000000000000000000000000000000000000000000000000000000000000000'
106106

107107
steps:
108-
# A same-repository pull request checks out through a git mirror on a sticky disk, so a slow
109-
# clone from GitHub (1 in 10 took a minute or more, up to 4) no longer sets the run's pace.
110-
# The mirror is one disk per repository that job steps can write to, so fork pull requests
111-
# and pushes, whose checks gate a deploy, keep the plain checkout: same trust split as the
112-
# setup action's caches.
113-
- &checkout-mirror
114-
name: Checkout code (mirror)
115-
if: github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork
116-
uses: useblacksmith/checkout@25227e61ff9dafe400e22fa487b673eac4e4409a # v1.8.1
117-
- &checkout-plain
118-
name: Checkout code
119-
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork
108+
- name: Checkout code
120109
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6
121110

122111
- name: Setup workspace
123112
uses: ./.github/actions/setup
124113
with:
125-
provider: ${{ vars.CI_PROVIDER }}
114+
provider: github
126115

127116
- name: Provision a fresh database through the supported command
128117
working-directory: packages/db
@@ -212,7 +201,7 @@ jobs:
212201
--health-interval 5s
213202
--health-timeout 5s
214203
--health-retries 10
215-
# Project file subscriptions and the desktop executor use Redis.
204+
# The desktop executor uses Redis.
216205
redis:
217206
image: redis:8.2-alpine
218207
ports:
@@ -228,8 +217,19 @@ jobs:
228217
ENCRYPTION_KEY: '0000000000000000000000000000000000000000000000000000000000000000'
229218

230219
steps:
231-
- *checkout-mirror
232-
- *checkout-plain
220+
# A same-repository pull request checks out through a git mirror on a sticky disk, so a slow
221+
# clone from GitHub (1 in 10 took a minute or more, up to 4) no longer sets the run's pace.
222+
# The mirror is one disk per repository that job steps can write to, so fork pull requests
223+
# and pushes, whose checks gate a deploy, keep the plain checkout: same trust split as the
224+
# setup action's caches.
225+
- &checkout-mirror
226+
name: Checkout code (mirror)
227+
if: github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork
228+
uses: useblacksmith/checkout@25227e61ff9dafe400e22fa487b673eac4e4409a # v1.8.1
229+
- &checkout-plain
230+
name: Checkout code
231+
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork
232+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6
233233

234234
- name: Setup workspace
235235
uses: ./.github/actions/setup
@@ -258,6 +258,7 @@ jobs:
258258

259259
# Browser and Project Files suites reuse the production build, avoiding dev-route compiles.
260260
# Each job has its own database and runtime flags; the build reads public env at startup.
261+
# Each browser partitions the full viewport list across two shards, including future additions.
261262
#
262263
# 8 vCPU: `next start`, Postgres, the realtime server and one Playwright browser would fit a 4
263264
# vCPU runner's 16 GB, but the suite is timing-sensitive, and CPU contention between the browser
@@ -273,16 +274,29 @@ jobs:
273274
matrix:
274275
include:
275276
- group: mobile
276-
name: mobile, chromium
277+
name: mobile, chromium, 1/2
278+
browser: chromium
279+
shard: '1/2'
280+
artifact: mobile-chromium-1
281+
- group: mobile
282+
name: mobile, chromium, 2/2
277283
browser: chromium
278-
artifact: mobile-chromium
284+
shard: '2/2'
285+
artifact: mobile-chromium-2
279286
- group: mobile
280-
name: mobile, webkit
287+
name: mobile, webkit, 1/2
281288
browser: webkit
282-
artifact: mobile-webkit
289+
shard: '1/2'
290+
artifact: mobile-webkit-1
291+
- group: mobile
292+
name: mobile, webkit, 2/2
293+
browser: webkit
294+
shard: '2/2'
295+
artifact: mobile-webkit-2
283296
- group: project-files
284297
name: project-files
285298
browser: ''
299+
shard: ''
286300
artifact: project-files
287301
services:
288302
postgres:
@@ -357,18 +371,28 @@ jobs:
357371
working-directory: apps/sim
358372
env:
359373
MOBILE_E2E_BROWSER: ${{ matrix.browser }}
374+
MOBILE_E2E_SHARD: ${{ matrix.shard }}
360375
PROJECT_FILES_E2E_APP_SERVER: start
361376
run: bash ../../.github/scripts/http-e2e.sh "${{ matrix.group }}"
362377

363378
- name: Upload end-to-end reports and server logs
364-
if: always()
379+
if: failure()
365380
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
366381
with:
367382
name: http-e2e-${{ matrix.artifact }}
368383
path: ${{ runner.temp }}/e2e/
369384
if-no-files-found: ignore
370385
retention-days: 7
371386

387+
- name: Upload acceptance timing and coverage reports
388+
if: always()
389+
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
390+
with:
391+
name: e2e-reports-${{ matrix.artifact }}
392+
path: ${{ runner.temp }}/e2e/**/*.json
393+
if-no-files-found: ignore
394+
retention-days: 7
395+
372396
# Desktop tools in the real Electron app against a local app, on its own runner: the
373397
# Electron app, the dev app and its realtime server together outgrow the e2e runner.
374398
# Two runners: the lease-lapse test sleeps through a long import and then a lease (~3 min), as

‎apps/sim/scripts/test-mobile-e2e.ts‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import postgres from 'postgres'
2121
* Requires MOBILE_E2E_BASE_URL, MOBILE_E2E_DATABASE_URL, MOBILE_E2E_AUTH_SECRET,
2222
* and MOBILE_E2E_REPORT_PATH. Install Chromium and WebKit with Playwright first.
2323
* MOBILE_E2E_BROWSER and MOBILE_E2E_VIEWPORT narrow a diagnostic rerun.
24+
* MOBILE_E2E_SHARD (index/count, one-based) partitions all viewports across CI jobs.
2425
*/
2526
const logger = createLogger('MobileE2E')
2627
const expect = browserExpect.configure({ timeout: 20_000 })
@@ -104,6 +105,12 @@ assert(
104105
!selectedViewport || viewports.some((viewport) => viewport.name === selectedViewport),
105106
'Unknown viewport'
106107
)
108+
const shard = /^([1-9]\d*)\/([1-9]\d*)$/.exec(process.env.MOBILE_E2E_SHARD || '1/1')
109+
assert(shard, 'MOBILE_E2E_SHARD must be index/count, one-based')
110+
const shardIndex = Number(shard[1]) - 1
111+
const shardCount = Number(shard[2])
112+
assert(shardIndex < shardCount && shardCount <= viewports.length, 'Invalid viewport shard')
113+
assert(!selectedViewport || shardCount === 1, 'Select either a viewport or a shard')
107114

108115
interface CheckResult {
109116
name: string
@@ -1278,7 +1285,8 @@ try {
12781285
})
12791286
if (!browser) continue
12801287
try {
1281-
for (const viewport of viewports) {
1288+
for (const [index, viewport] of viewports.entries()) {
1289+
if (index % shardCount !== shardIndex) continue
12821290
if (selectedViewport && selectedViewport !== viewport.name) continue
12831291
await exerciseViewport(browser, browserName, cookie, viewport)
12841292
}

0 commit comments

Comments
 (0)