Skip to content

feat(cohorts): add used-in references endpoint and UI #84772

feat(cohorts): add used-in references endpoint and UI

feat(cohorts): add used-in references endpoint and UI #84772

name: Migration & Service Separation Check
on:
pull_request:
merge_group:
permissions:
contents: read
pull-requests: read
jobs:
check-migration-service-separation:
name: Check migrations and service changes are not in same PR
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
# Author-attested escape hatch for DB-noop migrations (e.g. SeparateDatabaseAndState
# state-only renames) that ship safely alongside service code. The check is path-only
# and can't tell a CREATE TABLE from an app-label rename, so the label opt-in matches
# the precedent set by skip-inkeep-docs.
- name: Check for skip label
id: skip-label
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
EVENT_NAME: ${{ github.event_name }}
PR_LABELS_JSON: ${{ toJson(github.event.pull_request.labels.*.name) }}
GITHUB_REF: ${{ github.ref }}
GITHUB_REPOSITORY: ${{ github.repository }}
run: |
if [ "$EVENT_NAME" = "pull_request" ]; then
labels="$PR_LABELS_JSON"
elif [ "$EVENT_NAME" = "merge_group" ]; then
# GITHUB_REF looks like refs/heads/gh-readonly-queue/<base>/pr-<num>-<sha>
pr_num=$(echo "$GITHUB_REF" | grep -oE 'pr-[0-9]+' | head -1 | sed 's/pr-//')
if [ -n "$pr_num" ]; then
labels=$(gh api "repos/$GITHUB_REPOSITORY/issues/$pr_num/labels" --jq '[.[].name]')
else
labels='[]'
fi
else
labels='[]'
fi
echo "Labels: $labels"
if echo "$labels" | grep -q '"skip-migration-service-check"'; then
echo "skip=true" >> "$GITHUB_OUTPUT"
echo "::notice::Skipping check — 'skip-migration-service-check' label is set."
else
echo "skip=false" >> "$GITHUB_OUTPUT"
fi
- uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
id: app-token
if: steps.skip-label.outputs.skip != 'true' && 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: steps.skip-label.outputs.skip != 'true'
with:
token: ${{ steps.app-token.outputs.token || github.token }}
filters: |
migrations:
- 'posthog/migrations/*.py'
- 'posthog/clickhouse/migrations/*.py'
- 'products/*/backend/migrations/*.py'
- 'ee/migrations/*.py'
sqlx_migrations:
- 'rust/persons_migrations/*.sql'
- 'rust/behavioral_cohorts_migrations/*.sql'
- 'rust/cyclotron-core/migrations/*.sql'
- 'rust/cyclotron-node-migrations/*.sql'
nodejs:
- 'nodejs/**'
# Separate step with predicate-quantifier: 'every' so that the
# negative patterns actually exclude migration directories.
# The default quantifier is 'some' (OR logic), which means a file
# matching ANY pattern (including the positive rust/**) passes the
# filter — the ! patterns are silently ignored.
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
id: rust-filter
if: steps.skip-label.outputs.skip != 'true'
with:
token: ${{ steps.app-token.outputs.token || github.token }}
predicate-quantifier: 'every'
filters: |
rust_services:
- 'rust/**'
- '!rust/persons_migrations/**'
- '!rust/behavioral_cohorts_migrations/**'
- '!rust/cyclotron-core/migrations/**'
- '!rust/cyclotron-node-migrations/**'
- name: Check for conflicting changes
id: check
if: steps.skip-label.outputs.skip != 'true'
env:
MIGRATIONS_CHANGED: ${{ steps.filter.outputs.migrations }}
NODEJS_CHANGED: ${{ steps.filter.outputs.nodejs }}
SQLX_MIGRATIONS_CHANGED: ${{ steps.filter.outputs.sqlx_migrations }}
RUST_SERVICES_CHANGED: ${{ steps.rust-filter.outputs.rust_services }}
run: |
has_error=false
error_messages=""
# Check nodejs + Django migrations
if [ "$MIGRATIONS_CHANGED" == "true" ] && [ "$NODEJS_CHANGED" == "true" ]; then
error_messages="${error_messages}❌ Found both Django migration and nodejs changes\n"
has_error=true
fi
# Check nodejs + sqlx migrations
if [ "$SQLX_MIGRATIONS_CHANGED" == "true" ] && [ "$NODEJS_CHANGED" == "true" ]; then
error_messages="${error_messages}❌ Found both sqlx migration and nodejs changes\n"
has_error=true
fi
# Check rust services + sqlx migrations
if [ "$SQLX_MIGRATIONS_CHANGED" == "true" ] && [ "$RUST_SERVICES_CHANGED" == "true" ]; then
error_messages="${error_messages}❌ Found both sqlx migration and rust service changes\n"
has_error=true
fi
if [ "$has_error" == "true" ]; then
echo "error=true" >> $GITHUB_OUTPUT
echo -e "$error_messages"
else
echo "error=false" >> $GITHUB_OUTPUT
echo "✅ No conflicting changes detected"
fi
- name: Fail if conflicting changes detected
if: steps.check.outputs.error == 'true'
run: |
echo "::error::This PR contains both migration files and service changes. These must be separated into different PRs."
echo ""
echo "Why? Migration jobs are not orchestrated with service deployments. If you merge"
echo "service code that depends on new database schema alongside the migration, your"
echo "service may deploy before the migration runs, causing failures."
echo ""
echo "Solution: Split into two PRs:"
echo " 1. First PR: Migration changes only (merge and wait for it to deploy)"
echo " 2. Second PR: Service code changes (merge after migration is deployed)"
echo ""
echo "If every migration in this PR is DB-noop (e.g. SeparateDatabaseAndState"
echo "with empty database_operations, or RunPython data-only steps), apply the"
echo "'skip-migration-service-check' label and the check will skip."
exit 1