Skip to content

feat(browser): server-side traced, /server and Next.js entries #5079

feat(browser): server-side traced, /server and Next.js entries

feat(browser): server-side traced, /server and Next.js entries #5079

Workflow file for this run

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.