-
Notifications
You must be signed in to change notification settings - Fork 119
862 lines (791 loc) · 42.3 KB
/
Copy pathci.yml
File metadata and controls
862 lines (791 loc) · 42.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
name: CI
on:
push:
branches: [main]
pull_request:
# Stacked PRs target another feature branch and need the same checks.
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
# Which jobs are worth running for this diff. Most filters carry the `base` anchor.
#
# Push events filter too. paths-filter compares `github.event.before..github.sha`
# there, which spans the whole push — correct for squash-merges, for direct
# pushes to main, and for the `Merge branch 'main'` commits this history carries.
# A main push therefore gets exactly the guarantee a PR gets. This used to force
# every output true on push, which cost ~40% of CI's monthly runner spend re-running
# the full suite for diffs that could not have affected it.
changes:
name: Detect changes
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
ts: ${{ steps.filter.outputs.ts == 'true' }}
tests: ${{ steps.tests.outputs.matrix }}
cli: ${{ steps.filter.outputs.cli == 'true' }}
archive: ${{ steps.filter.outputs.archive == 'true' }}
web: ${{ steps.filter.outputs.web == 'true' }}
clickhouse: ${{ steps.filter.outputs.clickhouse == 'true' }}
postgres: ${{ steps.filter.outputs.postgres == 'true' }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6
- uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4.0.3
id: filter
with:
filters: |
base: &base
- 'bun.lock'
- 'package.json'
- 'turbo.json'
- 'tsconfig*.json'
- '.oxlintrc*.json'
- 'mise.toml'
- '.github/workflows/ci.yml'
- '.github/scripts/*tests*'
- '.github/actions/**'
ts:
- *base
- 'alchemy.run.ts'
- 'apps/**'
- 'examples/**'
- 'packages/**'
- 'lib/**'
- 'scripts/**'
- '!apps/ingest/**'
# Swift; built by ios.yml. Note the asymmetry: a
# packages/domain change still triggers ios:openapi:check
# below, so an iOS contract break fails on the cheap
# Linux runner rather than a macOS one.
- '!apps/ios/**'
# The workspace-dependency closure of apps/cli + apps/local-ui
# (plus what alchemy:build-deps builds): cli → effect-sdk,
# domain, query-engine; local-ui → infra, query-engine, ui;
# domain/query-engine → effect-clickhouse; effect-sdk /
# @maple-dev/browser → browser-session. The old blanket
# `lib/**` + `packages/**` ran this job and all seven archive
# legs for changes to packages the bundle never links (db,
# auth, email, …). Update this list when those package.json
# workspace deps change.
cli:
- *base
- 'apps/cli/**'
- 'apps/local-ui/**'
- 'packages/effect-sdk/**'
- 'packages/domain/**'
- 'packages/query-engine/**'
- 'packages/infra/**'
- 'packages/ui/**'
- 'packages/browser/**'
- 'packages/browser-session/**'
- 'scripts/build-local-binary.sh'
- 'scripts/gen-ui-embed.ts'
# The archive probes test the CLI's own on-disk archive
# format: crash recovery, merge, GC, retention. The `cli`
# closure above is the right gate for "does the bundle still
# build and open a store", which `local-checkpoint-native`
# answers in one job. It is the wrong gate for these: measured
# over 60 merged PRs, `cli` fired on 78% but only 20% touched
# apps/cli at all — the rest were query-engine and domain edits
# dragging seven crash-recovery legs along with them, 38% of the
# repo's entire runner spend. Anything that changes archive
# behaviour lives in one of these paths (`base` included: a
# lockfile change can move chdb).
archive:
- *base
- 'apps/cli/**'
- 'apps/local-ui/**'
- 'scripts/build-local-binary.sh'
- 'scripts/gen-ui-embed.ts'
web:
- *base
- 'apps/web/**'
- 'packages/ui/**'
- 'packages/domain/**'
- 'packages/query-engine/**'
clickhouse:
- *base
- 'lib/effect-clickhouse-http/**'
- 'apps/api/scripts/query-bench/**'
- 'packages/backend/src/services/warehouse/**'
- 'packages/db/**'
- 'scripts/*clickhouse*'
# The SQL catalog sweep lives in this job, so a change to a
# query definition or to the warehouse schema has to trigger
# it. Without these two the PR that broke every main chart
# would still skip the only job that could have caught it.
- 'packages/query-engine/**'
- 'packages/domain/src/clickhouse/**'
- 'packages/domain/src/tinybird/**'
# The migrations this job replays, and the datasource
# definitions the generated DDL is derived from. Without
# these a PR that adds a migration only triggers the job
# if it incidentally touches `base` (e.g. bun.lock) —
# which is exactly how a schema change can merge without
# ever being replayed against a real ClickHouse.
- 'packages/domain/src/clickhouse/**'
- 'packages/domain/src/tinybird/**'
- 'packages/domain/src/generated/**'
postgres:
- *base
# The connection layer and everything that decides how many
# sockets a request opens. The unit suite replaces the dial
# with a fake and the rest of the api suite runs on PGlite,
# so this job is the ONLY place `layerPg` meets a real
# server — narrow these and the per-request socket
# guarantee stops being checked.
- 'packages/backend/src/platform/**'
- 'apps/api/test/integration/**'
- 'apps/api/vitest.integration.config.ts'
- 'packages/db/**'
shell:
- *base
- 'scripts/*.sh'
- 'apps/cli/test/*.sh'
- name: Plan test lanes and verify scaling
id: tests
run: |
python3 -m unittest discover -s .github/scripts -p 'test_plan_tests.py'
echo "matrix=$(python3 .github/scripts/plan-tests.py)" >> "$GITHUB_OUTPUT"
# Shell linting lives here rather than in its own job: it needs only the
# checkout this job already has — no bun, no mise, no install — so a
# dedicated runner was ~90% setup for ~4s of work. shellcheck is
# preinstalled on the runner image.
- name: shellcheck
if: ${{ steps.filter.outputs.shell == 'true' }}
run: >-
shellcheck scripts/install.sh scripts/uninstall.sh scripts/build-local-binary.sh
apps/cli/test/native-checkpoint-smoke.sh
apps/cli/test/native-checkpoint-refresh-probe.sh
apps/cli/test/native-local-store-migration.sh
apps/cli/test/native-archive-smoke.sh
apps/cli/test/native-archive-crash-recovery-probe.sh
apps/cli/test/native-archive-adversarial-probe.sh
apps/cli/test/native-archive-gc-probe.sh
apps/cli/test/native-archive-merge-probe.sh
apps/cli/test/native-retention-probe.sh
# Syntax-check the POSIX installers against the actual /bin/sh they claim.
- name: sh -n
if: ${{ steps.filter.outputs.shell == 'true' }}
run: |
sh -n scripts/install.sh
sh -n scripts/uninstall.sh
typescript:
name: TypeScript (${{ matrix.shard }})
needs: changes
if: ${{ needs.changes.outputs.ts == 'true' }}
# Builds and typechecks retain independent caches. Tests have their own
# automatically sized matrix below so workspace moves cannot strand them.
# Shards are sized to ~1-2 minutes of work: each one pays ~30s of setup,
# and the org runs ~16 jobs at once, so short shards queued the whole run.
runs-on: ubuntu-latest
timeout-minutes: 12
strategy:
fail-fast: false
matrix:
include:
# Every Worker: the root alchemy.run.ts imports each src/worker.ts,
# so the Alchemy-entrypoints typecheck resolves each graph through
# that app's node_modules.
# Knip needs the full install, which covers the Workers too.
- shard: quality
install-filters: ""
- shard: effect-lint
install-filters: ""
- shard: web
install-filters: "@maple/web"
- shard: build-other
install-filters: >-
@maple-dev/alchemy @maple-dev/browser
@maple-dev/effect-sdk
@maple/landing @maple/local-ui
- shard: typecheck-core
install-filters: "@maple/api ./packages/*"
- shard: typecheck-rest
install-filters: >-
@maple/ai @maple/alerting @maple/chat-bot @maple/cli
@maple/clickhouse-builder-docs
@maple/electric-sync @maple/landing @maple/local-ui
@maple/sandbox @maple/scraper ./lib/* ./examples/*
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6
- uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4.3.0
with:
# Rust and Python are for the ingest and Tinybird workflows; skipping
# them keeps this job off their ~1 GB of toolchain cache.
install_args: bun node
- uses: ./.github/actions/bun-install
with:
filters: ${{ matrix.install-filters }}
# The append-only schema-history gate below reads the base branch's
# history file through `git show origin/<base>:...`. checkout leaves a
# narrow refspec behind, so name the destination explicitly — a plain
# `git fetch origin <branch>` can leave refs/remotes/origin/<branch>
# absent, which the gate would read as "no base history yet".
- name: Fetch base branch for the append-only schema gate
if: ${{ matrix.shard == 'quality' && github.event_name == 'pull_request' }}
run: |
git fetch --no-tags --depth=1 origin \
"+refs/heads/$GITHUB_BASE_REF:refs/remotes/origin/$GITHUB_BASE_REF"
# One turbo cache shared by every job and workflow, served from the
# Actions cache one artifact at a time. A result any job has produced
# (the effect-sdk build every test lane depends on, say) is a hit for
# all the others; per-job tarballs rebuilt it in each of them.
- name: Turbo remote cache
uses: rharkor/caching-for-turbo@2238fae6eb9a9936f92356f54cb3660200d105e7 # v2.5.1
# Alchemy entrypoints reach Workers whose SDK and Alchemy imports
# need declarations built from the workspace sources.
- name: Build the libraries the quality checks import
if: ${{ matrix.shard == 'quality' }}
run: >-
bun turbo build
--filter=@maple-dev/effect-sdk --filter=@maple-dev/alchemy
- name: Validate generated ClickHouse and local-store schemas
if: ${{ matrix.shard == 'quality' }}
run: bun run clickhouse:schema:check
# The Swift client is generated from a committed, pruned copy of the v2
# OpenAPI document. Checking it here means a contract change that breaks
# the iOS client fails on Linux, not on a 10x-priced macOS runner.
- name: Validate the generated iOS OpenAPI spec
if: ${{ matrix.shard == 'quality' }}
run: bun run ios:openapi:check
# Shared design tokens live in packages/ui/src/styles/tokens.css;
# fails if an app re-declares one (landing ↔ web drift guard).
- name: Validate shared design tokens
if: ${{ matrix.shard == 'quality' }}
run: bun run check:tokens
# Type-aware lint resolves the three workspace packages whose exports point
# at dist/. Build them before linting so Effect requirement/error channels
# cannot silently degrade to `any` through missing declarations.
- name: Build workspace libraries for type-aware Effect lint
if: ${{ matrix.shard == 'effect-lint' }}
run: >-
bun turbo build --filter=@maple-dev/effect-sdk
--filter=@maple-dev/alchemy
--filter=@maple-dev/browser --concurrency=2
- name: Lint TypeScript and Effect code
if: ${{ matrix.shard == 'effect-lint' }}
run: bun run lint
- name: Build web production bundle
if: ${{ matrix.shard == 'web' }}
run: bun turbo build --filter=@maple/web --concurrency=2
- name: Build remaining production bundles
if: ${{ matrix.shard == 'build-other' }}
run: >-
bun turbo build --filter=@maple-dev/alchemy
--filter=@maple-dev/browser
--filter=@maple-dev/effect-sdk --filter=@maple/landing
--filter=@maple/local-ui --concurrency=2
# Guard what installing @maple-dev/browser costs a page. Splits the
# bundled+minified+gzipped graph into what every page load pays and
# what only replay-sampled visitors pay, and fails on the budgets in
# scripts/size.ts. Without this an eager-graph regression is only
# discoverable in a customer's Lighthouse report — which is how a
# bundled `effect/Schema` once took the eager side 53% over budget.
- name: Browser SDK bundle budget
if: ${{ matrix.shard == 'build-other' }}
run: bun run size
working-directory: packages/browser
# `turbo typecheck` does not cover tsconfig.alchemy.json (alchemy.run.ts,
# the ECS factories and every Worker module it imports) — only the root
# `typecheck` script chains it, and CI calls turbo directly.
- name: Typecheck Alchemy entrypoints
if: ${{ matrix.shard == 'quality' }}
run: bunx tsc -p tsconfig.alchemy.json
# Guard the code-split web bundle: initial-load gzip budget and no
# chat/replay code in the startup graph. Reads the dist/ the turbo
# build step just produced (or restored from cache).
- name: Web bundle budget
if: ${{ matrix.shard == 'web' }}
run: bun run check:bundle
working-directory: apps/web
# Two Turbo tasks at a time bounds how many tsc heaps are live at once.
- name: Typecheck web
if: ${{ matrix.shard == 'web' }}
run: bun turbo typecheck --filter=@maple/web --concurrency=2
- name: Typecheck remaining apps, libraries, and examples
if: ${{ matrix.shard == 'typecheck-rest' }}
run: >-
bun turbo typecheck --filter='./apps/*' --filter='./lib/*'
--filter='./examples/*' --filter='!@maple/api'
--filter='!@maple/web' --filter='!@maple/ingest' --concurrency=2
- name: Typecheck API and packages
if: ${{ matrix.shard == 'typecheck-core' }}
run: >-
bun turbo typecheck --filter=@maple/api --filter='./packages/*'
--concurrency=2
- name: Find unused files and dependencies
if: ${{ matrix.shard == 'quality' }}
run: bun knip
tests:
name: Tests (${{ matrix.name }})
needs: changes
if: ${{ needs.changes.outputs.ts == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 8
strategy:
fail-fast: false
matrix: ${{ fromJSON(needs.changes.outputs.tests) }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6
- uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4.3.0
with:
# Rust and Python are for the ingest and Tinybird workflows; skipping
# them keeps this job off their ~1 GB of toolchain cache.
install_args: bun node
- uses: ./.github/actions/bun-install
with:
filters: ${{ join(matrix.filters, ' ') }}
- name: Read Playwright version
if: matrix.browser-workspace != ''
id: test-playwright
working-directory: ${{ matrix.browser-workspace }}
run: echo "version=$(bun -e 'console.log(require("playwright/package.json").version)')" >> "$GITHUB_OUTPUT"
- name: Cache Chromium
if: matrix.browser-workspace != ''
id: test-browser-cache
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ steps.test-playwright.outputs.version }}-chromium
# As in the performance job, ubuntu-latest supplies Chromium's shared
# libraries. Install the pinned browser; don't repeat apt on every lane.
- name: Install Chromium
if: matrix.browser-workspace != '' && steps.test-browser-cache.outputs.cache-hit != 'true'
working-directory: ${{ matrix.browser-workspace }}
run: bun run playwright install chromium
- name: Turbo remote cache
uses: rharkor/caching-for-turbo@2238fae6eb9a9936f92356f54cb3660200d105e7 # v2.5.1
- name: Run bounded test lane
timeout-minutes: 5
env:
TEST_LANE: ${{ toJSON(matrix) }}
run: python3 .github/scripts/run-tests.py
# The only job where `layerPg` meets a real Postgres. Everything else that
# touches the Database service runs on PGlite (in-process, single-connection,
# marks connected immediately), and the connection-scope unit tests inject a
# fake dial — so "one socket per request" is asserted here against
# pg_stat_activity, or nowhere. Port 5499 matches
# docker-compose.development.yml so the same command works locally after
# `bun db:up`.
postgres-connection-e2e:
name: Postgres connection scope E2E
needs: changes
if: ${{ needs.changes.outputs.postgres == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 15
services:
postgres:
image: postgres:17-alpine
env:
POSTGRES_USER: maple
POSTGRES_PASSWORD: maple
POSTGRES_DB: maple
ports:
- 5499:5432
options: >-
--health-cmd "pg_isready -U maple -d maple"
--health-interval 5s
--health-timeout 5s
--health-retries 20
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6
- uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4.3.0
with:
# Rust and Python are for the ingest and Tinybird workflows; skipping
# them keeps this job off their ~1 GB of toolchain cache.
install_args: bun node
- uses: ./.github/actions/bun-install
- name: Turbo remote cache
uses: rharkor/caching-for-turbo@2238fae6eb9a9936f92356f54cb3660200d105e7 # v2.5.1
# No migrations: the suite creates the one scratch table it needs and
# drops it. Keeping it schema-independent means a migration change can
# never make this job red for a reason unrelated to connections.
- name: Connection scope against a real Postgres
env:
MAPLE_TEST_PG_URL: postgres://maple:maple@127.0.0.1:5499/maple
run: bun run --cwd apps/api test:integration
# Single version, not a matrix. Two legs cost ~380s of setup each to run ~50s of
# actual work, so the second version was ~88% overhead. Pinned to the same tag as
# docker-compose.development.yml so local dev and CI verify the same server.
clickhouse-schema-e2e:
name: ClickHouse schema + SQL catalog E2E
needs: changes
if: ${{ needs.changes.outputs.clickhouse == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 20
services:
clickhouse:
image: clickhouse/clickhouse-server:26.2
env:
CLICKHOUSE_USER: maple
CLICKHOUSE_PASSWORD: maple
CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT: "1"
ports:
- 8123:8123
options: >-
--health-cmd "clickhouse-client --user maple --password maple --query 'SELECT 1'"
--health-interval 5s
--health-timeout 5s
--health-retries 20
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6
- uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4.3.0
with:
# Rust and Python are for the ingest and Tinybird workflows; skipping
# them keeps this job off their ~1 GB of toolchain cache.
install_args: bun node
- uses: ./.github/actions/bun-install
- name: Native ClickHouse HTTP protocol and live wire-format tests
env:
CLICKHOUSE_HTTP_LIVE: "1"
CLICKHOUSE_HTTP_URL: http://127.0.0.1:8123
CLICKHOUSE_HTTP_USER: maple
CLICKHOUSE_HTTP_PASSWORD: maple
run: bun run --cwd lib/effect-clickhouse-http test
- name: Turbo remote cache
uses: rharkor/caching-for-turbo@2238fae6eb9a9936f92356f54cb3660200d105e7 # v2.5.1
- name: Replay migrations and verify search indexes
env:
CLICKHOUSE_E2E: "1"
CLICKHOUSE_E2E_URL: http://127.0.0.1:8123
CLICKHOUSE_E2E_USER: maple
CLICKHOUSE_E2E_PASSWORD: maple
run: >-
bun run --filter=@maple/backend test --
src/services/warehouse/WarehouseQueryService.clickhouse.e2e.test.ts
# Type-checks every SQL shape the product can emit against the real
# analyzer. ~80 shapes, ~2s — the unit tests only compare SQL text,
# which is how a NO_COMMON_TYPE reached production with CI green.
- name: Analyze the SQL catalog
env:
CLICKHOUSE_E2E: "1"
CLICKHOUSE_E2E_URL: http://127.0.0.1:8123
CLICKHOUSE_E2E_USER: maple
CLICKHOUSE_E2E_PASSWORD: maple
run: >-
bun run --filter=@maple/api test --
scripts/query-bench/catalog.clickhouse.e2e.test.ts
# The catalog sweep proves the rollup SQL parses; this proves it
# AGREES with the raw path it replaced. A divergence here renders as a
# plausible-looking wrong number, so nothing downstream would catch it.
- name: Verify web analytics raw-vs-rollup parity
env:
CLICKHOUSE_E2E: "1"
CLICKHOUSE_E2E_URL: http://127.0.0.1:8123
CLICKHOUSE_E2E_USER: maple
CLICKHOUSE_E2E_PASSWORD: maple
run: >-
bun run --filter=@maple/backend test --
src/services/warehouse/web-analytics-parity.clickhouse.e2e.test.ts
# A cascaded rollup that never fires reads as "no data", not as an error.
- name: Verify the trace facets rollup
env:
CLICKHOUSE_E2E: "1"
CLICKHOUSE_E2E_URL: http://127.0.0.1:8123
CLICKHOUSE_E2E_USER: maple
CLICKHOUSE_E2E_PASSWORD: maple
run: >-
bun run --filter=@maple/backend test --
src/services/warehouse/trace-facets-hourly-materialization.clickhouse.e2e.test.ts
local-checkpoint-native:
name: Local checkpoint native
needs: changes
if: ${{ needs.changes.outputs.cli == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6
- uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4.3.0
- uses: ./.github/actions/bun-install
- name: Turbo remote cache
uses: rharkor/caching-for-turbo@2238fae6eb9a9936f92356f54cb3660200d105e7 # v2.5.1
- name: Build native local bundle
timeout-minutes: 5
run: scripts/build-local-binary.sh
- name: Run repeated checkpoint restore and fresh-process reopen smoke
timeout-minutes: 4
env:
KEEP_ROOT: "1"
TMPDIR: ${{ runner.temp }}/maple-native-checkpoint
# Probes induce failures on purpose; keep them out of the prod errors hub.
MAPLE_TELEMETRY: off
run: |
mkdir -p "$TMPDIR"
apps/cli/test/native-checkpoint-smoke.sh "$GITHUB_WORKSPACE/dist"
# The crash path the checkpoint smoke does not cover: a FRESH store
# that accumulates telemetry and then dies to a SIGKILL, which is the
# journey that stranded people with a dirty store and nothing to
# restore from.
- name: Run native checkpoint refresh and crash-recovery probe
timeout-minutes: 5
env:
KEEP_ROOT: "1"
TMPDIR: ${{ runner.temp }}/maple-native-cp-refresh
MAPLE_TELEMETRY: off
run: |
mkdir -p "$TMPDIR"
apps/cli/test/native-checkpoint-refresh-probe.sh "$GITHUB_WORKSPACE/dist"
- name: Run native local-store migration and reopen probe
timeout-minutes: 4
env:
KEEP_ROOT: "1"
TMPDIR: ${{ runner.temp }}/maple-native-migration
MAPLE_TELEMETRY: off
run: |
mkdir -p "$TMPDIR"
apps/cli/test/native-local-store-migration.sh "$GITHUB_WORKSPACE/dist"
# failure() alone skips the upload when a step-level timeout or a
# concurrency cancel kills the probe mid-hang — precisely the runs
# whose evidence matters most.
- name: Upload checkpoint failure evidence
if: ${{ failure() || cancelled() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: native-checkpoint-failure-${{ github.run_id }}
# Both probes' roots: the migration probe used to fail without
# leaving any evidence because only the checkpoint dir was
# collected.
path: |
${{ runner.temp }}/maple-native-checkpoint
${{ runner.temp }}/maple-native-migration
if-no-files-found: ignore
local-archive-native:
# Was a single 7-probe job on ubuntu-latest: ~355s of probes end to end, 7m
# typical and 21m at worst — the wall-clock critical path for all of CI. The
# probes are independent, so they fan out; wall clock is now the slowest single
# probe (crash-recovery, ~125s) plus setup.
#
# Each leg rebuilds the bundle rather than sharing one via upload-artifact:
# dist/ is ~403MB (libchdb.so 334MB + maple 69MB), and build-local-binary.sh
# takes ~14s including the libchdb download. Uploading once and downloading
# seven times is strictly slower.
name: Local archive native (${{ matrix.leg }})
needs: changes
# Narrower than the `cli` closure that gates local-checkpoint-native — see the
# `archive` filter. A change that only reaches the bundle through the workspace
# dependency graph gets the checkpoint job's build + store-open probes; it does
# not get all the archive legs.
if: ${{ needs.changes.outputs.archive == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 20
# The two long probes get a leg each. The four short ones (7-27s each)
# share one: alone, each paid ~50s of install and bundle build for seconds
# of work, and the extra legs queued behind the org's concurrency limit.
# Each entry is `script:tmp-dir:KEEP_ROOT`; adversarial leaves KEEP_ROOT
# empty on purpose, and every probe reads "${KEEP_ROOT:-0}" == "1".
strategy:
fail-fast: false
matrix:
include:
- leg: crash-recovery
probes: native-archive-crash-recovery-probe.sh:maple-native-archive-recovery:1
- leg: gc
probes: native-archive-gc-probe.sh:maple-native-archive-gc:1
- leg: quick
probes: >-
native-archive-smoke.sh:maple-native-archive:1
native-archive-adversarial-probe.sh:maple-native-archive-adversarial:
native-archive-merge-probe.sh:maple-native-archive-merge:1
native-retention-probe.sh:maple-native-retention:1
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6
- uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4.3.0
- uses: ./.github/actions/bun-install
# Every leg runs a probe that reads archives with DuckDB; installing it is ~2s.
- name: Install pinned DuckDB CLI
run: |
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors \
https://github.com/duckdb/duckdb/releases/download/v1.5.3/duckdb_cli-linux-amd64.gz \
-o "$RUNNER_TEMP/duckdb.gz"
echo "f05f3b448a9a1bc6e7ac27ff14dfe67bf5761b153c2002723365a456618ef35b $RUNNER_TEMP/duckdb.gz" \
| sha256sum --check --strict
gunzip "$RUNNER_TEMP/duckdb.gz"
chmod +x "$RUNNER_TEMP/duckdb"
echo "$RUNNER_TEMP" >> "$GITHUB_PATH"
- name: Build native local bundle
run: scripts/build-local-binary.sh
# Every probe in the leg runs even after one fails, so a red leg reports
# all of its failures at once.
- name: Run archive ${{ matrix.leg }} probes
env:
PROBES: ${{ matrix.probes }}
EVIDENCE: ${{ runner.temp }}/archive-${{ matrix.leg }}
# Probes induce failures on purpose; keep them out of the prod errors hub.
MAPLE_TELEMETRY: off
run: |
failed=()
read -r -a probes <<< "$PROBES"
for probe in "${probes[@]}"; do
IFS=: read -r script tmp keep_root <<< "$probe"
echo "::group::$script"
mkdir -p "$EVIDENCE/$tmp"
if ! KEEP_ROOT="$keep_root" TMPDIR="$EVIDENCE/$tmp" \
"apps/cli/test/$script" "$GITHUB_WORKSPACE/dist"; then
failed+=("$script")
fi
echo "::endgroup::"
done
if (( ${#failed[@]} )); then
echo "::error::Failed archive probes: ${failed[*]}"
exit 1
fi
# The artifact name must carry the leg: legs run concurrently and would
# otherwise collide on a single name, failing the upload.
- name: Upload archive failure evidence
if: ${{ failure() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: native-archive-failure-${{ matrix.leg }}-${{ github.run_id }}
path: ${{ runner.temp }}/archive-${{ matrix.leg }}
if-no-files-found: ignore
service-map-perf:
name: Web perf (${{ matrix.shard }})
needs: changes
if: ${{ needs.changes.outputs.web == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 12
# One Playwright worker per machine (playwright.config.ts) so concurrent
# browsers cannot distort frame timings. service-map (~125s of tests) is split
# across two machines and the two non-Chromium smokes get one each; the short
# Chromium groups (~25-35s each) run back to back on one, still one browser
# at a time, instead of each paying ~45s of install and dependency build.
strategy:
fail-fast: false
matrix:
include:
# The spec runs its tests in parallel mode, which lets --shard
# split them; with one worker they still run one at a time.
- shard: service-map-1
browser: chromium
projects: chromium-performance
specs: perf/service-map.perf.spec.ts
playwright-args: --shard=1/2
- shard: service-map-2
browser: chromium
projects: chromium-performance
specs: perf/service-map.perf.spec.ts
playwright-args: --shard=2/2
# The projects split by the @cross-browser tag, so their union
# runs each test exactly once.
- shard: chromium
browser: chromium
projects: chromium-performance chromium-smoke
specs: >-
perf/infra.perf.spec.ts perf/logs.perf.spec.ts
perf/logs-hover.spec.ts perf/service-detail.perf.spec.ts
perf/cross-browser.perf.spec.ts
- shard: firefox-smoke
browser: firefox
projects: firefox-smoke
specs: perf/cross-browser.perf.spec.ts
- shard: webkit-smoke
browser: webkit
projects: webkit-smoke
specs: perf/cross-browser.perf.spec.ts
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6
- uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4.3.0
with:
# Rust and Python are for the ingest and Tinybird workflows; skipping
# them keeps this job off their ~1 GB of toolchain cache.
install_args: bun node
- uses: ./.github/actions/bun-install
with:
filters: "@maple/web"
- name: Turbo remote cache
uses: rharkor/caching-for-turbo@2238fae6eb9a9936f92356f54cb3660200d105e7 # v2.5.1
# The perf dev server (vite) resolves @maple-dev/browser and
# @maple-dev/effect-sdk from their built dist/ (gitignored), so build
# web's workspace deps before booting it — otherwise vite fails to
# resolve those imports and the bench page never signals ready.
- name: Build web workspace deps
run: bun turbo build --filter=@maple/web^...
# Browser binaries are version-pinned, so key on the resolved
# @playwright/test version rather than the caret range in package.json.
- name: Resolve Playwright version
id: playwright
working-directory: apps/web
run: |
version="$(bun -e 'console.log(require("@playwright/test/package.json").version)')"
echo "version=$version" >> "$GITHUB_OUTPUT"
- name: Cache Playwright browsers
id: playwright-cache
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ steps.playwright.outputs.version }}-${{ matrix.browser }}
restore-keys: |
playwright-${{ runner.os }}-${{ steps.playwright.outputs.version }}-${{ matrix.browser }}-
playwright-${{ runner.os }}-${{ steps.playwright.outputs.version }}
- name: Install Playwright browser
if: steps.playwright-cache.outputs.cache-hit != 'true'
env:
PW_BROWSER: ${{ matrix.browser }}
run: bunx playwright install "$PW_BROWSER"
working-directory: apps/web
# Deliberately NOT folded into `install --with-deps`: system libs go to
# apt paths that ~/.cache/ms-playwright does not cover, so on a cache hit
# the combined form would skip them and webkit would fail to launch.
#
# Skipped for chromium, where it installed nothing we need and cost more
# than the tests. On ubuntu-latest the runner image already carries every
# shared library chromium links, so `install-deps chromium` resolved to
# exactly nine font packages -- fonts-{wqy-zenhei,ipafont-gothic,unifont,
# tlwg-loma-otf,freefont-ttf} and xfonts-* -- i.e. CJK, Cyrillic and Thai
# glyph coverage. The bench pages render latin service names only, so those
# fonts change no pixel we measure, yet the 21 MB pull off the Azure mirror
# ran 12s on a good day and 205s on a bad one, against a 55s test body.
# firefox (40 packages) and webkit (100+) do pull real libraries -- keep it.
#
# If chromium ever fails to launch here, the runner image dropped a library:
# re-enable this for chromium rather than debugging the browser.
- name: Install Playwright system deps
if: ${{ matrix.browser != 'chromium' }}
env:
PW_BROWSER: ${{ matrix.browser }}
run: bunx playwright install-deps "$PW_BROWSER"
working-directory: apps/web
- name: Run perf shard
env:
PW_PROJECTS: ${{ matrix.projects }}
PW_SPECS: ${{ matrix.specs }}
PW_ARGS: ${{ matrix.playwright-args }}
run: |
read -r -a specs <<< "$PW_SPECS"
read -r -a projects <<< "$PW_PROJECTS"
read -r -a args <<< "$PW_ARGS"
bunx playwright test "${specs[@]}" "${projects[@]/#/--project=}" "${args[@]}"
working-directory: apps/web
# The single required status check. Individual jobs above are path-filtered, and a
# skipped job never reports — so if any of them were required directly, branch
# protection would hang pending forever on PRs that legitimately skip them. This
# job always runs and collapses the fan-in to one result.
#
# `skipped` is deliberately absent from the failure condition; that is the point.
# Requires branch protection to require exactly "CI passed" and nothing else.
ci:
name: CI passed
runs-on: ubuntu-latest
timeout-minutes: 5
if: ${{ always() }}
needs:
- changes
- typescript
- tests
- postgres-connection-e2e
- clickhouse-schema-e2e
- local-checkpoint-native
- local-archive-native
- service-map-perf
steps:
- name: Fail if any job failed or was cancelled
if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}
run: |
echo "One or more CI jobs did not pass." >&2
exit 1
# Note: the `apps/ingest` Rust crate is built and tested by
# `.github/workflows/ingest-rust-tests.yml`, which also runs the load
# benchmark and posts results to the PR.