Skip to content

test(c-messages): add aimock e2e (Task #4 pilot slice 1) #1200

test(c-messages): add aimock e2e (Task #4 pilot slice 1)

test(c-messages): add aimock e2e (Task #4 pilot slice 1) #1200

Workflow file for this run

name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
concurrency:
group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
permissions:
contents: read
env:
DO_NOT_TRACK: '1'
jobs:
ci-scope:
name: CI scope
runs-on: ubuntu-latest
outputs:
library: ${{ steps.scope.outputs.library }}
website: ${{ steps.scope.outputs.website }}
cockpit: ${{ steps.scope.outputs.cockpit }}
cockpit_examples: ${{ steps.scope.outputs.cockpit_examples }}
cockpit_smoke: ${{ steps.scope.outputs.cockpit_smoke }}
cockpit_secret: ${{ steps.scope.outputs.cockpit_secret }}
cockpit_deploy_smoke: ${{ steps.scope.outputs.cockpit_deploy_smoke }}
examples_chat: ${{ steps.scope.outputs.examples_chat }}
cockpit_e2e: ${{ steps.scope.outputs.cockpit_e2e }}
website_e2e: ${{ steps.scope.outputs.website_e2e }}
posthog: ${{ steps.scope.outputs.posthog }}
steps:
- uses: actions/checkout@v6.0.2
with:
fetch-depth: 0
- name: Test CI scope classifier
run: node --test scripts/ci-scope.spec.mjs
- name: Detect changed CI surfaces
id: scope
run: |
set -euo pipefail
if [ "${{ github.event_name }}" = "push" ]; then
node scripts/ci-scope.mjs \
--event push \
--output "$GITHUB_OUTPUT"
exit 0
fi
base_sha="${{ github.event.pull_request.base.sha }}"
head_sha="${{ github.event.pull_request.head.sha }}"
if ! git cat-file -e "$base_sha^{commit}" 2>/dev/null || ! git cat-file -e "$head_sha^{commit}" 2>/dev/null; then
base_sha="$(git rev-parse HEAD^1)"
head_sha="$(git rev-parse HEAD^2)"
fi
node scripts/ci-scope.mjs \
--event pull_request \
--base "$base_sha" \
--head "$head_sha" \
--output "$GITHUB_OUTPUT"
- name: Validate CI workflow guards
run: node --test scripts/ci-workflow.spec.mjs
library:
name: Library — lint / test / build
needs: ci-scope
if: github.event_name == 'push' || needs.ci-scope.outputs.library == 'true'
runs-on: ubuntu-latest
env:
LIBS: chat,langgraph,ag-ui,render,a2ui,licensing,telemetry
steps:
- uses: actions/checkout@v6.0.2
- uses: actions/setup-node@v6.3.0
with:
node-version: 22
cache: npm
- run: npm ci
- run: npx nx run-many -t lint --projects=$LIBS
- run: npx nx run-many -t test --projects=$LIBS --coverage
- run: npx nx run-many -t build --projects=$LIBS --configuration=production
- run: node scripts/verify-release-versions.mjs
website:
name: Website — lint / build
needs: ci-scope
if: github.event_name == 'push' || needs.ci-scope.outputs.website == 'true'
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v6.0.2
with:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && github.head_ref || github.sha }}
- uses: actions/setup-node@v6.3.0
with:
node-version: 22
cache: npm
- run: npm ci
- run: npx nx lint website
- run: npm run generate-api-docs
- name: Commit generated API docs to same-repo PR
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
run: |
if git diff --quiet -- apps/website/content/docs/*/api/api-docs.json; then
echo "Generated API docs are already committed."
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add apps/website/content/docs/*/api/api-docs.json
git commit -m "chore(docs): regenerate api docs"
git push origin "HEAD:${{ github.head_ref }}"
- name: Verify generated API docs are committed
run: git diff --exit-code -- apps/website/content/docs/*/api/api-docs.json
- run: npx nx build website
cockpit:
name: Cockpit — build / test
needs: ci-scope
if: github.event_name == 'push' || needs.ci-scope.outputs.cockpit == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6.0.2
- uses: actions/setup-node@v6.3.0
with:
node-version: 22
cache: npm
- run: npm ci
- run: npx nx build cockpit --skip-nx-cache
- run: npx nx test cockpit --skip-nx-cache
cockpit-examples-build:
name: Cockpit — build all examples
needs: ci-scope
if: github.event_name == 'push' || needs.ci-scope.outputs.cockpit_examples == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6.0.2
- uses: actions/setup-node@v6.3.0
with:
node-version: 22
cache: npm
- run: npm ci
- run: npx nx run-many -t build --projects='cockpit-*-angular' --skip-nx-cache
cockpit-smoke:
name: Cockpit — representative capability smoke
needs: ci-scope
if: github.event_name == 'push' || needs.ci-scope.outputs.cockpit_smoke == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6.0.2
- uses: actions/setup-node@v6.3.0
with:
node-version: 22
cache: npm
- run: npm ci
- run: npx nx run-many -t smoke --projects=cockpit-deep-agents-planning-python,cockpit-deep-agents-filesystem-python,cockpit-deep-agents-subagents-python,cockpit-deep-agents-memory-python,cockpit-deep-agents-skills-python,cockpit-deep-agents-sandboxes-python,cockpit-langgraph-persistence-python,cockpit-langgraph-durable-execution-python,cockpit-langgraph-streaming-python,cockpit-langgraph-interrupts-python,cockpit-langgraph-memory-python,cockpit-langgraph-subgraphs-python,cockpit-langgraph-time-travel-python,cockpit-langgraph-deployment-runtime-python --skip-nx-cache
cockpit-secret-integration:
name: Cockpit — secret-gated integration
needs: ci-scope
if: github.event_name == 'push' || needs.ci-scope.outputs.cockpit_secret == 'true'
runs-on: ubuntu-latest
steps:
- name: Check integration secret
id: integration_secret
run: |
if [ -z "${COCKPIT_SECRET_TOKEN}" ]; then
echo "enabled=false" >> "$GITHUB_OUTPUT"
echo "::notice::Skipping secret-gated integration: COCKPIT_SECRET_TOKEN is not configured"
else
echo "enabled=true" >> "$GITHUB_OUTPUT"
fi
env:
COCKPIT_SECRET_TOKEN: ${{ secrets.COCKPIT_SECRET_TOKEN }}
- uses: actions/checkout@v6.0.2
if: steps.integration_secret.outputs.enabled == 'true'
- uses: actions/setup-node@v6.3.0
if: steps.integration_secret.outputs.enabled == 'true'
with:
node-version: 22
cache: npm
- if: steps.integration_secret.outputs.enabled == 'true'
run: npm ci
- if: steps.integration_secret.outputs.enabled == 'true'
run: npx nx run cockpit-langgraph-deployment-runtime-python:integration --skip-nx-cache
env:
COCKPIT_SECRET_TOKEN: ${{ secrets.COCKPIT_SECRET_TOKEN }}
cockpit-deploy-smoke:
name: Cockpit — deploy smoke dry-run
needs: ci-scope
if: github.event_name == 'push' || needs.ci-scope.outputs.cockpit_deploy_smoke == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6.0.2
- uses: actions/setup-node@v6.3.0
with:
node-version: 22
cache: npm
- run: npm ci
- run: npx tsx apps/cockpit/scripts/deploy-smoke.ts --url https://cockpit.threadplane.ai --dry-run
examples-chat-smoke:
name: examples/chat — python smoke
needs: ci-scope
if: github.event_name == 'push' || needs.ci-scope.outputs.examples_chat == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6.0.2
- uses: actions/setup-node@v6.3.0
with:
node-version: 22
cache: npm
- name: Install uv
uses: astral-sh/setup-uv@v8.0.0
with:
python-version: '3.12'
- run: npm ci
- working-directory: examples/chat/python
run: uv sync
- run: npx nx run examples-chat-python:smoke --skip-nx-cache
examples-chat-e2e:
name: examples/chat — e2e
needs: ci-scope
if: github.event_name == 'push' || needs.ci-scope.outputs.examples_chat == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6.0.2
- uses: actions/setup-node@v6.3.0
with:
node-version: 22
cache: npm
- name: Install uv
uses: astral-sh/setup-uv@v8.0.0
with:
python-version: '3.12'
- run: npm ci
- working-directory: examples/chat/python
run: uv sync
- run: npx playwright install --with-deps chromium
- run: npx nx e2e examples-chat-angular --skip-nx-cache
- name: Upload Playwright trace on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: examples-chat-e2e-trace
path: examples/chat/angular/e2e/test-results/
retention-days: 7
cockpit-e2e:
name: "Cockpit — e2e (${{ matrix.cap.angular }})"
needs: ci-scope
if: github.event_name == 'push' || needs.ci-scope.outputs.cockpit_e2e == 'true'
runs-on: ubuntu-latest
strategy:
fail-fast: false
max-parallel: 5
matrix:
cap:
- { angular: cockpit-langgraph-streaming-angular, python: cockpit/langgraph/streaming/python }
- { angular: cockpit-chat-tool-calls-angular, python: cockpit/chat/tool-calls/python }
- { angular: cockpit-chat-subagents-angular, python: cockpit/chat/subagents/python }
- { angular: cockpit-chat-interrupts-angular, python: cockpit/chat/interrupts/python }
- { angular: cockpit-chat-messages-angular, python: cockpit/chat/messages/python }
steps:
- uses: actions/checkout@v6.0.2
- uses: actions/setup-node@v6.3.0
with:
node-version: 22
cache: npm
- name: Install uv
uses: astral-sh/setup-uv@v8.0.0
with:
python-version: '3.12'
- run: npm ci
- name: uv sync per-cap python
working-directory: ${{ matrix.cap.python }}
run: uv sync
- run: npx playwright install --with-deps chromium
- name: nx e2e ${{ matrix.cap.angular }}
run: npx nx e2e "${{ matrix.cap.angular }}" --skip-nx-cache
- name: Upload Playwright trace on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: cockpit-e2e-trace-${{ matrix.cap.angular }}
path: |
cockpit/**/angular/e2e/test-results/
retention-days: 7
cockpit-e2e-summary:
name: "Cockpit — e2e"
needs: cockpit-e2e
if: always() && (github.event_name == 'push' || needs.ci-scope.outputs.cockpit_e2e == 'true')
runs-on: ubuntu-latest
steps:
- name: Aggregate matrix outcome
run: |
if [[ "${{ needs.cockpit-e2e.result }}" != "success" ]]; then
echo "Matrix outcome: ${{ needs.cockpit-e2e.result }}"
exit 1
fi
echo "All cockpit-e2e matrix expansions passed."
website-e2e:
name: Website — e2e
needs: ci-scope
if: github.event_name == 'push' || needs.ci-scope.outputs.website_e2e == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6.0.2
- uses: actions/setup-node@v6.3.0
with:
node-version: 22
cache: npm
- run: npm ci
- run: npx playwright install --with-deps chromium
- run: npx nx e2e website --skip-nx-cache
deploy:
name: Deploy → Vercel
needs:
[
library,
website,
cockpit,
cockpit-examples-build,
cockpit-smoke,
cockpit-secret-integration,
cockpit-deploy-smoke,
examples-chat-smoke,
examples-chat-e2e,
cockpit-e2e,
website-e2e,
]
runs-on: ubuntu-latest
# Only deploy on pushes to main, not on pull requests
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
steps:
- uses: actions/checkout@v6.0.2
with:
fetch-depth: 0
- name: Detect deploy-relevant changes
id: deploy_preflight
run: |
base_sha="${{ github.event.before }}"
head_sha="${{ github.sha }}"
if [ -z "$base_sha" ] || [ "$base_sha" = "0000000000000000000000000000000000000000" ]; then
base_sha="$(git rev-parse "$head_sha^")"
fi
if ! git cat-file -e "$base_sha^{commit}" 2>/dev/null; then
git fetch --no-tags origin "$base_sha"
fi
changed_files="$(git diff --name-only "$base_sha" "$head_sha")"
deploy_relevant=false
if printf '%s\n' "$changed_files" | grep -E '^(\.github/workflows/ci\.yml|vercel\.(json|cockpit\.json|examples\.json)|apps/(website|cockpit)/.*|cockpit/.*|examples/chat/.*|libs/.*|scripts/(assemble-examples|deploy-smoke|demo-middleware|langgraph-proxy|rate-limit)\.ts|scripts/assemble-demo\.ts)$' >/dev/null; then
deploy_relevant=true
fi
echo "relevant=$deploy_relevant" >> "$GITHUB_OUTPUT"
if [ "$deploy_relevant" != "true" ]; then
echo "::notice::No deploy-relevant files changed; skipping Vercel dependency setup."
fi
# ── Angular examples deploy ──────────────────────────────────────────
- name: Check if examples changed
id: examples_changed
run: |
base_sha="${{ github.event.before }}"
head_sha="${{ github.sha }}"
if [ -z "$base_sha" ] || [ "$base_sha" = "0000000000000000000000000000000000000000" ]; then
base_sha="$(git rev-parse "$head_sha^")"
fi
changed_files="$(git diff --name-only "$base_sha" "$head_sha")"
examples_changed=false
if printf '%s\n' "$changed_files" | grep -E '^cockpit/.*/angular/' >/dev/null; then
examples_changed=true
fi
if printf '%s\n' "$changed_files" | grep -E '^(vercel\.examples\.json|scripts/assemble-examples\.ts)$' >/dev/null; then
examples_changed=true
fi
# Any libs/ change retriggers examples deploy. Previous hand-maintained
# allow-list silently broke whenever a new lib was added; cost of a
# spurious example rebuild is far cheaper than a missed deploy.
if printf '%s\n' "$changed_files" | grep -E '^libs/' >/dev/null; then
examples_changed=true
fi
echo "changed=$examples_changed" >> "$GITHUB_OUTPUT"
- uses: actions/setup-node@v6.3.0
if: steps.deploy_preflight.outputs.relevant == 'true' || steps.examples_changed.outputs.changed == 'true'
with:
node-version: 22
cache: npm
# Required GitHub secrets (Settings → Secrets and variables → Actions):
# VERCEL_TOKEN — vercel.com/account/tokens
# VERCEL_ORG_ID — Vercel team id
# VERCEL_WEBSITE_PROJECT_ID — website project id
# VERCEL_COCKPIT_PROJECT_ID — cockpit project id
# VERCEL_EXAMPLES_PROJECT_ID — examples project id
- if: steps.deploy_preflight.outputs.relevant == 'true' || steps.examples_changed.outputs.changed == 'true'
run: npm ci
- name: Resolve deploy targets
if: steps.deploy_preflight.outputs.relevant == 'true'
id: affected
run: |
base_sha="${{ github.event.before }}"
head_sha="${{ github.sha }}"
if [ -z "$base_sha" ] || [ "$base_sha" = "0000000000000000000000000000000000000000" ]; then
base_sha="$(git rev-parse "$head_sha^")"
fi
if ! git cat-file -e "$base_sha^{commit}" 2>/dev/null; then
git fetch --no-tags origin "$base_sha"
fi
affected_projects="$(npx nx show projects --affected --base="$base_sha" --head="$head_sha")"
changed_files="$(git diff --name-only "$base_sha" "$head_sha")"
website_changed=false
cockpit_changed=false
if printf '%s\n' "$affected_projects" | grep -Fx 'website' >/dev/null; then
website_changed=true
fi
if printf '%s\n' "$affected_projects" | grep -Fx 'cockpit' >/dev/null; then
cockpit_changed=true
fi
if printf '%s\n' "$changed_files" | grep -E '^(\.github/workflows/ci\.yml|vercel\.json)$' >/dev/null; then
website_changed=true
fi
if printf '%s\n' "$changed_files" | grep -E '^(\.github/workflows/ci\.yml|vercel\.cockpit\.json)$' >/dev/null; then
cockpit_changed=true
fi
echo "website=$website_changed" >> "$GITHUB_OUTPUT"
echo "cockpit=$cockpit_changed" >> "$GITHUB_OUTPUT"
- name: Install Playwright browsers
if: steps.affected.outputs.website == 'true'
run: npx playwright install --with-deps chromium
- name: Prepare website Vercel project
if: steps.affected.outputs.website == 'true'
run: |
mkdir -p .vercel
cat > .vercel/project.json <<EOF
{"projectId":"${{ secrets.VERCEL_WEBSITE_PROJECT_ID }}","orgId":"${{ secrets.VERCEL_ORG_ID }}","projectName":"threadplane"}
EOF
npx vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}
rm -rf .vercel/output
- name: Build and deploy website to Vercel (production)
if: steps.affected.outputs.website == 'true'
id: deploy_website
run: |
npx vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}
url=$(npx vercel deploy --prebuilt --archive=tgz --prod --yes --token=${{ secrets.VERCEL_TOKEN }} | tail -n 1)
echo "deployment_url=$url" >> "$GITHUB_OUTPUT"
- name: Verify deployed website
if: steps.affected.outputs.website == 'true'
run: npx nx e2e website --skip-nx-cache
env:
BASE_URL: https://threadplane.ai
- name: Prepare cockpit Vercel project
if: steps.affected.outputs.cockpit == 'true'
run: |
mkdir -p .vercel
cat > .vercel/project.json <<EOF
{"projectId":"${{ secrets.VERCEL_COCKPIT_PROJECT_ID }}","orgId":"${{ secrets.VERCEL_ORG_ID }}","projectName":"threadplane-cockpit"}
EOF
npx vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}
rm -rf .vercel/output
- name: Build and deploy cockpit to Vercel (production)
if: steps.affected.outputs.cockpit == 'true'
id: deploy_cockpit
run: |
npx vercel build --prod --local-config vercel.cockpit.json --token=${{ secrets.VERCEL_TOKEN }}
url=$(npx vercel deploy --prebuilt --archive=tgz --prod --yes --token=${{ secrets.VERCEL_TOKEN }} | tail -n 1)
echo "deployment_url=$url" >> "$GITHUB_OUTPUT"
- name: Verify deployed cockpit
if: steps.affected.outputs.cockpit == 'true'
run: |
npx tsx apps/cockpit/scripts/deploy-smoke.ts --url https://cockpit.threadplane.ai --retries 20 --retry-delay-ms 5000
- name: Build and assemble Angular examples
if: steps.examples_changed.outputs.changed == 'true'
run: npx tsx scripts/assemble-examples.ts
- name: Deploy Angular examples to Vercel (production)
if: steps.examples_changed.outputs.changed == 'true'
working-directory: deploy/examples
run: |
mkdir -p .vercel
cat > .vercel/project.json <<EOF
{"projectId":"${{ secrets.VERCEL_EXAMPLES_PROJECT_ID }}","orgId":"${{ secrets.VERCEL_ORG_ID }}","projectName":"threadplane-examples"}
EOF
npx vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}
npx vercel deploy --prebuilt --prod --yes --token=${{ secrets.VERCEL_TOKEN }}
demo-deploy:
name: Canonical demo → Vercel
needs: [examples-chat-smoke, examples-chat-e2e]
runs-on: ubuntu-latest
if: ${{ always() && !cancelled() && github.ref == 'refs/heads/main' && github.event_name == 'push' }}
steps:
- name: Require demo prerequisite jobs
run: |
if [ "${{ needs.examples-chat-smoke.result }}" != "success" ]; then
echo "::error::examples/chat — python smoke finished with ${{ needs.examples-chat-smoke.result }}; refusing to deploy the canonical demo."
exit 1
fi
if [ "${{ needs.examples-chat-e2e.result }}" != "success" ]; then
echo "::error::examples/chat — e2e finished with ${{ needs.examples-chat-e2e.result }}; refusing to deploy the canonical demo."
exit 1
fi
- uses: actions/checkout@v6.0.2
- uses: actions/setup-node@v6.3.0
with:
node-version: 22
cache: npm
- run: npm ci
- name: Build and assemble canonical demo
run: npx tsx scripts/assemble-demo.ts
- name: Deploy canonical demo to Vercel (production)
working-directory: deploy/demo
run: |
mkdir -p .vercel
cat > .vercel/project.json <<EOF
{"projectId":"${{ secrets.VERCEL_DEMO_PROJECT_ID }}","orgId":"${{ secrets.VERCEL_ORG_ID }}","projectName":"threadplane-demo"}
EOF
npx vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}
npx vercel deploy --prebuilt --prod --yes --token=${{ secrets.VERCEL_TOKEN }}
- name: Verify canonical demo build stamp
env:
DEMO_URL: https://demo.threadplane.ai
EXPECTED_SHA: ${{ github.sha }}
run: |
node <<'NODE'
const { setTimeout: sleep } = require('node:timers/promises');
async function main() {
const demoUrl = process.env.DEMO_URL;
const expectedSha = process.env.EXPECTED_SHA;
let last = 'no response yet';
for (let attempt = 1; attempt <= 20; attempt += 1) {
try {
const response = await fetch(`${demoUrl}/__build.json?t=${Date.now()}`);
last = `HTTP ${response.status}`;
if (response.ok) {
const metadata = await response.json();
last = JSON.stringify(metadata);
if (metadata.sha === expectedSha) {
console.log(`Canonical demo is serving ${expectedSha}.`);
return;
}
}
} catch (error) {
last = error instanceof Error ? error.message : String(error);
}
console.log(`Waiting for canonical demo stamp ${expectedSha}; attempt ${attempt}/20. Last: ${last}`);
await sleep(5000);
}
throw new Error(`Canonical demo did not serve build stamp ${expectedSha}. Last: ${last}`);
}
main().catch((error) => {
console.error(`::error::${error instanceof Error ? error.message : String(error)}`);
process.exit(1);
});
NODE
production-smoke:
name: Production smoke
needs: [deploy, demo-deploy]
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
steps:
- uses: actions/checkout@v6.0.2
- uses: actions/setup-node@v6.3.0
with:
node-version: 22
cache: npm
- run: npm ci
- name: Verify shared LangGraph backend
run: npx tsx scripts/verify-shared-deployment.ts
env:
LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }}
- run: npx playwright install --with-deps chromium
- name: Run production smoke tests
run: npx playwright test apps/cockpit/e2e/production-smoke.spec.ts --reporter=list
env:
BASE_URL: https://cockpit.threadplane.ai
EXAMPLES_URL: https://examples.threadplane.ai
DEMO_URL: https://demo.threadplane.ai
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
posthog-sync-plan:
name: PostHog — dashboards-as-code drift check
needs: ci-scope
if: github.event_name == 'push' || needs.ci-scope.outputs.posthog == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Detect PostHog-relevant changes
id: posthog_preflight
run: |
if [ "${{ github.event_name }}" = "push" ]; then
base_sha="${{ github.event.before }}"
head_sha="${{ github.sha }}"
if [ -z "$base_sha" ] || [ "$base_sha" = "0000000000000000000000000000000000000000" ]; then
base_sha="$(git rev-parse "$head_sha^")"
fi
else
base_sha=$(git merge-base origin/main HEAD)
head_sha=$(git rev-parse HEAD)
fi
changed_files="$(git diff --name-only "$base_sha" "$head_sha")"
posthog_relevant=false
if printf '%s\n' "$changed_files" | grep -E '^(tools/posthog/|package(-lock)?\.json|nx\.json|tsconfig\.base\.json|\.github/workflows/ci\.yml)$' >/dev/null; then
posthog_relevant=true
fi
echo "relevant=$posthog_relevant" >> "$GITHUB_OUTPUT"
if [ "$posthog_relevant" != "true" ]; then
echo "::notice::No PostHog tooling files changed — skipping dependency setup and drift check."
fi
- uses: actions/setup-node@v4
if: steps.posthog_preflight.outputs.relevant == 'true'
with:
node-version: '20'
cache: 'npm'
- if: steps.posthog_preflight.outputs.relevant == 'true'
run: npm ci
- name: Detect affected
if: steps.posthog_preflight.outputs.relevant == 'true'
id: affected
run: |
if [ "${{ github.event_name }}" = "push" ]; then
base_sha="${{ github.event.before }}"
head_sha="${{ github.sha }}"
if [ -z "$base_sha" ] || [ "$base_sha" = "0000000000000000000000000000000000000000" ]; then
base_sha="$(git rev-parse "$head_sha^")"
fi
else
base_sha=$(git merge-base origin/main HEAD)
head_sha=$(git rev-parse HEAD)
fi
affected="$(npx nx show projects --affected --base=$base_sha --head=$head_sha)"
if printf '%s\n' "$affected" | grep -Fx 'posthog-tools' >/dev/null; then
echo "is_affected=yes" >> "$GITHUB_OUTPUT"
else
echo "is_affected=no" >> "$GITHUB_OUTPUT"
echo "::notice::posthog-tools not in affected projects — skipping drift check."
fi
- name: posthog:sync --plan
if: steps.affected.outputs.is_affected == 'yes'
env:
POSTHOG_PERSONAL_API_KEY: ${{ secrets.POSTHOG_PERSONAL_API_KEY_READONLY }}
POSTHOG_HOST: https://us.i.posthog.com
POSTHOG_PROJECT_ID: ${{ secrets.POSTHOG_PROJECT_ID }}
run: |
if [ -z "$POSTHOG_PERSONAL_API_KEY" ]; then
echo "::notice::POSTHOG_PERSONAL_API_KEY_READONLY not set — soft skip for contributor PRs."
exit 0
fi
npx nx run posthog-tools:sync:plan