test(c-messages): add aimock e2e (Task #4 pilot slice 1) #1200
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: 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 |