feat(metrics): characterize-metric-anomaly endpoint and MCP tool #138597
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
| name: MCP CI | |
| on: | |
| push: | |
| branches: [master] | |
| pull_request: | |
| # Draft PRs run build + unit tests only; the integration tests (which boot | |
| # the full PostHog backend and trigger on any Python change) run when the | |
| # PR is marked ready for review — that full run is the merge gate. | |
| types: [opened, synchronize, reopened, ready_for_review] | |
| # Required so `MCP Tests Pass` (a required status check) reports on merge | |
| # queue entries — without it the queue would wait for the check until timeout. | |
| # The jobs themselves skip on merge_group (the suite already runs in full on | |
| # every PR) and the aggregator treats skipped as success. | |
| merge_group: | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} | |
| cancel-in-progress: ${{ github.event_name == 'pull_request' }} | |
| permissions: | |
| contents: read | |
| pull-requests: read | |
| env: | |
| DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} | |
| DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} | |
| jobs: | |
| changes: | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 5 | |
| name: Determine need to run MCP checks | |
| if: github.event_name != 'merge_group' | |
| outputs: | |
| mcp: ${{ steps.filter.outputs.mcp || 'true' }} | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| # MCP integration tests boot the full PostHog backend (Django web server, | |
| # Celery worker, migrations, demo data), so the filter mirrors the Dagster | |
| # approach: trigger on any Python change across the app. | |
| - uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 | |
| id: app-token | |
| if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository | |
| with: | |
| client-id: ${{ secrets.GH_APP_POSTHOG_PATHS_FILTER_APP_ID }} | |
| private-key: ${{ secrets.GH_APP_POSTHOG_PATHS_FILTER_PRIVATE_KEY }} | |
| - uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1 | |
| id: filter | |
| if: github.event_name != 'push' # Run all tests on master push | |
| with: | |
| token: ${{ steps.app-token.outputs.token || github.token }} | |
| filters: | | |
| mcp: | |
| # MCP service and per-product tool/UI-app configs | |
| - 'services/mcp/**' | |
| - 'products/*/mcp/**' | |
| # MCP integration tests exercise the full PostHog backend | |
| - 'posthog/**/*.py' | |
| - 'ee/**/*.py' | |
| - 'common/**/*.py' | |
| - 'products/*/backend/**/*.py' | |
| # Query schemas used by MCP analytics tools | |
| - 'frontend/src/queries/schema.json' | |
| # Shared UI component library (used by MCP UI apps) | |
| - 'common/mosaic/**' | |
| # Code generation tooling | |
| - 'tools/openapi-codegen/**' | |
| # Django entry point (manage.py migrate / generate_demo_data) | |
| - manage.py | |
| # Python dependencies (build + integration-tests run `uv sync --frozen`) | |
| - pyproject.toml | |
| - uv.lock | |
| # Node runtime + workspace dependencies | |
| - .nvmrc | |
| - pnpm-lock.yaml | |
| # Docker compose stack (dev.yml extends base.yml) | |
| - docker-compose.base.yml | |
| - docker-compose.dev.yml | |
| - docker-compose.profiles.yml | |
| # Docker helper scripts invoked in CI setup | |
| - bin/ci-wait-for-docker | |
| - bin/wait-for-docker | |
| # Database init scripts used by docker compose | |
| - 'docker/postgres-init-scripts/**' | |
| # Persons DB migrations (sqlx migrate run in integration tests) | |
| - 'rust/persons_migrations/**' | |
| # ClickHouse UDF copied in integration test setup | |
| - 'posthog/user_scripts/latest_user_defined_function.xml' | |
| # Composite actions used by the workflow | |
| - '.github/actions/commit-snapshots/**' | |
| # hogli db setup (db:restore-test-db). Scoped to the | |
| # modules that command actually imports — framework, | |
| # manifest, boot modules, db_schema — so unrelated hogli | |
| # commands (doctor, devbox, ...) don't trigger this suite. | |
| - 'tools/hogli/**' | |
| - 'hogli.yaml' | |
| - 'tools/hogli-commands/hogli_commands/db_schema.py' | |
| - 'tools/hogli-commands/hogli_commands/prechecks.py' | |
| - 'tools/hogli-commands/hogli_commands/telemetry_props.py' | |
| - 'tools/hogli-commands/hogli_commands/hint_hook.py' | |
| - 'tools/hogli-commands/hogli_commands/hints.py' | |
| # CI config | |
| - '.github/workflows/ci-mcp.yml' | |
| - '.github/workflows/mcp-publish.yml' | |
| build: | |
| name: Build Package | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| needs: changes | |
| if: needs.changes.outputs.mcp == 'true' | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Set up Python | |
| uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 | |
| with: | |
| python-version: 3.13.13 | |
| token: ${{ github.token }} | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 | |
| with: | |
| version: '0.11.14' # pinned: unpinned setup-uv calls GH API on every job, exhausts rate limit | |
| enable-cache: true | |
| cache-dependency-glob: uv.lock | |
| save-cache: ${{ github.ref == 'refs/heads/master' }} | |
| - name: Install Python dependencies | |
| run: UV_PROJECT_ENVIRONMENT=$pythonLocation uv sync --frozen --dev | |
| - name: Setup pnpm | |
| uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 | |
| with: | |
| node-version-file: .nvmrc | |
| cache: 'pnpm' | |
| token: ${{ github.token }} | |
| - name: Install dependencies | |
| run: pnpm --filter=@posthog/mcp... --filter='./products/*' install --frozen-lockfile | |
| - name: Build package | |
| run: cd services/mcp && pnpm build | |
| - name: Type check (tsgo) | |
| run: cd services/mcp && pnpm typecheck | |
| - name: Check MCP schema is up to date | |
| run: | | |
| ./bin/hogli build:schema-mcp | |
| if ! git diff --exit-code services/mcp/schema/tool-inputs.json; then | |
| echo "" | |
| echo "::error::MCP tool-inputs.json is out of date. Run 'hogli build:schema-mcp' and commit the result." | |
| exit 1 | |
| fi | |
| - name: Check generated UI apps are up to date | |
| run: | | |
| cd services/mcp && pnpm run generate:ui-apps | |
| if ! git diff --exit-code -- src/ui-apps/apps/generated/ src/resources/ui-apps.generated.ts; then | |
| echo "" | |
| echo "::warning::The following generated files are out of date:" | |
| git diff --name-only -- src/ui-apps/apps/generated/ src/resources/ui-apps.generated.ts | |
| echo "" | |
| echo "::error::Generated UI app files are out of date. Run 'pnpm --filter=@posthog/mcp run generate:ui-apps' and commit the result." | |
| exit 1 | |
| fi | |
| - name: Lint tool names | |
| run: cd services/mcp && pnpm lint-tool-names | |
| unit-tests: | |
| name: Unit Tests | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| needs: changes | |
| if: needs.changes.outputs.mcp == 'true' | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Setup pnpm | |
| uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 | |
| with: | |
| node-version-file: .nvmrc | |
| cache: 'pnpm' | |
| token: ${{ github.token }} | |
| - name: Install dependencies | |
| run: pnpm --filter=@posthog/mcp... --filter='./products/*' install --frozen-lockfile | |
| # The workers-pool project in vitest.config.mts parses wrangler.jsonc | |
| # up front and needs `assets.directory` (./public/) populated and | |
| # `shared/guidelines.md` (imported by src/mcp.ts) on disk. Both are | |
| # gitignored: public/ is produced by `pnpm build` (UI apps) and | |
| # guidelines.md by the wrangler build command. | |
| - name: Run wrangler pre-build steps | |
| working-directory: services/mcp | |
| run: | | |
| pnpm build | |
| pnpm exec tsx scripts/copy-instructions.ts | |
| - name: Run unit tests | |
| run: | | |
| pnpm --filter=@posthog/mcp run test -u | |
| if [ -n "$(git status --porcelain -- services/mcp/tests/unit)" ]; then | |
| echo "" | |
| git status --short -- services/mcp/tests/unit | |
| echo "::error::MCP unit test artifacts are out of date. Run 'pnpm --filter=@posthog/mcp run test -u' and commit the result." | |
| exit 1 | |
| fi | |
| - name: Run Hono unit tests | |
| run: cd services/mcp && pnpm run test:hono | |
| - name: Create snapshot patch | |
| id: create-snapshot-patch | |
| if: ${{ always() && github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && !contains(github.actor, '[bot]') && github.actor != 'posthog-bot' && !contains(github.actor, 'github-actions') }} | |
| run: | | |
| SNAPSHOT_PATH="services/mcp/tests/unit" | |
| if [ -z "$(git status --porcelain -- "$SNAPSHOT_PATH")" ]; then | |
| echo "No MCP unit test changes" | |
| echo "has-changes=false" >> $GITHUB_OUTPUT | |
| else | |
| echo "MCP unit test changes detected:" | |
| git status --short -- "$SNAPSHOT_PATH" | |
| git add -N "$SNAPSHOT_PATH" | |
| mkdir -p /tmp/patches | |
| git diff --binary --full-index "$SNAPSHOT_PATH" > /tmp/patches/mcp-unit-tests.patch | |
| echo "has-changes=true" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Upload snapshot patch | |
| if: ${{ steps.create-snapshot-patch.outputs.has-changes == 'true' && always() }} | |
| uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 | |
| with: | |
| name: mcp-snapshot-patch | |
| path: /tmp/patches/mcp-unit-tests.patch | |
| retention-days: 1 | |
| if-no-files-found: ignore | |
| integration-tests: | |
| name: Integration Tests (depot-ubuntu-latest-4) | |
| runs-on: depot-ubuntu-latest-4 | |
| needs: changes | |
| timeout-minutes: 30 | |
| # Skipped on drafts: these boot the full PostHog backend and trigger on any | |
| # Python change, so they're the expensive half of this workflow. Build and | |
| # unit tests still run on drafts; the `MCP Tests Pass` aggregator treats | |
| # skipped as success, and the ready-for-review run is the merge gate. | |
| if: needs.changes.outputs.mcp == 'true' && github.event.pull_request.draft != true | |
| env: | |
| OPT_OUT_CAPTURE: 1 | |
| SECRET_KEY: '6b01eee4f945ca25045b5aab440b953461faf08693a9abbf1166dc7c6b9772da' | |
| DATABASE_URL: 'postgres://posthog:posthog@localhost:5432/posthog' | |
| PERSONS_DB_WRITER_URL: 'postgres://posthog:posthog@localhost:5432/posthog_persons' | |
| REDIS_URL: 'redis://localhost' | |
| CLICKHOUSE_HOST: 'localhost' | |
| CLICKHOUSE_SECURE: 'False' | |
| CLICKHOUSE_VERIFY: 'False' | |
| OBJECT_STORAGE_ENABLED: 'True' | |
| OBJECT_STORAGE_ENDPOINT: 'http://localhost:19000' | |
| OBJECT_STORAGE_ACCESS_KEY_ID: 'object_storage_root_user' | |
| OBJECT_STORAGE_SECRET_ACCESS_KEY: 'object_storage_root_password' | |
| SKIP_SERVICE_VERSION_REQUIREMENTS: 1 | |
| NO_RESTART_LOOP: 1 | |
| DISABLE_SECURE_SSL_REDIRECT: 'True' | |
| CLICKHOUSE_LOGS_CLUSTER_SECURE: 'False' | |
| CELERY_METRICS_PORT: 8999 | |
| POSTHOG_FEATURE_FLAGS_FORCE_ENABLED: 'logs-alerting,dashboard-widgets,product-business-knowledge' | |
| # Required for the feature_flags test_evaluation endpoint, which authenticates | |
| # historical-flag override requests to the Rust flags service. Any non-empty | |
| # value is fine for CI — the Rust service in the test stack accepts it. | |
| INTERNAL_REQUEST_TOKEN: 'mcp-ci-internal-request-token' | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| # Deep clone so we can compute the merge-base with the PR base ref | |
| # for the schema cache key (matches ci-backend.yml's turbo-discover). | |
| fetch-depth: 1000 | |
| filter: blob:none | |
| - name: Clean up data directories with container permissions | |
| run: | | |
| [ -d "data" ] && docker run --rm -v "$(pwd)/data:/data" alpine sh -c "rm -rf /data/seaweedfs /data/minio" || true | |
| continue-on-error: true | |
| - name: Stop/Start stack with Docker Compose | |
| shell: bash | |
| env: | |
| COMPOSE_PROFILES: temporal | |
| run: | | |
| cp posthog/user_scripts/latest_user_defined_function.xml docker/clickhouse/user_defined_function.xml | |
| ( | |
| max_attempts=3 | |
| attempt=1 | |
| delay=5 | |
| while [ $attempt -le $max_attempts ]; do | |
| echo "Attempt $attempt of $max_attempts to start stack..." | |
| if docker compose -f docker-compose.dev.yml down && \ | |
| docker compose -f docker-compose.dev.yml -f docker-compose.profiles.yml up -d; then | |
| echo "Stack started successfully" | |
| exit 0 | |
| fi | |
| echo "Failed to start stack on attempt $attempt" | |
| if [ $attempt -lt $max_attempts ]; then | |
| sleep_time=$((delay * 2 ** (attempt - 1))) | |
| echo "Waiting ${sleep_time} seconds before retry..." | |
| sleep $sleep_time | |
| fi | |
| attempt=$((attempt + 1)) | |
| done | |
| echo "Failed to start stack after $max_attempts attempts" | |
| exit 1 | |
| ) & | |
| - name: Add service hostnames to /etc/hosts | |
| shell: bash | |
| run: echo "127.0.0.1 db redis7 kafka clickhouse clickhouse-coordinator objectstorage seaweedfs temporal" | sudo tee -a /etc/hosts | |
| - name: Set up Python | |
| uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 | |
| with: | |
| python-version: 3.13.13 | |
| token: ${{ github.token }} | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 | |
| with: | |
| version: '0.11.14' # pinned: unpinned setup-uv calls GH API on every job, exhausts rate limit | |
| enable-cache: true | |
| cache-dependency-glob: uv.lock | |
| save-cache: ${{ github.ref == 'refs/heads/master' }} | |
| - name: Install SAML (python3-saml) dependencies | |
| shell: bash | |
| run: | | |
| sudo apt-get update && sudo apt-get install libxml2-dev libxmlsec1-dev libxmlsec1-openssl | |
| - name: Setup pnpm | |
| uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 | |
| with: | |
| node-version-file: .nvmrc | |
| cache: 'pnpm' | |
| token: ${{ github.token }} | |
| - name: Install Python dependencies | |
| shell: bash | |
| run: | | |
| UV_PROJECT_ENVIRONMENT=$pythonLocation uv sync --frozen --dev | |
| - name: Install Node dependencies | |
| run: pnpm --filter=@posthog/mcp... --filter='./products/*' install --frozen-lockfile | |
| - name: Set up needed files | |
| shell: bash | |
| run: | | |
| mkdir -p frontend/dist | |
| touch frontend/dist/index.html | |
| touch frontend/dist/layout.html | |
| touch frontend/dist/exporter.html | |
| - name: Install Rust | |
| uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 | |
| with: | |
| toolchain: 1.91.1 | |
| components: cargo | |
| - name: Cache Rust dependencies | |
| uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 | |
| with: | |
| shared-key: 'v2-rust-mcp' | |
| workspaces: rust | |
| save-if: ${{ github.ref == 'refs/heads/master' }} | |
| - name: Install sqlx-cli | |
| uses: ./.github/actions/setup-sqlx-cli | |
| - name: Build Rust flags service | |
| working-directory: rust | |
| env: | |
| # Debug builds compile much faster than release (~4 min vs ~9 min). | |
| # We disable debug assertions because the codebase contains | |
| # debug_assert! checks that intentionally panic on data invariant | |
| # violations, which would crash the service mid-test-run. | |
| RUSTFLAGS: '-C debug-assertions=no' | |
| run: cargo build -p feature-flags | |
| - name: Download MaxMind Database | |
| run: ./bin/download-mmdb | |
| - name: Log in to Docker Hub | |
| if: ${{ env.DOCKERHUB_USERNAME != '' && env.DOCKERHUB_TOKEN != '' }} | |
| uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 | |
| with: | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - name: Start Docker services | |
| shell: bash | |
| env: | |
| COMPOSE_FILE: docker-compose.dev.yml:docker-compose.profiles.yml | |
| COMPOSE_PROFILES: temporal | |
| run: bin/ci-wait-for-docker launch --down | |
| - name: Wait for Docker services | |
| shell: bash | |
| env: | |
| COMPOSE_FILE: docker-compose.dev.yml:docker-compose.profiles.yml | |
| COMPOSE_PROFILES: temporal | |
| run: bin/ci-wait-for-docker wait temporal | |
| - name: Fetch current PR base for merge-base | |
| if: github.event_name == 'pull_request' | |
| env: | |
| BASE_REF: ${{ github.event.pull_request.base.ref }} | |
| run: git fetch --no-tags --depth=1000 --filter=blob:none origin "$BASE_REF:refs/remotes/origin/$BASE_REF" | |
| - name: Compute schema cache key from merge-base | |
| id: schema-key | |
| if: github.event_name == 'pull_request' | |
| env: | |
| BASE_REF: ${{ github.event.pull_request.base.ref }} | |
| run: | | |
| # HEAD is the synthetic merge commit; HEAD^2 is the PR branch tip. | |
| # The fetch-depth:1000 checkout + base-ref fetch above ensure the | |
| # full ancestry needed to find the divergence point is available. | |
| MERGE_BASE=$(git merge-base HEAD^2 "origin/${BASE_REF}" 2>/dev/null || echo "") | |
| if [ -n "$MERGE_BASE" ]; then | |
| echo "key=posthog-schema-master-${MERGE_BASE}" >> $GITHUB_OUTPUT | |
| else | |
| echo "key=" >> $GITHUB_OUTPUT | |
| echo "::notice::merge-base not found (branch too stale?) — schema cache will be skipped" | |
| fi | |
| - name: Restore schema cache from master | |
| if: ${{ github.event_name == 'pull_request' && steps.schema-key.outputs.key != '' }} | |
| uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 | |
| with: | |
| path: schema.sql.gz | |
| key: ${{ steps.schema-key.outputs.key }} | |
| - name: Prime posthog from cached schema | |
| # No-op on cache miss; manage.py migrate below falls back to a full migrate. | |
| # MCP integration tests hit the live posthog DB, not test_posthog — that's | |
| # the only deviation from ci-backend.yml's equivalent step. | |
| if: ${{ github.event_name == 'pull_request' }} | |
| env: | |
| TARGET_DB: posthog | |
| run: | | |
| if [ ! -f schema.sql.gz ]; then | |
| echo "::notice::Schema cache miss — manage.py migrate will run from scratch" | |
| exit 0 | |
| fi | |
| mkdir -p .postgres-backups | |
| mv schema.sql.gz .postgres-backups/schema-latest.sql.gz | |
| ./bin/hogli db:restore-test-db | |
| # The dump is produced by ci-backend.yml, where TEST=1 activates | |
| # ProductDBRouter and routes the apps in products/db_routing.yaml to | |
| # separate databases: their tables are absent from the dump, but | |
| # django_migrations still records their migrations as applied. This job | |
| # configures no product DBs, so those apps live on the default DB — | |
| # drop their migration records so migrate below replays them from 0001 | |
| # and actually creates the tables. | |
| APPS=$(python - <<'EOF' | |
| import yaml | |
| with open("products/db_routing.yaml") as f: | |
| routes = yaml.safe_load(f)["routes"] | |
| print(",".join(sorted({"'%s'" % r["app_label"] for r in routes}))) | |
| EOF | |
| ) | |
| if [ -n "$APPS" ]; then | |
| docker compose -f docker-compose.dev.yml exec -T db psql -U posthog posthog \ | |
| -c "DELETE FROM django_migrations WHERE app IN ($APPS);" | |
| fi | |
| - name: Run migrations | |
| run: | | |
| # Create persons database and run sqlx migrations first | |
| DATABASE_URL="postgres://posthog:posthog@localhost:5432/posthog_persons" \ | |
| sqlx database create | |
| DATABASE_URL="postgres://posthog:posthog@localhost:5432/posthog_persons" \ | |
| sqlx migrate run --source rust/persons_migrations/ | |
| # Then run Django migrations. When the schema cache primed the DB above, | |
| # this only applies migrations added by the PR (the django_migrations | |
| # table from the dump tells Django which ones are already applied). | |
| python manage.py migrate --noinput | |
| python manage.py migrate_clickhouse | |
| - name: Start Rust flags service | |
| working-directory: rust | |
| env: | |
| BIND_HOST: '127.0.0.1' | |
| BIND_PORT: '3001' | |
| READ_DATABASE_URL: postgres://posthog:posthog@localhost:5432/posthog | |
| WRITE_DATABASE_URL: postgres://posthog:posthog@localhost:5432/posthog | |
| PERSONS_READ_DATABASE_URL: postgres://posthog:posthog@localhost:5432/posthog_persons | |
| PERSONS_WRITE_DATABASE_URL: postgres://posthog:posthog@localhost:5432/posthog_persons | |
| REDIS_URL: redis://localhost:6379/ | |
| CACHE_TTL_SECONDS: '0' | |
| RUST_BACKTRACE: '1' | |
| run: | | |
| ./target/debug/feature-flags > /tmp/rust-flags.log 2>&1 & | |
| RUST_FLAGS_PID=$! | |
| echo "RUST_FLAGS_PID=$RUST_FLAGS_PID" >> $GITHUB_ENV | |
| for i in {1..30}; do | |
| if ! kill -0 "$RUST_FLAGS_PID" 2>/dev/null; then | |
| echo "ERROR: Rust flags service exited unexpectedly" | |
| cat /tmp/rust-flags.log || true | |
| exit 1 | |
| fi | |
| if curl -s http://127.0.0.1:3001/_readiness > /dev/null 2>&1; then | |
| echo "Rust flags service is ready" | |
| break | |
| fi | |
| echo "Waiting for Rust flags service... ($i/30)" | |
| sleep 2 | |
| done | |
| if ! curl -s http://127.0.0.1:3001/_readiness > /dev/null 2>&1; then | |
| echo "ERROR: Rust flags service failed to start after 30 attempts" | |
| cat /tmp/rust-flags.log || true | |
| exit 1 | |
| fi | |
| - name: Seed test data | |
| run: python manage.py generate_demo_data --n-clusters 10 --days-past 7 --days-future 0 --skip-materialization --skip-flag-sync --skip-user-product-list | |
| - name: Create API key and extract test IDs | |
| run: | | |
| python -c " | |
| import django, os | |
| os.environ['DJANGO_SETTINGS_MODULE'] = 'posthog.settings' | |
| django.setup() | |
| from posthog.models import Organization, Team, User, EventDefinition | |
| from posthog.models.personal_api_key import PersonalAPIKey | |
| from posthog.models.utils import hash_key_value | |
| org = Organization.objects.first() | |
| team = Team.objects.first() | |
| user = User.objects.first() | |
| PersonalAPIKey.objects.create( | |
| user=user, | |
| label='mcp_ci_api_key', | |
| secure_value=hash_key_value('phx_e2e_demo_api_key'), | |
| scopes=['*'], | |
| ) | |
| # Ensure common event definitions exist for MCP integration tests | |
| for event_name in ['\$pageview', '\$pageleave', '\$autocapture', '\$screen']: | |
| EventDefinition.objects.get_or_create(name=event_name, team=team) | |
| print(f'TEST_ORG_ID={org.id}') | |
| print(f'TEST_PROJECT_ID={team.id}') | |
| " >> $GITHUB_ENV | |
| - name: Source celery queues | |
| run: | | |
| source ./bin/celery-queues.env | |
| echo "CELERY_WORKER_QUEUES=$CELERY_WORKER_QUEUES" >> $GITHUB_ENV | |
| - name: Start PostHog web & Celery worker | |
| run: | | |
| python manage.py run_autoreload_celery --type=worker &> /tmp/celery.log & | |
| python -m granian --interface asgi posthog.asgi:application --host 0.0.0.0 --port 8000 --workers 1 &> /tmp/server.log & | |
| - name: Wait for PostHog to be ready | |
| uses: iFaxity/wait-on-action@1fe019e0475491e9e8c4f421b6914ccc3ed8f99c # v1.2.1 | |
| with: | |
| resource: http://localhost:8000 | |
| timeout: 180000 | |
| interval: 2000 | |
| verbose: true | |
| - name: Run integration tests | |
| run: cd services/mcp && pnpm run test:integration | |
| env: | |
| TEST_POSTHOG_API_BASE_URL: http://localhost:8000 | |
| TEST_POSTHOG_PERSONAL_API_KEY: phx_e2e_demo_api_key | |
| TEST_ORG_ID: ${{ env.TEST_ORG_ID }} | |
| TEST_PROJECT_ID: ${{ env.TEST_PROJECT_ID }} | |
| - name: Show server logs on failure | |
| if: failure() | |
| run: cat /tmp/server.log || true | |
| - name: Show celery logs on failure | |
| if: failure() | |
| run: cat /tmp/celery.log || true | |
| - name: Show Rust flags service logs on failure | |
| if: failure() | |
| run: cat /tmp/rust-flags.log || true | |
| - name: Show docker compose logs on failure | |
| if: failure() | |
| shell: bash | |
| run: docker compose -f docker-compose.dev.yml logs | |
| handle-snapshots: | |
| name: Commit snapshot changes | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| needs: [changes, unit-tests] | |
| if: ${{ always() && needs.changes.outputs.mcp == 'true' && github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && !contains(github.actor, '[bot]') && github.actor != 'posthog-bot' && !contains(github.actor, 'github-actions') }} | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| steps: | |
| - name: Get app token | |
| id: app-token | |
| uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 | |
| with: | |
| client-id: ${{ secrets.GH_APP_POSTHOG_TESTS_APP_ID }} | |
| private-key: ${{ secrets.GH_APP_POSTHOG_TESTS_PRIVATE_KEY }} | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| ref: ${{ github.event.pull_request.head.sha }} | |
| repository: ${{ github.event.pull_request.head.repo.full_name }} | |
| token: ${{ steps.app-token.outputs.token }} | |
| fetch-depth: 1 | |
| - name: Download snapshot patches | |
| id: download-patches | |
| continue-on-error: true | |
| uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 | |
| with: | |
| pattern: mcp-snapshot-patch | |
| path: /tmp/snapshot-patches | |
| merge-multiple: true | |
| - name: Check for patches | |
| id: check-patches | |
| run: | | |
| if [ "${{ steps.download-patches.outcome }}" == "failure" ] || [ ! -d /tmp/snapshot-patches ]; then | |
| echo "has-patches=false" >> $GITHUB_OUTPUT | |
| echo "No snapshot patches found" | |
| exit 0 | |
| fi | |
| if [ -n "$(find /tmp/snapshot-patches -name '*.patch' -type f -size +0c 2>/dev/null)" ]; then | |
| echo "has-patches=true" >> $GITHUB_OUTPUT | |
| echo "Found patches:" | |
| ls -la /tmp/snapshot-patches/ | |
| else | |
| echo "has-patches=false" >> $GITHUB_OUTPUT | |
| echo "Patch files empty or missing - no snapshot changes" | |
| fi | |
| - name: Commit snapshots | |
| if: steps.check-patches.outputs.has-patches == 'true' | |
| uses: ./.github/actions/commit-snapshots | |
| with: | |
| workflow-type: backend | |
| patch-path: /tmp/snapshot-patches | |
| snapshot-path: 'services/mcp/tests/unit/' | |
| commit-message: 'test(mcp): update unit test snapshots' | |
| pr-number: ${{ github.event.pull_request.number }} | |
| repository: ${{ github.repository }} | |
| commit-sha: ${{ github.event.pull_request.head.sha }} | |
| branch-name: ${{ github.event.pull_request.head.ref }} | |
| github-token: ${{ steps.app-token.outputs.token }} | |
| # Collation job for required status check | |
| mcp_tests: | |
| needs: [changes, build, unit-tests, integration-tests, handle-snapshots] | |
| name: MCP Tests Pass | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| if: always() | |
| steps: | |
| - name: Check all MCP jobs | |
| run: | | |
| # Fail if change detection itself failed | |
| if [[ "${{ needs.changes.result }}" == "failure" ]]; then | |
| echo "Change detection job failed." | |
| exit 1 | |
| fi | |
| # Pass if no MCP changes detected (jobs were skipped) | |
| if [[ "${{ needs.changes.outputs.mcp }}" != "true" ]]; then | |
| echo "MCP checks were skipped (no relevant changes)." | |
| exit 0 | |
| fi | |
| if [[ "${{ needs.build.result }}" != "success" && "${{ needs.build.result }}" != "skipped" ]]; then | |
| echo "MCP build failed." | |
| exit 1 | |
| fi | |
| if [[ "${{ needs.unit-tests.result }}" != "success" && "${{ needs.unit-tests.result }}" != "skipped" ]]; then | |
| echo "MCP unit tests failed." | |
| exit 1 | |
| fi | |
| if [[ "${{ needs.integration-tests.result }}" != "success" && "${{ needs.integration-tests.result }}" != "skipped" ]]; then | |
| echo "MCP integration tests failed." | |
| exit 1 | |
| fi | |
| if [[ "${{ needs.handle-snapshots.result }}" == "failure" ]]; then | |
| echo "MCP snapshot commit job failed." | |
| exit 1 | |
| fi | |
| echo "All MCP checks passed." |