Skip to content

fix(signals): evict stale db connections in temporal activities #108390

fix(signals): evict stale db connections in temporal activities

fix(signals): evict stale db connections in temporal activities #108390

Workflow file for this run

name: MCP CI
on:
push:
branches: [master]
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
permissions:
contents: read
pull-requests: read
jobs:
changes:
runs-on: ubuntu-24.04
timeout-minutes: 5
name: Determine need to run MCP checks
outputs:
mcp: ${{ steps.filter.outputs.mcp || 'true' }}
steps:
- uses: actions/checkout@v6
# 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/**'
# 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@v6
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: 3.12.12
- name: Install uv
uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0
with:
version: '0.10.2' # 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@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- name: Fix node-gyp permissions
run: chmod +x ~/setup-pnpm/node_modules/.pnpm/pnpm@*/node_modules/pnpm/dist/node_modules/node-gyp/gyp/gyp_main.py
- name: Setup Node.js
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version-file: .nvmrc
cache: 'pnpm'
- 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
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@v6
- name: Setup pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- name: Fix node-gyp permissions
run: chmod +x ~/setup-pnpm/node_modules/.pnpm/pnpm@*/node_modules/pnpm/dist/node_modules/node-gyp/gyp/gyp_main.py
- name: Setup Node.js
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version-file: .nvmrc
cache: 'pnpm'
- 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: 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
if: needs.changes.outputs.mcp == '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'
# 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@v6
with:
fetch-depth: 1
- 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.12.12
- name: Install uv
uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0
with:
version: '0.10.2' # 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@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- name: Fix node-gyp permissions
run: chmod +x ~/setup-pnpm/node_modules/.pnpm/pnpm@*/node_modules/pnpm/dist/node_modules/node-gyp/gyp/gyp_main.py
- name: Setup Node.js
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version-file: .nvmrc
cache: 'pnpm'
- 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@0b1efabc08b657293548b77fb76cc02d26091c7e
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
run: |
cargo install sqlx-cli --version 0.8.0 --features postgres --no-default-features --locked
- 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: 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: 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
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('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: 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@v6
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."