feat(prompts): add Jinja2 conditional templating with compiled preview #64
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
| name: CI/CD | |
| on: | |
| workflow_dispatch: | |
| push: | |
| branches: | |
| - "main" | |
| tags: | |
| - "v*" | |
| merge_group: | |
| pull_request: | |
| branches: | |
| - "**" | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: ${{ github.event_name == 'pull_request' }} | |
| env: | |
| # Keep the test job node-version matrices in sync. GitHub does not allow the | |
| # env context in jobs.<job_id>.name, so matrix jobs use matrix.node-version | |
| # for their display names. | |
| NODE_VERSION: 24 | |
| # Disable CI cache restores for fork PRs. | |
| # | |
| # Fork PRs can only read base-branch caches, and CI cache paths must not | |
| # contain secrets. This is conservative defense-in-depth, not required for | |
| # release cache integrity: PR-created caches are scoped to the PR merge ref, | |
| # main does not restore them, and release image builds intentionally do not | |
| # restore CI caches. | |
| CI_CACHE_ALLOWED: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} | |
| jobs: | |
| pre-job: | |
| runs-on: blacksmith-4vcpu-ubuntu-2404 | |
| outputs: | |
| should_skip: ${{ steps.skip_check.outputs.should_skip }} | |
| llm_connections_changed: ${{ steps.filter.outputs.llm_connections }} | |
| timeout-minutes: 15 | |
| permissions: | |
| contents: read | |
| pull-requests: read | |
| steps: | |
| # Replaces fkirc/skip-duplicate-actions — skips runs whose git tree | |
| # was already tested in a prior successful run of this workflow. | |
| - id: skip_check | |
| env: | |
| EVENT_NAME: ${{ github.event_name }} | |
| GH_TOKEN: ${{ github.token }} | |
| REPOSITORY: ${{ github.repository }} | |
| COMMIT_SHA: ${{ github.sha }} | |
| RUN_ID: ${{ github.run_id }} | |
| run: | | |
| if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then | |
| echo "should_skip=false" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| CURRENT_TREE=$(gh api "repos/$REPOSITORY/git/commits/$COMMIT_SHA" --jq '.tree.sha') | |
| MATCH=$(gh api "repos/$REPOSITORY/actions/workflows/pipeline.yml/runs?status=success&per_page=20" \ | |
| | jq -r --argjson run_id "$RUN_ID" --arg current_tree "$CURRENT_TREE" \ | |
| '[.workflow_runs[] | select(.id != $run_id and .head_commit.tree_id == $current_tree)] | first | .head_sha // empty') | |
| if [[ -n "$MATCH" ]]; then | |
| echo "::notice::Tree $CURRENT_TREE already tested in a prior successful run (commit $MATCH) — skipping" | |
| echo "should_skip=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "should_skip=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| # Recommended for paths-filter action | |
| # may save additional git fetch roundtrip if | |
| # merge-base is found within latest N commits | |
| fetch-depth: 20 | |
| persist-credentials: false | |
| - uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1 | |
| id: filter | |
| with: | |
| filters: | | |
| llm_connections: | |
| - 'packages/shared/package.json' | |
| - 'packages/shared/src/server/llm/fetchLLMCompletion.ts' | |
| lint: | |
| runs-on: blacksmith-4vcpu-ubuntu-2404 | |
| needs: | |
| - pre-job | |
| if: needs.pre-job.outputs.should_skip != 'true' | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - uses: pnpm/action-setup@739bfe42ca9233c5e6aca07c1a25a9d34aca49b0 # v6.0.7 | |
| with: | |
| version: 11.1.3 | |
| - name: Use Node.js ${{ env.NODE_VERSION }} without cache | |
| if: env.CI_CACHE_ALLOWED != 'true' | |
| uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| package-manager-cache: false | |
| - name: Use Node.js ${{ env.NODE_VERSION }} with pnpm cache | |
| if: env.CI_CACHE_ALLOWED == 'true' | |
| uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # zizmor: ignore[cache-poisoning] lint job dependency cache only; no released artifacts are built or published from this cached state | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: "pnpm" | |
| cache-dependency-path: "pnpm-lock.yaml" | |
| - name: Setup Turbo cache | |
| if: env.CI_CACHE_ALLOWED == 'true' | |
| uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # zizmor: ignore[cache-poisoning] lint cache only; publish jobs rebuild artifacts and do not restore this cache | |
| with: | |
| path: .turbo | |
| key: ${{ runner.os }}-turbo-lint-${{ github.sha }} | |
| restore-keys: | | |
| ${{ runner.os }}-turbo-lint- | |
| ${{ runner.os }}-turbo- | |
| - name: install dependencies | |
| run: | | |
| pnpm i | |
| - name: Load default env | |
| run: | | |
| cp .env.dev.example .env | |
| - name: lint web | |
| run: pnpm run lint | |
| - name: typecheck | |
| run: pnpm run typecheck | |
| prettier-check: | |
| runs-on: blacksmith-4vcpu-ubuntu-2404 | |
| needs: | |
| - pre-job | |
| if: needs.pre-job.outputs.should_skip != 'true' | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - uses: pnpm/action-setup@739bfe42ca9233c5e6aca07c1a25a9d34aca49b0 # v6.0.7 | |
| with: | |
| version: 11.1.3 | |
| - name: Use Node.js ${{ env.NODE_VERSION }} without cache | |
| if: env.CI_CACHE_ALLOWED != 'true' | |
| uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| package-manager-cache: false | |
| - name: Use Node.js ${{ env.NODE_VERSION }} with pnpm cache | |
| if: env.CI_CACHE_ALLOWED == 'true' | |
| uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # zizmor: ignore[cache-poisoning] prettier check dependency cache only; no released artifacts are built or published from this cached state | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: "pnpm" | |
| cache-dependency-path: "pnpm-lock.yaml" | |
| - name: install dependencies | |
| run: | | |
| pnpm i | |
| - name: Load default env | |
| run: | | |
| cp .env.dev.example .env | |
| - name: Check formatting on changed files | |
| env: | |
| EVENT_NAME: ${{ github.event_name }} | |
| PR_BASE_SHA: ${{ github.event.pull_request.base.sha }} | |
| run: | | |
| if [ "$EVENT_NAME" = "pull_request" ]; then | |
| BASE_SHA="$PR_BASE_SHA" | |
| else | |
| BASE_SHA=$(git merge-base origin/main HEAD) | |
| fi | |
| echo "Checking files changed from $BASE_SHA to HEAD" | |
| if git diff --quiet --exit-code --diff-filter=d "$BASE_SHA" HEAD -- '*.js' '*.jsx' '*.ts' '*.tsx' '*.css'; then | |
| echo "No JS/TS/CSS files changed - skipping prettier check" | |
| else | |
| # --experimental-cli is intentionally omitted: its glob resolver treats bracket characters | |
| # in Next.js dynamic-route paths (e.g. [projectId]) as glob character classes, causing | |
| # "No files matching the given patterns were found" (exit 123) for any PR that touches a | |
| # file inside a bracket-named directory. Standard prettier resolves explicit paths correctly. | |
| # Parallelism and the ephemeral cache (the only extras --experimental-cli adds) provide no | |
| # benefit when checking a handful of per-PR changed files. | |
| git diff --name-only -z --diff-filter=d "$BASE_SHA" HEAD -- '*.js' '*.jsx' '*.ts' '*.tsx' '*.css' \ | |
| | xargs -0 --no-run-if-empty pnpm prettier --check -- | |
| fi | |
| tests-eslint-plugin: | |
| runs-on: blacksmith-4vcpu-ubuntu-2404 | |
| needs: | |
| - pre-job | |
| if: needs.pre-job.outputs.should_skip != 'true' | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - uses: pnpm/action-setup@739bfe42ca9233c5e6aca07c1a25a9d34aca49b0 # v6.0.7 | |
| with: | |
| version: 11.1.3 | |
| - name: Use Node.js ${{ env.NODE_VERSION }} without cache | |
| if: env.CI_CACHE_ALLOWED != 'true' | |
| uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| package-manager-cache: false | |
| - name: Use Node.js ${{ env.NODE_VERSION }} with pnpm cache | |
| if: env.CI_CACHE_ALLOWED == 'true' | |
| uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # zizmor: ignore[cache-poisoning] eslint-plugin test dependency cache only; no published artifacts are built from this cached state | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: "pnpm" | |
| cache-dependency-path: "pnpm-lock.yaml" | |
| - name: install dependencies | |
| run: | | |
| pnpm install | |
| - name: run eslint-plugin tests | |
| run: pnpm --filter @repo/eslint-plugin run test | |
| test-docker-build: | |
| timeout-minutes: 20 | |
| runs-on: blacksmith-4vcpu-ubuntu-2404 | |
| needs: | |
| - pre-job | |
| if: needs.pre-job.outputs.should_skip != 'true' | |
| env: | |
| DOCKER_COMPOSE_FILE: docker-compose.build.yml | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - name: Login to Docker Hub | |
| if: github.repository == 'langfuse/langfuse' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) | |
| uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 | |
| with: | |
| username: ${{ secrets.DOCKERHUB_USERNAME_READ }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN_READ }} | |
| - name: Set NEXT_PUBLIC_BUILD_ID | |
| run: echo "NEXT_PUBLIC_BUILD_ID=$(git rev-parse --short HEAD)" >> $GITHUB_ENV | |
| - name: Build images | |
| run: | | |
| docker compose -f "$DOCKER_COMPOSE_FILE" build --print > /tmp/bake.json | |
| docker buildx bake -f /tmp/bake.json | |
| - name: Start compose services and wait for healthchecks | |
| run: | | |
| docker compose -f "$DOCKER_COMPOSE_FILE" up -d --wait --wait-timeout 180 | |
| - name: Check worker health | |
| run: | | |
| curl --retry 10 --retry-delay 2 --retry-all-errors --silent --show-error --fail \ | |
| http://localhost:3030/api/health | |
| - name: Check server health | |
| run: | | |
| curl --retry 10 --retry-delay 2 --retry-all-errors --silent --show-error --fail \ | |
| http://localhost:3000/api/public/health | |
| - name: Capture docker diagnostics on failure | |
| if: failure() | |
| run: | | |
| mkdir -p /tmp/docker-diagnostics | |
| docker compose -f "$DOCKER_COMPOSE_FILE" ps -a \ | |
| | tee /tmp/docker-diagnostics/compose-state.txt || true | |
| timeout 15 docker events --since 20m --until "$(date -u +'%Y-%m-%dT%H:%M:%SZ')" \ | |
| | tee /tmp/docker-diagnostics/docker-events.txt || true | |
| ss -ltnp | tee /tmp/docker-diagnostics/host-listeners.txt || true | |
| for service in langfuse-web langfuse-worker postgres redis minio clickhouse; do | |
| container_id="$(docker compose -f "$DOCKER_COMPOSE_FILE" ps -q "$service" || true)" | |
| if [ -z "$container_id" ]; then | |
| echo "No container found for ${service}" | tee -a /tmp/docker-diagnostics/missing-containers.txt | |
| continue | |
| fi | |
| docker inspect "$container_id" > "/tmp/docker-diagnostics/${service}-inspect.json" || true | |
| docker logs --timestamps "$container_id" > "/tmp/docker-diagnostics/${service}-docker-logs.txt" 2>&1 || true | |
| done | |
| - name: Upload docker diagnostics | |
| if: failure() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: test-docker-build-diagnostics-${{ github.run_id }}-${{ github.run_attempt }} | |
| path: /tmp/docker-diagnostics | |
| if-no-files-found: ignore | |
| tests-web: | |
| timeout-minutes: 30 | |
| runs-on: blacksmith-4vcpu-ubuntu-2404 | |
| needs: | |
| - pre-job | |
| if: needs.pre-job.outputs.should_skip != 'true' | |
| name: tests-web (node${{ matrix.node-version }}, pg${{ matrix.postgres-version }}, mode${{ matrix.deploy-mode }}) | |
| strategy: | |
| matrix: | |
| node-version: [24] | |
| postgres-version: [12, 15] | |
| deploy-mode: ["", "-azure", "-redis-cluster"] | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - name: Install golang-migrate for Clickhouse migrations | |
| run: | | |
| curl --fail --location --retry 5 --retry-delay 2 --retry-all-errors \ | |
| --output migrate.linux-amd64.tar.gz \ | |
| https://github.com/golang-migrate/migrate/releases/download/v4.19.1/migrate.linux-amd64.tar.gz | |
| tar xzf migrate.linux-amd64.tar.gz | |
| sudo mv migrate /usr/bin/migrate | |
| which migrate | |
| - uses: pnpm/action-setup@739bfe42ca9233c5e6aca07c1a25a9d34aca49b0 # v6.0.7 | |
| with: | |
| version: 11.1.3 | |
| - name: Login to Docker Hub | |
| if: github.repository == 'langfuse/langfuse' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) | |
| uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 | |
| with: | |
| username: ${{ secrets.DOCKERHUB_USERNAME_READ }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN_READ }} | |
| - name: Use Node.js ${{ matrix.node-version }} without cache | |
| if: env.CI_CACHE_ALLOWED != 'true' | |
| uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 | |
| with: | |
| node-version: ${{ matrix.node-version }} | |
| package-manager-cache: false | |
| - name: Use Node.js ${{ matrix.node-version }} with pnpm cache | |
| if: env.CI_CACHE_ALLOWED == 'true' | |
| uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # zizmor: ignore[cache-poisoning] test job dependency cache only; no released artifacts are built or published from this cached state | |
| with: | |
| node-version: ${{ matrix.node-version }} | |
| cache: "pnpm" | |
| cache-dependency-path: "pnpm-lock.yaml" | |
| - name: install dependencies | |
| run: | | |
| pnpm install | |
| - name: Load default env | |
| run: | | |
| cp .env.dev${{ matrix.deploy-mode }}.example .env | |
| grep -v -e '^LANGFUSE_S3_BATCH_EXPORT_ENABLED=' -e '^NEXT_PUBLIC_LANGFUSE_RUN_NEXT_INIT=' .env.dev${{ matrix.deploy-mode }}.example > .env | |
| echo "LANGFUSE_INGESTION_QUEUE_DELAY_MS=1" >> .env | |
| echo "LANGFUSE_CACHE_PROMPT_ENABLED=false" >> .env | |
| echo "LANGFUSE_INGESTION_CLICKHOUSE_WRITE_INTERVAL_MS=1" >> .env | |
| echo "LANGFUSE_TRACE_DELETE_DELAY_MS=1" >> .env | |
| echo "LANGFUSE_TRACE_DELETE_CONCURRENCY=100" >> .env | |
| echo "ADMIN_API_KEY=admin-api-key" >> .env | |
| echo "LANGFUSE_EE_LICENSE_KEY=langfuse_ee_test" >> .env | |
| echo "LANGFUSE_SKIP_EVALUATOR_MODEL_CALL_VALIDATION=true" >> .env | |
| - name: Setup Turbo cache | |
| if: env.CI_CACHE_ALLOWED == 'true' | |
| uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # zizmor: ignore[cache-poisoning] test job cache only; publish jobs rebuild artifacts and do not restore this cache | |
| with: | |
| path: .turbo | |
| key: ${{ runner.os }}-turbo-${{ github.sha }} | |
| restore-keys: | | |
| ${{ runner.os }}-turbo- | |
| - name: Cache Next.js builds | |
| if: env.CI_CACHE_ALLOWED == 'true' | |
| uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # zizmor: ignore[cache-poisoning] test-only Next.js cache; not consumed by artifact publishing or release jobs | |
| with: | |
| path: | | |
| ~/.npm | |
| ${{ github.workspace }}/web/.next/cache | |
| key: ${{ runner.os }}-nextjs-web-${{ matrix.node-version }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('web/**/*.js', 'web/**/*.jsx', 'web/**/*.ts', 'web/**/*.tsx') }} | |
| restore-keys: | | |
| ${{ runner.os }}-nextjs-web-${{ matrix.node-version }}-${{ hashFiles('**/pnpm-lock.yaml') }}- | |
| ${{ runner.os }}-nextjs-web-${{ matrix.node-version }}- | |
| - name: Run dev containers | |
| run: | | |
| docker compose -f docker-compose.dev${{ matrix.deploy-mode }}.yml up -d --wait --wait-timeout 180 | |
| docker compose ps | |
| env: | |
| POSTGRES_VERSION: ${{ matrix.postgres-version }} | |
| - name: Migrate DB | |
| run: | | |
| pnpm run db:migrate | |
| pnpm --filter=shared ch:up | |
| - name: Download and extract ClickHouse client for dev-tables setup | |
| if: matrix.deploy-mode == '' | |
| run: | | |
| CH_VERSION="24.9.3.128" | |
| ARCH="amd64" | |
| wget "https://packages.clickhouse.com/deb/pool/main/c/clickhouse/clickhouse-common-static_${CH_VERSION}_${ARCH}.deb" -O clickhouse-client.deb | |
| # The binary is usually located in ./usr/bin/clickhouse in the archive | |
| dpkg-deb -x clickhouse-client.deb ch_client_dir | |
| sudo cp ch_client_dir/usr/bin/clickhouse /usr/local/bin/ | |
| - name: Setup Dev Tables | |
| if: matrix.deploy-mode == '' | |
| run: | | |
| pnpm --filter=shared ch:dev-tables | |
| - name: Build | |
| run: pnpm run build | |
| env: | |
| NODE_OPTIONS: --max_old_space_size=8192 | |
| # TypeScript is checked explicitly in the lint job; skip duplicate | |
| # Next.js type checks in test builds to reduce CI runtime. | |
| NEXT_IGNORE_BUILD_ERRORS: "true" | |
| - name: Start Langfuse | |
| run: (pnpm run start&) | |
| env: | |
| LANGFUSE_INIT_ORG_ID: "seed-org-id" | |
| LANGFUSE_INIT_ORG_NAME: "Seed Org" | |
| LANGFUSE_INIT_ORG_CLOUD_PLAN: "Team" | |
| 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: run tests | |
| working-directory: web | |
| run: npx dotenv -e ../.env.test -e ../.env -- vitest run --project server | |
| - name: run test-client | |
| if: matrix.deploy-mode == '' | |
| working-directory: web | |
| run: npx dotenv -e ../.env.test -e ../.env -- vitest run --project client | |
| tests-worker: | |
| timeout-minutes: 20 | |
| runs-on: blacksmith-4vcpu-ubuntu-2404 | |
| needs: | |
| - pre-job | |
| if: needs.pre-job.outputs.should_skip != 'true' | |
| name: tests-worker (node${{ matrix.node-version }}, pg${{ matrix.postgres-version }}, mode${{ matrix.deploy-mode }}) | |
| strategy: | |
| matrix: | |
| node-version: [24] | |
| postgres-version: [12, 15] | |
| deploy-mode: ["", "-azure", "-redis-cluster"] | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - uses: pnpm/action-setup@739bfe42ca9233c5e6aca07c1a25a9d34aca49b0 # v6.0.7 | |
| with: | |
| version: 11.1.3 | |
| - name: Login to Docker Hub | |
| if: github.repository == 'langfuse/langfuse' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) | |
| uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 | |
| with: | |
| username: ${{ secrets.DOCKERHUB_USERNAME_READ }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN_READ }} | |
| - name: Use Node.js ${{ matrix.node-version }} without cache | |
| if: env.CI_CACHE_ALLOWED != 'true' | |
| uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 | |
| with: | |
| node-version: ${{ matrix.node-version }} | |
| package-manager-cache: false | |
| - name: Use Node.js ${{ matrix.node-version }} with pnpm cache | |
| if: env.CI_CACHE_ALLOWED == 'true' | |
| uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # zizmor: ignore[cache-poisoning] worker test dependency cache only; no release artifacts are produced from this cache | |
| with: | |
| node-version: ${{ matrix.node-version }} | |
| cache: "pnpm" | |
| cache-dependency-path: "pnpm-lock.yaml" | |
| - name: install dependencies | |
| run: | | |
| pnpm install | |
| - name: Install golang-migrate for Clickhouse migrations | |
| run: | | |
| curl --fail --location --retry 5 --retry-delay 2 --retry-all-errors \ | |
| --output migrate.linux-amd64.tar.gz \ | |
| https://github.com/golang-migrate/migrate/releases/download/v4.19.1/migrate.linux-amd64.tar.gz | |
| tar xzf migrate.linux-amd64.tar.gz | |
| sudo mv migrate /usr/bin/migrate | |
| which migrate | |
| - name: Load default env | |
| run: | | |
| cp .env.dev${{ matrix.deploy-mode }}.example .env | |
| cp .env.dev${{ matrix.deploy-mode }}.example web/.env | |
| cp .env.dev${{ matrix.deploy-mode }}.example worker/.env | |
| - name: Run + migrate | |
| run: | | |
| docker compose -f docker-compose.dev${{ matrix.deploy-mode }}.yml up -d --wait --wait-timeout 180 | |
| docker compose ps | |
| - name: Ensure no unhealthy status | |
| run: | | |
| if docker compose ps | grep "(unhealthy)"; then | |
| echo "One or more services are unhealthy" | |
| exit 1 | |
| else | |
| echo "All services are healthy" | |
| fi | |
| - name: Seed DB | |
| run: | | |
| pnpm run db:migrate | |
| pnpm --filter=shared run db:seed | |
| pnpm run --filter=shared ch:up | |
| - name: Download and extract ClickHouse client for dev-tables setup | |
| if: matrix.deploy-mode == '' | |
| run: | | |
| CH_VERSION="24.9.3.128" | |
| ARCH="amd64" | |
| wget "https://packages.clickhouse.com/deb/pool/main/c/clickhouse/clickhouse-common-static_${CH_VERSION}_${ARCH}.deb" -O clickhouse-client.deb | |
| dpkg-deb -x clickhouse-client.deb ch_client_dir | |
| sudo cp ch_client_dir/usr/bin/clickhouse /usr/local/bin/ | |
| - name: Setup Dev Tables | |
| if: matrix.deploy-mode == '' | |
| run: | | |
| pnpm --filter=shared ch:dev-tables | |
| - name: Build | |
| run: pnpm --filter=worker... run build | |
| - name: run tests | |
| run: pnpm --filter=worker run test:exclude-llm-connections | |
| test-worker-llm-connections: | |
| timeout-minutes: 20 | |
| runs-on: blacksmith-4vcpu-ubuntu-2404 | |
| needs: | |
| - pre-job | |
| if: startsWith(github.ref, 'refs/tags/') || (needs.pre-job.outputs.should_skip != 'true' && (needs.pre-job.outputs.llm_connections_changed == 'true' || github.event_name == 'workflow_dispatch')) | |
| name: test-worker-llm-connections (node24, pg15) | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - uses: pnpm/action-setup@739bfe42ca9233c5e6aca07c1a25a9d34aca49b0 # v6.0.7 | |
| with: | |
| version: 11.1.3 | |
| - name: Login to Docker Hub | |
| if: github.repository == 'langfuse/langfuse' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) | |
| uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 | |
| with: | |
| username: ${{ secrets.DOCKERHUB_USERNAME_READ }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN_READ }} | |
| # Keep this secret-bearing job cache-cold to avoid exposing real provider | |
| # credentials to potentially poisoned shared package-manager state. | |
| - name: Use Node.js ${{ env.NODE_VERSION }} | |
| uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| package-manager-cache: false | |
| - name: install dependencies | |
| run: | | |
| pnpm install | |
| - name: Install golang-migrate for Clickhouse migrations | |
| run: | | |
| curl --fail --location --retry 5 --retry-delay 2 --retry-all-errors \ | |
| --output migrate.linux-amd64.tar.gz \ | |
| https://github.com/golang-migrate/migrate/releases/download/v4.19.1/migrate.linux-amd64.tar.gz | |
| tar xzf migrate.linux-amd64.tar.gz | |
| sudo mv migrate /usr/bin/migrate | |
| which migrate | |
| - name: Load default env | |
| run: | | |
| cp .env.dev.example .env | |
| cp .env.dev.example web/.env | |
| cp .env.dev.example worker/.env | |
| - name: Run + migrate | |
| run: | | |
| docker compose -f docker-compose.dev.yml up -d --wait --wait-timeout 180 | |
| docker compose ps | |
| env: | |
| POSTGRES_VERSION: 15 | |
| - name: Ensure no unhealthy status | |
| run: | | |
| if docker compose ps | grep "(unhealthy)"; then | |
| echo "One or more services are unhealthy" | |
| exit 1 | |
| else | |
| echo "All services are healthy" | |
| fi | |
| - name: Seed DB | |
| run: | | |
| pnpm run db:migrate | |
| pnpm --filter=shared run db:seed | |
| pnpm run --filter=shared ch:up | |
| - name: Build | |
| run: pnpm --filter=worker... run build | |
| - name: run llm connection tests | |
| run: pnpm --filter=worker run test:llm-connections-only | |
| env: | |
| LANGFUSE_LLM_CONNECTION_OPENAI_KEY: ${{ secrets.LANGFUSE_LLM_CONNECTION_OPENAI_KEY }} | |
| LANGFUSE_LLM_CONNECTION_ANTHROPIC_KEY: ${{ secrets.LANGFUSE_LLM_CONNECTION_ANTHROPIC_KEY }} | |
| LANGFUSE_LLM_CONNECTION_AZURE_KEY: ${{ secrets.LANGFUSE_LLM_CONNECTION_AZURE_KEY }} | |
| LANGFUSE_LLM_CONNECTION_AZURE_BASE_URL: ${{ secrets.LANGFUSE_LLM_CONNECTION_AZURE_BASE_URL }} | |
| LANGFUSE_LLM_CONNECTION_AZURE_MODEL: ${{ secrets.LANGFUSE_LLM_CONNECTION_AZURE_MODEL }} | |
| LANGFUSE_LLM_CONNECTION_BEDROCK_ACCESS_KEY_ID: ${{ secrets.LANGFUSE_LLM_CONNECTION_BEDROCK_ACCESS_KEY_ID }} | |
| LANGFUSE_LLM_CONNECTION_BEDROCK_SECRET_ACCESS_KEY: ${{ secrets.LANGFUSE_LLM_CONNECTION_BEDROCK_SECRET_ACCESS_KEY }} | |
| LANGFUSE_LLM_CONNECTION_BEDROCK_REGION: ${{ secrets.LANGFUSE_LLM_CONNECTION_BEDROCK_REGION }} | |
| LANGFUSE_LLM_CONNECTION_BEDROCK_API_KEY: ${{ secrets.LANGFUSE_LLM_CONNECTION_BEDROCK_API_KEY }} | |
| LANGFUSE_LLM_CONNECTION_VERTEXAI_KEY: ${{ secrets.LANGFUSE_LLM_CONNECTION_VERTEXAI_KEY }} | |
| LANGFUSE_LLM_CONNECTION_GOOGLEAISTUDIO_KEY: ${{ secrets.LANGFUSE_LLM_CONNECTION_GOOGLEAISTUDIO_KEY }} | |
| e2e-tests: | |
| runs-on: blacksmith-4vcpu-ubuntu-2404 | |
| needs: | |
| - pre-job | |
| if: needs.pre-job.outputs.should_skip != 'true' | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - uses: pnpm/action-setup@739bfe42ca9233c5e6aca07c1a25a9d34aca49b0 # v6.0.7 | |
| with: | |
| version: 11.1.3 | |
| - name: Use Node.js ${{ env.NODE_VERSION }} without cache | |
| if: env.CI_CACHE_ALLOWED != 'true' | |
| uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| package-manager-cache: false | |
| - name: Use Node.js ${{ env.NODE_VERSION }} with pnpm cache | |
| if: env.CI_CACHE_ALLOWED == 'true' | |
| uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # zizmor: ignore[cache-poisoning] e2e dependency cache only; release images are rebuilt later without restoring this cache | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: "pnpm" | |
| cache-dependency-path: "pnpm-lock.yaml" | |
| - name: Login to Docker Hub | |
| if: github.repository == 'langfuse/langfuse' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) | |
| uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 | |
| with: | |
| username: ${{ secrets.DOCKERHUB_USERNAME_READ }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN_READ }} | |
| - name: Setup Turbo cache | |
| if: env.CI_CACHE_ALLOWED == 'true' | |
| uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # zizmor: ignore[cache-poisoning] e2e cache only; no published artifact path restores this cache | |
| with: | |
| path: .turbo | |
| key: ${{ runner.os }}-turbo-e2e-${{ github.sha }} | |
| restore-keys: | | |
| ${{ runner.os }}-turbo-e2e- | |
| ${{ runner.os }}-turbo- | |
| - name: Cache Next.js builds | |
| if: env.CI_CACHE_ALLOWED == 'true' | |
| uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # zizmor: ignore[cache-poisoning] e2e-only Next.js cache; not part of any artifact build or publishing flow | |
| with: | |
| path: | | |
| ~/.npm | |
| ${{ github.workspace }}/web/.next/cache | |
| key: ${{ runner.os }}-nextjs-e2e-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('web/**/*.js', 'web/**/*.jsx', 'web/**/*.ts', 'web/**/*.tsx') }} | |
| restore-keys: | | |
| ${{ runner.os }}-nextjs-e2e-${{ hashFiles('**/pnpm-lock.yaml') }}- | |
| ${{ runner.os }}-nextjs-e2e- | |
| - name: install dependencies | |
| run: | | |
| pnpm install | |
| - name: Load default env | |
| run: | | |
| cp .env.dev.example .env | |
| cp .env.dev.example web/.env | |
| - name: Install golang-migrate for Clickhouse migrations | |
| run: | | |
| curl --fail --location --retry 5 --retry-delay 2 --retry-all-errors \ | |
| --output migrate.linux-amd64.tar.gz \ | |
| https://github.com/golang-migrate/migrate/releases/download/v4.19.1/migrate.linux-amd64.tar.gz | |
| tar xzf migrate.linux-amd64.tar.gz | |
| sudo mv migrate /usr/bin/migrate | |
| which migrate | |
| - name: Run + migrate | |
| run: | | |
| docker compose -f docker-compose.dev.yml up -d --wait --wait-timeout 180 | |
| docker compose ps | |
| - name: Ensure Docker dependencies are healthy | |
| run: | | |
| if docker compose ps | grep "(unhealthy)"; then | |
| echo "One or more services are unhealthy" | |
| exit 1 | |
| else | |
| echo "All services are healthy" | |
| fi | |
| - name: Seed DB | |
| run: | | |
| pnpm run db:migrate | |
| pnpm --filter=shared run ch:up | |
| pnpm --filter=shared run db:seed | |
| - name: Build | |
| run: pnpm run build | |
| env: | |
| NODE_OPTIONS: --max_old_space_size=8192 | |
| # TypeScript is checked explicitly in the lint job; skip duplicate | |
| # Next.js type checks in test builds to reduce CI runtime. | |
| NEXT_IGNORE_BUILD_ERRORS: "true" | |
| - name: Install playwright | |
| run: pnpm --filter=web exec playwright install --with-deps --only-shell chromium | |
| - name: Run e2e tests | |
| run: pnpm --filter=web run test:e2e | |
| e2e-server-tests: | |
| runs-on: blacksmith-4vcpu-ubuntu-2404 | |
| needs: | |
| - pre-job | |
| if: needs.pre-job.outputs.should_skip != 'true' | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - name: Login to Docker Hub | |
| if: github.repository == 'langfuse/langfuse' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) | |
| uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 | |
| with: | |
| username: ${{ secrets.DOCKERHUB_USERNAME_READ }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN_READ }} | |
| - uses: pnpm/action-setup@739bfe42ca9233c5e6aca07c1a25a9d34aca49b0 # v6.0.7 | |
| with: | |
| version: 11.1.3 | |
| - name: Use Node.js ${{ env.NODE_VERSION }} without cache | |
| if: env.CI_CACHE_ALLOWED != 'true' | |
| uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| package-manager-cache: false | |
| - name: Use Node.js ${{ env.NODE_VERSION }} with pnpm cache | |
| if: env.CI_CACHE_ALLOWED == 'true' | |
| uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # zizmor: ignore[cache-poisoning] server e2e dependency cache only; release/publish steps rebuild separately | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: "pnpm" | |
| cache-dependency-path: "pnpm-lock.yaml" | |
| - name: install dependencies | |
| run: | | |
| pnpm install | |
| - name: Install golang-migrate for Clickhouse migrations | |
| run: | | |
| curl --fail --location --retry 5 --retry-delay 2 --retry-all-errors \ | |
| --output migrate.linux-amd64.tar.gz \ | |
| https://github.com/golang-migrate/migrate/releases/download/v4.19.1/migrate.linux-amd64.tar.gz | |
| tar xzf migrate.linux-amd64.tar.gz | |
| sudo mv migrate /usr/bin/migrate | |
| which migrate | |
| - name: Load default env | |
| run: | | |
| cp .env.dev.example .env | |
| - name: Run + migrate | |
| run: | | |
| docker compose -f docker-compose.dev.yml up -d --wait --wait-timeout 180 | |
| docker compose ps | |
| - name: Ensure Docker dependencies are healthy | |
| run: | | |
| if docker compose ps | grep "(unhealthy)"; then | |
| echo "One or more services are unhealthy" | |
| exit 1 | |
| else | |
| echo "All services are healthy" | |
| fi | |
| - name: Seed DB | |
| run: | | |
| pnpm run db:migrate | |
| pnpm --filter=shared run ch:up | |
| pnpm --filter=shared run db:seed:examples | |
| - name: Build | |
| run: pnpm run build | |
| env: | |
| NODE_OPTIONS: --max_old_space_size=8192 | |
| # TypeScript is checked explicitly in the lint job; skip duplicate | |
| # Next.js type checks in test builds to reduce CI runtime. | |
| NEXT_IGNORE_BUILD_ERRORS: "true" | |
| - name: Run server | |
| run: (pnpm run start&) | |
| - name: Check worker health | |
| run: | | |
| timeout 10 bash -c 'until curl -f http://localhost:3030/api/health; do sleep 2; done' | |
| - name: Check server health | |
| run: | | |
| timeout 10 bash -c 'until curl -f http://localhost:3000/api/public/health; do sleep 2; done' | |
| - name: Run e2e tests | |
| run: pnpm --filter=web run test:e2e:server | |
| all-ci-passed: | |
| # This allows us to have a branch protection rule for tests and deploys with matrix | |
| runs-on: blacksmith-4vcpu-ubuntu-2404 | |
| needs: | |
| [ | |
| lint, | |
| prettier-check, | |
| tests-eslint-plugin, | |
| tests-web, | |
| tests-worker, | |
| test-worker-llm-connections, | |
| e2e-tests, | |
| test-docker-build, | |
| e2e-server-tests, | |
| ] | |
| if: always() | |
| outputs: | |
| success: ${{ steps.set-success-output.outputs.success }} | |
| steps: | |
| - name: Set check result as an output | |
| id: set-success-output | |
| run: | | |
| if [[ "${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}" == "true" ]]; then | |
| echo "success=false" >> $GITHUB_OUTPUT | |
| else | |
| echo "success=true" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Successful deploy | |
| if: ${{ !(contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')) }} | |
| run: exit 0 | |
| working-directory: . | |
| - name: Failing deploy | |
| if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }} | |
| run: exit 1 | |
| working-directory: . | |
| - name: Notify Slack | |
| if: failure() && github.event_name == 'push' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/')) | |
| uses: slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3 | |
| with: | |
| webhook: ${{ secrets.SLACK_WEBHOOK_URL }} | |
| webhook-type: incoming-webhook | |
| payload: | | |
| { | |
| "text": "❌ CI failed on ${{ github.ref_name }}", | |
| "blocks": [ | |
| { | |
| "type": "header", | |
| "text": { | |
| "type": "plain_text", | |
| "text": "❌ CI Failed", | |
| "emoji": true | |
| } | |
| }, | |
| { | |
| "type": "section", | |
| "fields": [ | |
| { | |
| "type": "mrkdwn", | |
| "text": "*Branch/Tag:*\n`${{ github.ref_name }}`" | |
| }, | |
| { | |
| "type": "mrkdwn", | |
| "text": "*Triggered by:*\n${{ github.actor }}" | |
| } | |
| ] | |
| }, | |
| { | |
| "type": "actions", | |
| "elements": [ | |
| { | |
| "type": "button", | |
| "text": { | |
| "type": "plain_text", | |
| "text": "View Workflow Logs", | |
| "emoji": true | |
| }, | |
| "url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}", | |
| "style": "danger" | |
| } | |
| ] | |
| } | |
| ] | |
| } | |
| - name: Output job results | |
| run: | | |
| echo "Job results: ${STEPS_SET_SUCCESS_OUTPUT_OUTPUTS_SUCCESS}" | |
| env: | |
| STEPS_SET_SUCCESS_OUTPUT_OUTPUTS_SUCCESS: ${{ steps.set-success-output.outputs.success }} | |
| build-docker-image-release: | |
| needs: all-ci-passed | |
| # if something inside all-ci-passed was skipped, but everything that ran passed, we still want to deploy | |
| if: always() && needs.all-ci-passed.outputs.success == 'true' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') | |
| environment: "protected branches" | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - component: web | |
| image_name: langfuse | |
| dockerfile: ./web/Dockerfile | |
| platform: linux/amd64 | |
| platform_tag: amd64 | |
| runner: blacksmith-4vcpu-ubuntu-2404 | |
| - component: web | |
| image_name: langfuse | |
| dockerfile: ./web/Dockerfile | |
| platform: linux/arm64 | |
| platform_tag: arm64 | |
| runner: blacksmith-4vcpu-ubuntu-2404-arm | |
| - component: worker | |
| image_name: langfuse-worker | |
| dockerfile: ./worker/Dockerfile | |
| platform: linux/amd64 | |
| platform_tag: amd64 | |
| runner: blacksmith-4vcpu-ubuntu-2404 | |
| - component: worker | |
| image_name: langfuse-worker | |
| dockerfile: ./worker/Dockerfile | |
| platform: linux/arm64 | |
| platform_tag: arm64 | |
| runner: blacksmith-4vcpu-ubuntu-2404-arm | |
| runs-on: ${{ matrix.runner }} | |
| permissions: | |
| packages: write | |
| contents: read | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - name: Set NEXT_PUBLIC_BUILD_ID | |
| run: echo "NEXT_PUBLIC_BUILD_ID=$(git rev-parse --short HEAD)" >> $GITHUB_ENV | |
| - name: Log in to the GitHub Container registry | |
| uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Log in to Docker Hub | |
| uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 | |
| with: | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - name: Setup Blacksmith Builder | |
| uses: useblacksmith/setup-docker-builder@722e97d12b1d06a961800dd6c05d79d951ad3c80 # v1.8.0 | |
| - name: Extract metadata (labels) for Docker | |
| id: meta | |
| uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0 | |
| with: | |
| images: | | |
| ghcr.io/langfuse/${{ matrix.image_name }} | |
| langfuse/${{ matrix.image_name }} | |
| flavor: | | |
| latest=false | |
| tags: | | |
| type=ref,event=branch | |
| type=ref,event=pr | |
| type=sha | |
| type=semver,pattern={{version}} | |
| type=semver,pattern={{major}}.{{minor}},enable=${{ !contains(github.ref, '-rc') }} | |
| type=semver,pattern={{major}},enable=${{ !contains(github.ref, '-rc') }} | |
| type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v3') && !contains(github.ref, '-rc') }} | |
| - name: Build and push image by digest to GitHub Container Registry (${{ matrix.component }}, ${{ matrix.platform_tag }}) | |
| id: build-ghcr | |
| uses: useblacksmith/build-push-action@fb9e3e6a9299c78462bfadd0d93352c316adc9b8 # v2.2.0 | |
| with: | |
| context: . | |
| file: ${{ matrix.dockerfile }} | |
| outputs: type=image,name=ghcr.io/langfuse/${{ matrix.image_name }},push-by-digest=true,name-canonical=true,push=true | |
| labels: ${{ steps.meta.outputs.labels }} | |
| platforms: ${{ matrix.platform }} | |
| provenance: false | |
| sbom: false | |
| - name: Build and push image by digest to Docker Hub (${{ matrix.component }}, ${{ matrix.platform_tag }}) | |
| id: build-dockerhub | |
| uses: useblacksmith/build-push-action@fb9e3e6a9299c78462bfadd0d93352c316adc9b8 # v2.2.0 | |
| with: | |
| context: . | |
| file: ${{ matrix.dockerfile }} | |
| outputs: type=image,name=langfuse/${{ matrix.image_name }},push-by-digest=true,name-canonical=true,push=true | |
| labels: ${{ steps.meta.outputs.labels }} | |
| platforms: ${{ matrix.platform }} | |
| provenance: false | |
| sbom: false | |
| - name: Record pushed digests | |
| env: | |
| DIGEST_GHCR: ${{ steps.build-ghcr.outputs.digest }} | |
| DIGEST_DOCKERHUB: ${{ steps.build-dockerhub.outputs.digest }} | |
| PLATFORM_TAG: ${{ matrix.platform_tag }} | |
| run: | | |
| if [ -z "$DIGEST_GHCR" ] || [ -z "$DIGEST_DOCKERHUB" ]; then | |
| echo "Missing registry digest output" | |
| exit 1 | |
| fi | |
| mkdir -p "$RUNNER_TEMP/digests/ghcr" "$RUNNER_TEMP/digests/dockerhub" | |
| printf '%s\n' "$DIGEST_GHCR" > "$RUNNER_TEMP/digests/ghcr/${PLATFORM_TAG}.txt" | |
| printf '%s\n' "$DIGEST_DOCKERHUB" > "$RUNNER_TEMP/digests/dockerhub/${PLATFORM_TAG}.txt" | |
| - name: Upload release digests | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: release-digests-${{ matrix.component }}-${{ matrix.platform_tag }} | |
| path: | | |
| ${{ runner.temp }}/digests/ghcr/${{ matrix.platform_tag }}.txt | |
| ${{ runner.temp }}/digests/dockerhub/${{ matrix.platform_tag }}.txt | |
| if-no-files-found: error | |
| publish-docker-image-release: | |
| needs: | |
| - build-docker-image-release | |
| if: always() && needs.build-docker-image-release.result == 'success' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') | |
| environment: "protected branches" | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - component: web | |
| image_name: langfuse | |
| - component: worker | |
| image_name: langfuse-worker | |
| runs-on: blacksmith-4vcpu-ubuntu-2404 | |
| permissions: | |
| packages: write | |
| contents: read | |
| steps: | |
| - name: Log in to the GitHub Container registry | |
| uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Log in to Docker Hub | |
| uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 | |
| with: | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - name: Setup Blacksmith Builder | |
| uses: useblacksmith/setup-docker-builder@722e97d12b1d06a961800dd6c05d79d951ad3c80 # v1.8.0 | |
| - name: Download release digests | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| pattern: release-digests-${{ matrix.component }}-* | |
| merge-multiple: true | |
| path: ${{ runner.temp }}/digests | |
| - name: Extract metadata (tags) for GitHub Container Registry | |
| id: meta-ghcr | |
| uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0 | |
| with: | |
| images: ghcr.io/langfuse/${{ matrix.image_name }} | |
| flavor: | | |
| latest=false | |
| tags: | | |
| type=ref,event=branch | |
| type=ref,event=pr | |
| type=sha | |
| type=semver,pattern={{version}} | |
| type=semver,pattern={{major}}.{{minor}},enable=${{ !contains(github.ref, '-rc') }} | |
| type=semver,pattern={{major}},enable=${{ !contains(github.ref, '-rc') }} | |
| type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v3') && !contains(github.ref, '-rc') }} | |
| - name: Extract metadata (tags) for Docker Hub | |
| id: meta-dockerhub | |
| uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0 | |
| with: | |
| images: langfuse/${{ matrix.image_name }} | |
| flavor: | | |
| latest=false | |
| tags: | | |
| type=ref,event=branch | |
| type=ref,event=pr | |
| type=sha | |
| type=semver,pattern={{version}} | |
| type=semver,pattern={{major}}.{{minor}},enable=${{ !contains(github.ref, '-rc') }} | |
| type=semver,pattern={{major}},enable=${{ !contains(github.ref, '-rc') }} | |
| type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v3') && !contains(github.ref, '-rc') }} | |
| - name: Publish multi-platform manifest to GitHub Container Registry | |
| env: | |
| STEPS_META_GHCR_OUTPUTS_TAGS: ${{ steps.meta-ghcr.outputs.tags }} | |
| IMAGE_NAME: ${{ matrix.image_name }} | |
| COMPONENT: ${{ matrix.component }} | |
| run: | | |
| ghcr_tags=() | |
| ghcr_sources=() | |
| while IFS= read -r tag; do | |
| [ -n "$tag" ] || continue | |
| ghcr_tags+=("-t" "$tag") | |
| done <<EOF | |
| ${STEPS_META_GHCR_OUTPUTS_TAGS} | |
| EOF | |
| shopt -s nullglob | |
| for digest_file in "$RUNNER_TEMP"/digests/ghcr/*.txt; do | |
| digest="$(cat "$digest_file")" | |
| ghcr_sources+=("ghcr.io/langfuse/${IMAGE_NAME}@$digest") | |
| done | |
| if [ "${#ghcr_sources[@]}" -lt 2 ]; then | |
| echo "Expected amd64 and arm64 GHCR digests for $COMPONENT" | |
| exit 1 | |
| fi | |
| docker buildx imagetools create "${ghcr_tags[@]}" "${ghcr_sources[@]}" | |
| - name: Publish multi-platform manifest to Docker Hub | |
| env: | |
| STEPS_META_DOCKERHUB_OUTPUTS_TAGS: ${{ steps.meta-dockerhub.outputs.tags }} | |
| IMAGE_NAME: ${{ matrix.image_name }} | |
| COMPONENT: ${{ matrix.component }} | |
| run: | | |
| dockerhub_tags=() | |
| dockerhub_sources=() | |
| while IFS= read -r tag; do | |
| [ -n "$tag" ] || continue | |
| dockerhub_tags+=("-t" "$tag") | |
| done <<EOF | |
| ${STEPS_META_DOCKERHUB_OUTPUTS_TAGS} | |
| EOF | |
| shopt -s nullglob | |
| for digest_file in "$RUNNER_TEMP"/digests/dockerhub/*.txt; do | |
| digest="$(cat "$digest_file")" | |
| dockerhub_sources+=("langfuse/${IMAGE_NAME}@$digest") | |
| done | |
| if [ "${#dockerhub_sources[@]}" -lt 2 ]; then | |
| echo "Expected amd64 and arm64 Docker Hub digests for $COMPONENT" | |
| exit 1 | |
| fi | |
| docker buildx imagetools create "${dockerhub_tags[@]}" "${dockerhub_sources[@]}" | |
| - name: Inspect published manifests | |
| run: | | |
| ghcr_first_tag="$(printf '%s\n' "${STEPS_META_GHCR_OUTPUTS_TAGS}" | sed -n '1p')" | |
| dockerhub_first_tag="$(printf '%s\n' "${STEPS_META_DOCKERHUB_OUTPUTS_TAGS}" | sed -n '1p')" | |
| docker buildx imagetools inspect "$ghcr_first_tag" | |
| docker buildx imagetools inspect "$dockerhub_first_tag" | |
| env: | |
| STEPS_META_GHCR_OUTPUTS_TAGS: ${{ steps.meta-ghcr.outputs.tags }} | |
| STEPS_META_DOCKERHUB_OUTPUTS_TAGS: ${{ steps.meta-dockerhub.outputs.tags }} | |
| notify-docker-image-release: | |
| needs: | |
| - build-docker-image-release | |
| - publish-docker-image-release | |
| if: always() && github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') | |
| runs-on: blacksmith-4vcpu-ubuntu-2404 | |
| steps: | |
| - name: Fail when a release image job failed | |
| if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') | |
| run: exit 1 | |
| - name: Notify Slack | |
| if: failure() | |
| uses: slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3 | |
| with: | |
| webhook: ${{ secrets.SLACK_WEBHOOK_URL }} | |
| webhook-type: incoming-webhook | |
| payload: | | |
| { | |
| "text": "❌ Docker release failed on ${{ github.ref_name }}", | |
| "blocks": [ | |
| { | |
| "type": "header", | |
| "text": { | |
| "type": "plain_text", | |
| "text": "❌ Docker Release Failed", | |
| "emoji": true | |
| } | |
| }, | |
| { | |
| "type": "section", | |
| "fields": [ | |
| { | |
| "type": "mrkdwn", | |
| "text": "*Tag:*\n`${{ github.ref_name }}`" | |
| }, | |
| { | |
| "type": "mrkdwn", | |
| "text": "*Triggered by:*\n${{ github.actor }}" | |
| } | |
| ] | |
| }, | |
| { | |
| "type": "actions", | |
| "elements": [ | |
| { | |
| "type": "button", | |
| "text": { | |
| "type": "plain_text", | |
| "text": "View Workflow Logs", | |
| "emoji": true | |
| }, | |
| "url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}", | |
| "style": "danger" | |
| } | |
| ] | |
| } | |
| ] | |
| } |