Skip to content

ci: e2e black-box job — boot web+worker, verify ingest→query over HTTP #228

ci: e2e black-box job — boot web+worker, verify ingest→query over HTTP

ci: e2e black-box job — boot web+worker, verify ingest→query over HTTP #228

Workflow file for this run

# 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