feat(desktop): launch team skills from pull request review #231475
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: Agent Skills | |
| # No paths filter on master pushes (matching the other ci-* workflows): the skills build | |
| # imports product code whose import graph any Python change can affect, so a filtered | |
| # master trigger lets unrelated commits break the build invisibly until the next | |
| # skills-path commit takes the blame. PR runs stay filtered via the changes job below. | |
| on: | |
| pull_request: | |
| push: | |
| branches: | |
| - master | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.event_name == 'push' && github.sha || github.head_ref || github.ref }} | |
| cancel-in-progress: ${{ github.event_name == 'pull_request' }} | |
| jobs: | |
| changes: | |
| permissions: | |
| contents: read | |
| pull-requests: read | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 5 | |
| name: Determine need to run skill checks | |
| outputs: | |
| skills: ${{ steps.filter.outputs.skills || 'true' }} | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| sparse-checkout: .github/actions/paths-filter | |
| sparse-checkout-cone-mode: false | |
| - 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: ${{ vars.GH_APP_POSTHOG_PATHS_FILTER_APP_ID }} | |
| private-key: ${{ secrets.GH_APP_POSTHOG_PATHS_FILTER_PRIVATE_KEY }} | |
| - uses: ./.github/actions/paths-filter | |
| id: filter | |
| if: github.event_name != 'push' | |
| with: | |
| token: ${{ steps.app-token.outputs.token || github.token }} | |
| filters: | | |
| skills: | |
| - 'products/*/skills/**' | |
| - 'products/*/backend/max_tools.py' | |
| - 'products/posthog_ai/**' | |
| - '.github/workflows/ci-agent-skills.yml' | |
| - '.github/actions/setup-uv/**' | |
| - 'services/mcp/schema/**' | |
| # check-skills zips products/*/skills/* via `hogli build:skills` | |
| # (see build.py's affected-path map), so posthog_ai's frontend | |
| # can't affect it. Scoped to posthog_ai, not | |
| # 'products/*/frontend/**', so a future deliberate frontend | |
| # include here isn't silently vetoed. | |
| - '!products/posthog_ai/frontend/**' | |
| check-skills: | |
| name: Check agent skills | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 30 | |
| needs: changes | |
| if: needs.changes.outputs.skills == 'true' | |
| permissions: | |
| contents: read | |
| env: | |
| DOCKERHUB_USERNAME: ${{ vars.DOCKERHUB_USER }} | |
| DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| # Deep enough to reach the PR's merge base, which keys the schema cache below. | |
| fetch-depth: 1000 | |
| filter: blob:none | |
| - name: Fetch base branch for merge-base computation | |
| if: github.event_name == 'pull_request' | |
| env: | |
| BASE_REF: ${{ github.event.pull_request.base.ref }} | |
| # Scoped, blob-less, no-tags: without an explicit refspec, git fetch | |
| # falls back to remote.origin.fetch and pulls every branch. | |
| run: git fetch --no-tags --depth=1000 --filter=blob:none origin "$BASE_REF:refs/remotes/origin/$BASE_REF" | |
| - name: Compute schema cache keys | |
| id: schema-key | |
| # Restores the schema dump ci-backend saves on master pushes, so the | |
| # migrate step below only tops up what is newer instead of replaying the | |
| # full migration history. On a PR the migration-set key is the merge-base's; | |
| # on a master push it is HEAD's. A cache miss falls through to a full | |
| # migrate, so an absent or wrong key is never a correctness risk. | |
| env: | |
| BASE_REF: ${{ github.event.pull_request.base.ref }} | |
| IS_PUSH: ${{ github.event_name == 'push' }} | |
| # Manual cache-bust knob. Keep in sync with SCHEMA_CACHE_EPOCH in | |
| # ci-backend.yml, which saves these caches on master pushes. | |
| SCHEMA_CACHE_EPOCH: v2 | |
| run: | | |
| if [ "$IS_PUSH" = "true" ]; then | |
| # Pushed commit is checked out directly: no merge commit, no base ref. | |
| # The per-SHA key would point at this same push's cache (saved | |
| # concurrently by ci-backend, so racy); rely on the stable | |
| # migration-set key below. | |
| REF=HEAD | |
| else | |
| # HEAD is the synthetic merge commit; HEAD^2 is the PR branch tip. | |
| MERGE_BASE=$(git merge-base HEAD^2 "origin/${BASE_REF}" 2>/dev/null || echo "") | |
| if [ -z "$MERGE_BASE" ]; then | |
| echo "key=" >> $GITHUB_OUTPUT | |
| echo "migrations_key=" >> $GITHUB_OUTPUT | |
| echo "::notice::merge-base not found (branch too stale?), so the schema cache is skipped" | |
| exit 0 | |
| fi | |
| # Routing decides which app labels reach the dump, so an edited routing | |
| # config matches no master dump: drop both keys and migrate from scratch. | |
| DB_ROUTING_BASE=$(git ls-tree -r --format='%(objectname) %(path)' "$MERGE_BASE" -- products/db_routing.yaml posthog/product_db_config.py posthog/product_db_router.py) | |
| DB_ROUTING_HEAD=$(git ls-tree -r --format='%(objectname) %(path)' HEAD -- products/db_routing.yaml posthog/product_db_config.py posthog/product_db_router.py) | |
| if [ "$DB_ROUTING_BASE" != "$DB_ROUTING_HEAD" ]; then | |
| echo "key=" >> $GITHUB_OUTPUT | |
| echo "migrations_key=" >> $GITHUB_OUTPUT | |
| echo "::notice::db routing config or router differs from the merge base, so the schema cache is skipped" | |
| exit 0 | |
| fi | |
| echo "key=posthog-schema-master-${MERGE_BASE}" >> $GITHUB_OUTPUT | |
| REF="$MERGE_BASE" | |
| fi | |
| # Last-resort restore prefix: matches the newest saved dump when the exact | |
| # keys miss. Gated on checkout age because a re-run of an old commit could | |
| # restore a dump newer than its code, which forward-only migrate cannot repair. | |
| HEAD_AGE_SECONDS=$(( $(date +%s) - $(git show -s --format=%ct HEAD) )) | |
| if [ "$HEAD_AGE_SECONDS" -lt 86400 ]; then | |
| echo "prefix_key=posthog-schema-mig-${SCHEMA_CACHE_EPOCH}-" >> $GITHUB_OUTPUT | |
| fi | |
| # Must match ci-backend.yml's save-side computation byte for byte or the key never hits. | |
| MIG_FILES=$(git ls-tree -r --format='%(objectname) %(path)' "$REF" \ | |
| | grep -E '/migrations/[^/]+\.py$|^[0-9a-f]+ uv\.lock$' \ | |
| | grep -vE '/(clickhouse|async_migrations)/migrations/' \ | |
| | LC_ALL=C sort) || true | |
| # Routing decides which app labels are in the dump, so these inputs belong in the key. | |
| DB_ROUTING=$(git ls-tree -r --format='%(objectname) %(path)' "$REF" -- products/db_routing.yaml posthog/product_db_config.py posthog/product_db_router.py) | |
| PG_IMAGE=$(git show "${REF}:docker-compose.base.yml" 2>/dev/null \ | |
| | grep -m1 -E '^[[:space:]]*image:[[:space:]]*postgres:' | tr -d '[:space:]') | |
| if [ -z "$MIG_FILES" ]; then | |
| echo "migrations_key=" >> $GITHUB_OUTPUT | |
| echo "::notice::no migration files matched, so restore falls back to the SHA schema key" | |
| else | |
| MIG_HASH=$(printf '%s\n%s\n%s' "$MIG_FILES" "$PG_IMAGE" "$DB_ROUTING" | sha256sum | cut -c1-40) | |
| echo "migrations_key=posthog-schema-mig-${SCHEMA_CACHE_EPOCH}-${MIG_HASH}" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Log in to Docker Hub | |
| continue-on-error: true | |
| if: ${{ env.DOCKERHUB_USERNAME != '' && env.DOCKERHUB_TOKEN != '' }} | |
| uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 | |
| with: | |
| username: ${{ vars.DOCKERHUB_USER }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - name: Start Docker services | |
| env: | |
| COMPOSE_FILE: docker-compose.dev.yml:docker-compose.profiles.yml | |
| run: | | |
| bin/ci-wait-for-docker launch --down | |
| - name: Mint setup-action GitHub token | |
| id: setup-gh-token | |
| if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository | |
| continue-on-error: true | |
| uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 | |
| with: | |
| client-id: ${{ vars.GH_APP_POSTHOG_SETUP_ACTIONS_APP_ID }} | |
| private-key: ${{ secrets.GH_APP_POSTHOG_SETUP_ACTIONS_PRIVATE_KEY }} | |
| skip-token-revoke: true | |
| - name: Set up Python | |
| uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 | |
| with: | |
| python-version-file: 'pyproject.toml' | |
| token: ${{ steps.setup-gh-token.outputs.token || github.token }} | |
| - name: Install uv | |
| uses: ./.github/actions/setup-uv | |
| - name: Install python dependencies | |
| run: UV_PROJECT_ENVIRONMENT=$pythonLocation uv sync --frozen --dev | |
| - name: Install Rust | |
| uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 | |
| with: | |
| toolchain: stable | |
| components: cargo | |
| - name: Install sqlx-cli | |
| uses: ./.github/actions/setup-sqlx-cli | |
| - name: Lint skills | |
| run: ./bin/hogli lint:skills | |
| - name: Wait for Docker services | |
| env: | |
| COMPOSE_FILE: docker-compose.dev.yml:docker-compose.profiles.yml | |
| run: bin/ci-wait-for-docker wait | |
| - name: Restore schema cache from master | |
| if: steps.schema-key.outputs.migrations_key != '' || steps.schema-key.outputs.key != '' | |
| uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 | |
| with: | |
| path: schema.sql.gz | |
| # Prefer the shared content key; fall back to the exact merge-base SHA | |
| # key, then to the newest dump of any migration set. | |
| key: ${{ steps.schema-key.outputs.migrations_key || steps.schema-key.outputs.key }} | |
| restore-keys: | | |
| ${{ steps.schema-key.outputs.key }} | |
| ${{ steps.schema-key.outputs.prefix_key }} | |
| - name: Prime posthog from cached schema | |
| # db:restore-schema-fresh restores the dump and seeds the RunPython data a | |
| # schema-only dump lacks; the migrate step below layers anything newer on | |
| # top. On failure it leaves a fresh empty db, so migrate runs the full history. | |
| env: | |
| SECRET_KEY: '6b01eee4f945ca25045b5aab440b953461faf08693a9abbf1166dc7c6b9772da' | |
| DATABASE_URL: 'postgres://posthog:posthog@localhost:5432/posthog' | |
| REDIS_URL: 'redis://localhost' | |
| OBJECT_STORAGE_ENABLED: 'False' | |
| TEST: 1 | |
| DEBUG: 1 | |
| TARGET_DB: posthog | |
| run: | | |
| if [ ! -f schema.sql.gz ]; then | |
| echo "::notice::Schema cache miss, so the migrate step below runs the full history" | |
| exit 0 | |
| fi | |
| mkdir -p .postgres-backups | |
| mv schema.sql.gz .postgres-backups/schema-latest.sql.gz | |
| if ! ./bin/hogli db:restore-schema-fresh; then | |
| echo "::warning::Schema restore failed (stale or incompatible cached dump?), so the migrate step below runs the full history" | |
| fi | |
| - name: Run migrations and create test data | |
| env: | |
| SECRET_KEY: '6b01eee4f945ca25045b5aab440b953461faf08693a9abbf1166dc7c6b9772da' | |
| DATABASE_URL: 'postgres://posthog:posthog@localhost:5432/posthog' | |
| REDIS_URL: 'redis://localhost' | |
| OBJECT_STORAGE_ENABLED: 'False' | |
| TEST: 1 | |
| DEBUG: 1 | |
| run: | | |
| 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/ | |
| python manage.py migrate --noinput | |
| python manage.py setup_dev --no-data | |
| - name: Build skills | |
| env: | |
| SECRET_KEY: '6b01eee4f945ca25045b5aab440b953461faf08693a9abbf1166dc7c6b9772da' | |
| DATABASE_URL: 'postgres://posthog:posthog@localhost:5432/posthog' | |
| REDIS_URL: 'redis://localhost' | |
| OBJECT_STORAGE_ENABLED: 'False' | |
| TEST: 1 | |
| DEBUG: 1 | |
| run: ./bin/hogli build:skills | |
| - name: Upload skills ZIP for the release job | |
| # release-skills publishes this exact artifact, so it does not repeat the | |
| # Postgres, Rust, and sqlx setup that produces it. | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/master' | |
| uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 | |
| with: | |
| name: agent-skills-zip | |
| path: products/posthog_ai/dist/skills.zip | |
| if-no-files-found: error | |
| retention-days: 7 | |
| current-master: | |
| name: Check current master | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 5 | |
| needs: check-skills | |
| if: github.repository_owner == 'PostHog' && github.ref == 'refs/heads/master' | |
| outputs: | |
| is-current: ${{ steps.current-master.outputs.is_current }} | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Check current master | |
| id: current-master | |
| env: | |
| GIT_SHA: ${{ github.sha }} | |
| REPOSITORY: ${{ github.repository }} | |
| run: | | |
| REMOTE_MASTER=$(git ls-remote "https://github.com/${REPOSITORY}.git" refs/heads/master || true) | |
| CURRENT_MASTER=$(printf '%s\n' "$REMOTE_MASTER" | awk '{print $1}') | |
| if [ "$CURRENT_MASTER" = "$GIT_SHA" ]; then | |
| echo "is_current=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "is_current=false" >> "$GITHUB_OUTPUT" | |
| echo "Skipping agent skills release for stale master SHA $GIT_SHA" | |
| fi | |
| release-skills: | |
| name: Release agent skills | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 10 | |
| needs: [check-skills, current-master] | |
| if: github.repository_owner == 'PostHog' && github.ref == 'refs/heads/master' && needs.current-master.outputs.is-current == 'true' | |
| permissions: | |
| contents: write | |
| steps: | |
| # The version step below only needs the origin remote for `git ls-remote`, so | |
| # nothing of the tree has to be materialized. | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| sparse-checkout: .github/workflows/ci-agent-skills.yml | |
| sparse-checkout-cone-mode: false | |
| filter: blob:none | |
| - name: Download skills ZIP built by check-skills | |
| uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 | |
| with: | |
| name: agent-skills-zip | |
| path: products/posthog_ai/dist | |
| - name: Determine next version | |
| id: version | |
| run: | | |
| REMOTE_TAGS=$(git ls-remote --tags --refs origin 'refs/tags/agent-skills-v*') | |
| LATEST_TAG=$(printf '%s\n' "$REMOTE_TAGS" | awk '{print $2}' | sed 's#refs/tags/##' | grep -E '^agent-skills-v[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -n 1) | |
| if [ -z "$LATEST_TAG" ]; then LATEST_TAG="agent-skills-v0.0.0"; fi | |
| MINOR=$(echo "${LATEST_TAG#agent-skills-v}" | cut -d. -f2) | |
| echo "tag=agent-skills-v0.$((MINOR + 1)).0" >> "$GITHUB_OUTPUT" | |
| - name: Create versioned release | |
| # The job's own contents:write GITHUB_TOKEN, like build-phrocs.yml and | |
| # build-livestream-tui.yml. No workflow triggers off these releases, so the | |
| # app token bought nothing and only added a permission that can drift away. | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| RELEASE_TAG: ${{ steps.version.outputs.tag }} | |
| run: | | |
| gh release create "$RELEASE_TAG" \ | |
| products/posthog_ai/dist/skills.zip \ | |
| --title "Agent skills $RELEASE_TAG" \ | |
| --notes "Build from ${{ github.sha }}" \ | |
| --target "${{ github.sha }}" \ | |
| --latest=false | |
| - name: Update latest release | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| RELEASE_TAG: ${{ steps.version.outputs.tag }} | |
| run: | | |
| if gh release view agent-skills-latest &>/dev/null; then | |
| gh release upload agent-skills-latest \ | |
| products/posthog_ai/dist/skills.zip --clobber | |
| gh release edit agent-skills-latest \ | |
| --title "Agent skills (Latest)" \ | |
| --notes "Latest build — same as $RELEASE_TAG" | |
| else | |
| gh release create agent-skills-latest \ | |
| products/posthog_ai/dist/skills.zip \ | |
| --title "Agent skills (Latest)" \ | |
| --notes "Latest build — same as $RELEASE_TAG" \ | |
| --target "${{ github.sha }}" \ | |
| --latest=false | |
| fi |