Skip to content

feat(metrics): characterize-metric-anomaly endpoint and MCP tool #138597

feat(metrics): characterize-metric-anomaly endpoint and MCP tool

feat(metrics): characterize-metric-anomaly endpoint and MCP tool #138597

Workflow file for this run

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."