ci: e2e black-box job — boot web+worker, verify ingest→query over HTTP #228
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Lean CI for the GreptimeDB fork (work in progress). | |
| # | |
| # The upstream Langfuse workflows live in .github/workflows-disabled/ — they need upstream | |
| # secrets / deploy infra / the full ClickHouse stack and are re-enabled one at a time as the | |
| # migration progresses. This workflow only covers what is green today: install, codegen, build, | |
| # typecheck, GreptimeDB unit tests, and a headless servertest subset. Heavy jobs are gated on the | |
| # `changes` filter so docs-only changes skip them. | |
| name: CI | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| # dorny/paths-filter reads the PR's changed-file list via the API on pull_request events; without | |
| # this read-only scope the `changes` gate can fail on shallow checkouts / fork PRs and block every | |
| # downstream job. Everything else stays least-privilege (contents: read). | |
| pull-requests: read | |
| concurrency: | |
| group: ci-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| # Gate the heavy jobs so docs-only changes (markdown / docs/ / .agents/) skip the full | |
| # install + build + typecheck + GreptimeDB-container servertests. This job is cheap and always | |
| # runs; the heavy jobs `needs: changes` and skip when only non-code files changed. GitHub counts | |
| # a skipped required check as satisfied for branch protection, so this does not block merges. | |
| changes: | |
| name: detect changes | |
| runs-on: ubuntu-latest | |
| outputs: | |
| code: ${{ steps.filter.outputs.code }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1 | |
| id: filter | |
| with: | |
| filters: | | |
| code: | |
| - '**' | |
| - '!**/*.md' | |
| - '!docs/**' | |
| - '!.agents/**' | |
| write-path: | |
| name: build + typecheck + unit tests | |
| needs: changes | |
| if: needs.changes.outputs.code == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: pnpm/action-setup@v4 # version comes from "packageManager" in package.json | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: 24 | |
| cache: pnpm | |
| # Incremental typecheck cache: the shared/worker typechecks run tsc/tsgo with | |
| # --incremental --tsBuildInfoFile .tsbuildinfo, so a cached tsbuildinfo lets unchanged TS skip | |
| # re-checking across runs. Rolling key (per-commit save + prefix restore fallback). | |
| - uses: actions/cache@v4 | |
| with: | |
| path: | | |
| .turbo | |
| packages/shared/.tsbuildinfo | |
| worker/.tsbuildinfo | |
| key: ts-incremental-${{ runner.os }}-${{ github.sha }} | |
| restore-keys: | | |
| ts-incremental-${{ runner.os }}- | |
| # Dummy env just to satisfy import-time env validation; no service is contacted here. | |
| - run: cp .env.dev.example .env | |
| # --config.minimumReleaseAge=0 skips pnpm's per-entry supply-chain age verification | |
| # (it re-checks all ~2300 lockfile entries' publish timestamps, the slowest step here). | |
| # That age gate (minimumReleaseAge in pnpm-workspace.yaml) guards resolve/update time so a | |
| # freshly-published compromised version can't enter; a --frozen-lockfile install only | |
| # materializes the already-committed-and-vetted lockfile, so re-verifying adds latency with | |
| # no security benefit. | |
| - run: pnpm install --frozen-lockfile --config.minimumReleaseAge=0 | |
| - run: pnpm run db:generate | |
| - run: pnpm --filter @langfuse/shared run build | |
| - name: Typecheck (shared) | |
| run: pnpm --filter @langfuse/shared run typecheck | |
| - name: Typecheck (worker) | |
| run: pnpm --filter worker run typecheck | |
| - name: Unit tests (GreptimeDB converters) | |
| run: pnpm --filter @langfuse/shared run test src/server/greptime/converters.test.ts | |
| # P7 cutover guard: the ClickHouse client/query/writer call-sites were | |
| # physically removed. Fail the build if any reappear so a refactor can't | |
| # silently reintroduce the retired backend. The compatibility leftovers | |
| # (DbResourceError error contract and convertDateToDbDateTime) | |
| # intentionally do NOT match these tokens. | |
| - name: Guard — ClickHouse client/query/writer stay removed | |
| run: | | |
| if grep -rEn '\b(clickhouseClient|queryClickhouse|commandClickhouse|ClickhouseWriter)\b' \ | |
| --include='*.ts' \ | |
| --exclude-dir=node_modules --exclude-dir=dist --exclude-dir=.next \ | |
| packages web worker; then | |
| echo "::error::ClickHouse client/query/writer call-sites reintroduced — P7 removed these; use the GreptimeDB readers/writers instead." | |
| exit 1 | |
| fi | |
| echo "OK: no clickhouseClient/queryClickhouse/commandClickhouse/ClickhouseWriter call-sites." | |
| servertests: | |
| name: GreptimeDB servertests (headless subset) | |
| needs: changes | |
| if: needs.changes.outputs.code == 'true' | |
| runs-on: ubuntu-latest | |
| services: | |
| postgres: | |
| image: postgres:17 | |
| env: | |
| POSTGRES_USER: postgres | |
| POSTGRES_PASSWORD: postgres | |
| POSTGRES_DB: postgres | |
| ports: | |
| - 5432:5432 | |
| options: >- | |
| --health-cmd "pg_isready -U postgres" | |
| --health-interval 10s --health-timeout 5s --health-retries 10 | |
| redis: | |
| image: redis:7.2.4 | |
| ports: | |
| - 6379:6379 | |
| options: >- | |
| --health-cmd "redis-cli ping" | |
| --health-interval 10s --health-timeout 5s --health-retries 10 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: pnpm/action-setup@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: 24 | |
| cache: pnpm | |
| # GreptimeDB needs a custom start command, which GitHub `services:` cannot | |
| # express — run it as a detached container and wait for its HTTP health. | |
| - name: Start GreptimeDB | |
| run: | | |
| docker run -d --name greptimedb \ | |
| -p 4000:4000 -p 4001:4001 -p 4002:4002 \ | |
| greptime/greptimedb:v1.1.1 standalone start \ | |
| --http-addr 0.0.0.0:4000 \ | |
| --rpc-bind-addr 0.0.0.0:4001 \ | |
| --mysql-addr 0.0.0.0:4002 | |
| for i in $(seq 1 30); do | |
| if curl -sf http://localhost:4000/health; then echo "greptimedb up"; break; fi | |
| echo "waiting for greptimedb ($i)"; sleep 2 | |
| done | |
| # MinIO also needs a custom command (create the path-style bucket dir, then | |
| # start the server), which GitHub `services:` cannot express. Mirrors the | |
| # docker-compose.dev.yml setup with the official image. | |
| - name: Start MinIO | |
| run: | | |
| docker run -d --name minio -p 9090:9000 \ | |
| -e MINIO_ROOT_USER=minio -e MINIO_ROOT_PASSWORD=miniosecret \ | |
| --entrypoint sh minio/minio \ | |
| -c 'mkdir -p /data/langfuse && minio server /data' | |
| for i in $(seq 1 30); do | |
| if curl -sf http://localhost:9090/minio/health/live; then echo "minio up"; break; fi | |
| echo "waiting for minio ($i)"; sleep 2 | |
| done | |
| - run: cp .env.dev.example .env | |
| # Skip the supply-chain age verification on the frozen lockfile (see the build job). | |
| - run: pnpm install --frozen-lockfile --config.minimumReleaseAge=0 | |
| - run: pnpm run db:generate | |
| - run: pnpm --filter @langfuse/shared run build | |
| # Apply the Postgres schema the servertests read/write through Prisma. | |
| # db:deploy is the non-interactive `prisma migrate deploy`; it reads | |
| # DATABASE_URL from the .env copied above (postgres service on localhost). | |
| - name: Prisma migrate | |
| run: pnpm --filter @langfuse/shared run db:deploy | |
| # Bootstrap the GreptimeDB schema (the D2 deploy step) before any test runs. | |
| # The web globalSetup also applies migrations idempotently, but the worker | |
| # servertests have no globalSetup and would otherwise depend on the web job | |
| # step having created the schema first. Running it explicitly here removes | |
| # that ordering dependency; the migrations are idempotent so the overlap is | |
| # harmless. GREPTIME_DB/host come from the .env copied above + the defaults | |
| # in packages/shared/src/env.ts (localhost:4002, db "openfuse"). | |
| - name: Bootstrap GreptimeDB schema | |
| env: | |
| GREPTIME_DB: openfuse | |
| run: pnpm --filter @langfuse/shared run greptime:migrate | |
| - name: Web repository + tRPC servertests (GreptimeDB-backed) | |
| env: | |
| DATABASE_URL: postgresql://postgres:postgres@localhost:5432/postgres | |
| GREPTIME_DB: openfuse | |
| LANGFUSE_MIGRATION_V4_ALLOW_PREVIEW_OPT_IN: "true" | |
| run: | | |
| pnpm --filter web run test \ | |
| src/__tests__/server/repositories/experiment-repository.servertest.ts \ | |
| src/__tests__/server/repositories/experiment-items-repository.servertest.ts \ | |
| src/__tests__/server/code-eval-test-run-trpc.servertest.ts \ | |
| src/__tests__/server/queryBuilder.servertest.ts \ | |
| src/__tests__/server/dashboard-v1-v2-consistency.servertest.ts \ | |
| src/__tests__/server/greptime-read-path.servertest.ts | |
| - name: Worker deletion + eval servertests (GreptimeDB-backed) | |
| env: | |
| DATABASE_URL: postgresql://postgres:postgres@localhost:5432/postgres | |
| GREPTIME_DB: openfuse | |
| run: | | |
| pnpm --filter worker run test \ | |
| src/__tests__/scoreDeletion.test.ts \ | |
| src/__tests__/traceDeletion.test.ts \ | |
| src/__tests__/projectDeletionProcessing.test.ts \ | |
| src/queues/__tests__/otelToObservationForEval.test.ts \ | |
| src/features/experiments/__tests__/scheduleExperimentEvals.test.ts | |
| # End-to-end black-box job: boots the real web (:3000) and worker (:3030) HTTP | |
| # servers, ingests through the public API (Langfuse SDK batch + OTLP), and | |
| # verifies the data is queryable back through the public API. This is the only | |
| # job that exercises the full ingest -> queue -> worker -> GreptimeDB -> query | |
| # path over HTTP; the `servertests` job above only calls the DB/tRPC layer | |
| # in-process and never starts the servers. Like that job, the test list is a | |
| # curated green subset that grows as more paths pass on GreptimeDB. | |
| e2e-blackbox: | |
| name: e2e black-box (HTTP ingest -> query) | |
| needs: changes | |
| if: needs.changes.outputs.code == 'true' | |
| timeout-minutes: 25 | |
| runs-on: ubuntu-latest | |
| services: | |
| postgres: | |
| image: postgres:17 | |
| env: | |
| POSTGRES_USER: postgres | |
| POSTGRES_PASSWORD: postgres | |
| POSTGRES_DB: postgres | |
| ports: | |
| - 5432:5432 | |
| options: >- | |
| --health-cmd "pg_isready -U postgres" | |
| --health-interval 10s --health-timeout 5s --health-retries 10 | |
| redis: | |
| image: redis:7.2.4 | |
| ports: | |
| - 6379:6379 | |
| options: >- | |
| --health-cmd "redis-cli ping" | |
| --health-interval 10s --health-timeout 5s --health-retries 10 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: pnpm/action-setup@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: 24 | |
| cache: pnpm | |
| # GreptimeDB and MinIO need custom start commands that GitHub `services:` | |
| # cannot express — run them detached and wait for their HTTP health, same | |
| # as the `servertests` job above. Fail fast (with container logs) if a | |
| # service never comes up, so a later step doesn't fail with a misleading | |
| # error that hides the real root cause. | |
| - name: Start GreptimeDB | |
| run: | | |
| docker run -d --name greptimedb \ | |
| -p 4000:4000 -p 4001:4001 -p 4002:4002 \ | |
| greptime/greptimedb:v1.1.1 standalone start \ | |
| --http-addr 0.0.0.0:4000 \ | |
| --rpc-bind-addr 0.0.0.0:4001 \ | |
| --mysql-addr 0.0.0.0:4002 | |
| for i in $(seq 1 30); do | |
| if curl -sf http://localhost:4000/health; then echo "greptimedb up"; break; fi | |
| echo "waiting for greptimedb ($i)"; sleep 2 | |
| done | |
| curl -sf http://localhost:4000/health > /dev/null \ | |
| || { echo "greptimedb never became healthy"; docker logs greptimedb; exit 1; } | |
| - name: Start MinIO | |
| run: | | |
| docker run -d --name minio -p 9090:9000 \ | |
| -e MINIO_ROOT_USER=minio -e MINIO_ROOT_PASSWORD=miniosecret \ | |
| --entrypoint sh minio/minio \ | |
| -c 'mkdir -p /data/langfuse && minio server /data' | |
| for i in $(seq 1 30); do | |
| if curl -sf http://localhost:9090/minio/health/live; then echo "minio up"; break; fi | |
| echo "waiting for minio ($i)"; sleep 2 | |
| done | |
| curl -sf http://localhost:9090/minio/health/live > /dev/null \ | |
| || { echo "minio never became healthy"; docker logs minio; exit 1; } | |
| - name: Load env | |
| run: | | |
| cp .env.dev.example .env | |
| # Drain the async ingestion pipeline as fast as possible so the | |
| # waitForExpect polls in the black-box tests resolve quickly: | |
| # queue delay before the worker picks a batch, and the worker's | |
| # GreptimeDB write flush interval. | |
| echo "LANGFUSE_INGESTION_QUEUE_DELAY_MS=1" >> .env | |
| echo "LANGFUSE_INGESTION_WRITE_INTERVAL_MS=1" >> .env | |
| # Trace deletion is queued with a delay and drained with bounded | |
| # concurrency; the defaults make the DELETE-then-poll tests time out. | |
| # Match upstream's test tuning so deletes propagate within the poll. | |
| echo "LANGFUSE_TRACE_DELETE_DELAY_MS=1" >> .env | |
| echo "LANGFUSE_TRACE_DELETE_CONCURRENCY=100" >> .env | |
| - run: pnpm install --frozen-lockfile --config.minimumReleaseAge=0 | |
| - run: pnpm run db:generate | |
| - name: Build (web + worker) | |
| run: pnpm run build | |
| env: | |
| NODE_OPTIONS: --max_old_space_size=8192 | |
| - name: Prisma migrate | |
| run: pnpm --filter @langfuse/shared run db:deploy | |
| - name: Bootstrap GreptimeDB schema | |
| env: | |
| GREPTIME_DB: openfuse | |
| run: pnpm --filter @langfuse/shared run greptime:migrate | |
| # Seed the fixed demo project (7a88...) plus the eval templates and job | |
| # configurations the __e2e__ suite asserts on (api.servertest.ts triggers | |
| # an eval on userId=user-1 and expects a jobExecution row). | |
| - name: Seed example data | |
| run: pnpm --filter @langfuse/shared run db:seed:examples | |
| # `pnpm run start` fans out via turbo to `next start` (web :3000) and | |
| # `node dist/index.js` (worker :3030). LANGFUSE_INIT_* provisions the same | |
| # fixed org/project/keys the seed uses, idempotently, so a live server is | |
| # available even before the seed's rows are read. | |
| - name: Start web + worker | |
| run: (pnpm run start > /tmp/langfuse-app.log 2>&1 &) | |
| env: | |
| LANGFUSE_INIT_ORG_ID: "seed-org-id" | |
| LANGFUSE_INIT_ORG_NAME: "Seed Org" | |
| LANGFUSE_INIT_PROJECT_ID: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" | |
| LANGFUSE_INIT_PROJECT_NAME: "Seed Project" | |
| LANGFUSE_INIT_PROJECT_PUBLIC_KEY: "pk-lf-1234567890" | |
| LANGFUSE_INIT_PROJECT_SECRET_KEY: "sk-lf-1234567890" | |
| LANGFUSE_INIT_USER_EMAIL: "demo@langfuse.com" | |
| LANGFUSE_INIT_USER_NAME: "Demo User" | |
| LANGFUSE_INIT_USER_PASSWORD: "password" | |
| - name: Wait for worker + web health | |
| run: | | |
| timeout 120 bash -c 'until curl -sf http://localhost:3030/api/health; do echo "waiting for worker"; sleep 2; done' \ | |
| || { echo "worker never became healthy"; cat /tmp/langfuse-app.log; exit 1; } | |
| timeout 120 bash -c 'until curl -sf http://localhost:3000/api/public/health; do echo "waiting for web"; sleep 2; done' \ | |
| || { echo "web never became healthy"; cat /tmp/langfuse-app.log; exit 1; } | |
| # HTTP black-box tests that self-provision their project via | |
| # createOrgProjectAndApiKey and hit http://localhost:3000, covering the | |
| # core OTLP/SDK ingest -> query families. | |
| - name: Public API black-box servertests (HTTP) | |
| env: | |
| DATABASE_URL: postgresql://postgres:postgres@localhost:5432/postgres | |
| GREPTIME_DB: openfuse | |
| run: | | |
| pnpm --filter web run test \ | |
| src/__tests__/server/ingestion-api.servertest.ts \ | |
| src/__tests__/server/otel-api.servertest.ts \ | |
| src/__tests__/server/traces-api.servertest.ts \ | |
| src/__tests__/server/observations-api.servertest.ts \ | |
| src/__tests__/server/scores-api-v2.servertest.ts \ | |
| src/__tests__/server/sessions-api.servertest.ts | |
| # The __e2e__ suite (seed-dependent): ingest a batch, poll the public | |
| # GET /traces API, and assert the worker-run eval produced a jobExecution; | |
| # plus OTLP tenant-isolation. | |
| - name: __e2e__ black-box suite | |
| env: | |
| DATABASE_URL: postgresql://postgres:postgres@localhost:5432/postgres | |
| GREPTIME_DB: openfuse | |
| run: pnpm --filter web run test:e2e:server | |
| - name: Dump app logs on failure | |
| if: failure() | |
| run: cat /tmp/langfuse-app.log || true |