feat(studies): omnipresent action-taking Abby copilot (Claude Agent SDK) #2011
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, master, "release/**"] | |
| tags: | |
| - "v*" | |
| pull_request: | |
| branches: [main, master] | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: ci-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| backend: | |
| name: Backend (Laravel) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 35 | |
| services: | |
| postgres: | |
| image: pgvector/pgvector:pg16 | |
| env: | |
| POSTGRES_DB: parthenon_test | |
| POSTGRES_USER: parthenon | |
| POSTGRES_PASSWORD: secret | |
| ports: | |
| - 5432:5432 | |
| options: >- | |
| --health-cmd="pg_isready -U parthenon" | |
| --health-interval=10s | |
| --health-timeout=5s | |
| --health-retries=5 | |
| redis: | |
| image: redis:7-alpine | |
| ports: | |
| - 6379:6379 | |
| options: >- | |
| --health-cmd="redis-cli ping" | |
| --health-interval=10s | |
| --health-timeout=5s | |
| --health-retries=5 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Install PostGIS into postgres service container | |
| run: | | |
| # Phase 19 (D-04): the gis schema migration MUST fail loudly when | |
| # PostGIS is missing, so CI installs postgresql-16-postgis-3 on top | |
| # of pgvector/pgvector:pg16 (mirrors docker/postgres/Dockerfile). | |
| PG_CID=$(docker ps --filter "ancestor=pgvector/pgvector:pg16" --format '{{.ID}}' | head -n1) | |
| if [ -z "$PG_CID" ]; then | |
| echo "Could not find pgvector postgres service container" >&2 | |
| docker ps | |
| exit 1 | |
| fi | |
| docker exec "$PG_CID" bash -c "apt-get update -qq && apt-get install -y --no-install-recommends postgresql-16-postgis-3" | |
| - name: Prepare Postgres schemas and extensions | |
| env: | |
| PGPASSWORD: secret | |
| PGDATABASE: parthenon_test | |
| PGUSER: parthenon | |
| POSTGRES_CREATE_TEST_DATABASE: "1" | |
| POSTGRES_ENABLE_POSTGIS: "1" | |
| run: bash scripts/ci/prepare-postgres.sh | |
| - name: Setup PHP | |
| uses: shivammathur/setup-php@v2 | |
| with: | |
| php-version: "8.4" | |
| extensions: pdo_pgsql, pgsql, redis, bcmath, intl, mbstring, pcntl, zip | |
| coverage: none | |
| - name: Install Composer dependencies | |
| working-directory: backend | |
| run: composer install --prefer-dist --no-interaction | |
| - name: Copy env | |
| working-directory: backend | |
| run: cp .env.example .env && php artisan key:generate | |
| - name: Run Pint (code style) | |
| working-directory: backend | |
| run: ./vendor/bin/pint --test | |
| - name: Run PHPStan (static analysis) | |
| working-directory: backend | |
| run: ./vendor/bin/phpstan analyse | |
| - name: Run Pest (tests) | |
| working-directory: backend | |
| env: | |
| DB_CONNECTION: pgsql | |
| DB_HOST: 127.0.0.1 | |
| DB_PORT: 5432 | |
| DB_DATABASE: parthenon_test | |
| DB_USERNAME: parthenon | |
| DB_PASSWORD: secret | |
| # phpunit.xml forces DB_CONNECTION=pgsql_testing, which reads DB_TEST_* | |
| # with fallback to DB_*. Its DB_TEST_PORT default is 5480 (host PG17 | |
| # dev convention), so without these explicit overrides the test | |
| # connection ignores the CI postgres service and tries port 5480. | |
| DB_TEST_HOST: 127.0.0.1 | |
| DB_TEST_PORT: 5432 | |
| DB_TEST_DATABASE: parthenon_testing | |
| DB_TEST_USERNAME: parthenon | |
| DB_TEST_PASSWORD: secret | |
| REDIS_HOST: 127.0.0.1 | |
| CACHE_STORE: array | |
| SESSION_DRIVER: array | |
| QUEUE_CONNECTION: sync | |
| run: | | |
| php artisan migrate --force | |
| set +e | |
| ./vendor/bin/pest \ | |
| --do-not-fail-on-skipped \ | |
| --do-not-fail-on-warning \ | |
| --do-not-fail-on-phpunit-warning \ | |
| --do-not-fail-on-deprecation \ | |
| --do-not-fail-on-phpunit-deprecation \ | |
| --do-not-fail-on-notice \ | |
| --do-not-fail-on-incomplete 2>&1 | tee pest.log | |
| pest_status=${PIPESTATUS[0]} | |
| set -e | |
| if [ "$pest_status" -ne 0 ]; then | |
| summary=$(grep -E 'Tests:[[:space:]]' pest.log | tail -1 || true) | |
| if echo "$summary" | grep -Eq '[1-9][0-9]* (failed|errored|risky)'; then | |
| exit "$pest_status" | |
| fi | |
| if grep -Eq 'Code coverage below expected' pest.log; then | |
| exit "$pest_status" | |
| fi | |
| if echo "$summary" | grep -q 'passed'; then | |
| echo "Pest exited $pest_status with non-failure issue statuses only; continuing." | |
| exit 0 | |
| fi | |
| exit "$pest_status" | |
| fi | |
| frontend: | |
| name: Frontend (React) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 25 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Setup Node | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: 22 | |
| cache: npm | |
| cache-dependency-path: frontend/package-lock.json | |
| - name: Install dependencies | |
| working-directory: frontend | |
| run: npm ci --legacy-peer-deps | |
| - name: Type check | |
| working-directory: frontend | |
| run: npx tsc --noEmit | |
| - name: Lint | |
| working-directory: frontend | |
| run: npm run lint | |
| - name: Test | |
| working-directory: frontend | |
| run: npx vitest run | |
| - name: Build | |
| working-directory: frontend | |
| run: npm run build | |
| ai: | |
| name: AI Service (Python) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 25 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Setup Python | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: "3.12" | |
| cache: pip | |
| cache-dependency-path: ai/requirements*.txt | |
| - name: Install dependencies | |
| working-directory: ai | |
| run: | | |
| pip install torch --index-url https://download.pytorch.org/whl/cpu | |
| pip install -r requirements.txt | |
| pip install -r requirements-dev.txt | |
| - name: Type check (mypy) | |
| working-directory: ai | |
| run: mypy app/ | |
| - name: Test (pytest) | |
| working-directory: ai | |
| run: pytest | |
| docs-build: | |
| name: Documentation (Docusaurus) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Setup Node | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: "22" | |
| cache: npm | |
| cache-dependency-path: docs/site/package-lock.json | |
| - name: Install dependencies | |
| working-directory: docs/site | |
| run: npm ci --legacy-peer-deps | |
| - name: Build | |
| working-directory: docs/site | |
| run: npm run build | |
| docs-pdf: | |
| name: Documentation PDF Export | |
| # Only runs on release branches (release/*, v*.*.*) | |
| if: startsWith(github.ref, 'refs/heads/release/') || startsWith(github.ref, 'refs/tags/v') | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 25 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Setup Node | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: "22" | |
| cache: npm | |
| cache-dependency-path: docs/site/package-lock.json | |
| - name: Install dependencies | |
| working-directory: docs/site | |
| run: npm ci --legacy-peer-deps | |
| - name: Build documentation | |
| working-directory: docs/site | |
| run: npm run build | |
| - name: Install Chromium | |
| run: npx puppeteer browsers install chrome | |
| - name: Generate PDF from built docs | |
| working-directory: docs/site | |
| run: | | |
| node -e " | |
| const puppeteer = require('puppeteer'); | |
| const path = require('path'); | |
| const fs = require('fs'); | |
| const { execSync } = require('child_process'); | |
| // Serve the built site locally | |
| const { createServer } = require('http'); | |
| const handler = require('serve-handler'); | |
| const server = createServer((req, res) => handler(req, res, { public: 'build' })); | |
| server.listen(3001, async () => { | |
| const browser = await puppeteer.launch({ args: ['--no-sandbox'] }); | |
| const page = await browser.newPage(); | |
| await page.goto('http://localhost:3001/', { waitUntil: 'networkidle0' }); | |
| await page.pdf({ path: 'parthenon-user-manual.pdf', format: 'A4', printBackground: true }); | |
| await browser.close(); | |
| server.close(); | |
| console.log('PDF generated: parthenon-user-manual.pdf'); | |
| }); | |
| " | |
| - name: Upload PDF artifact | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: parthenon-user-manual-pdf | |
| path: docs/site/parthenon-user-manual.pdf | |
| retention-days: 90 | |
| # docs-screenshots: | |
| # name: Documentation Screenshots (Playwright) | |
| # # Manual trigger or release branches only — requires a running Parthenon instance | |
| # # seeded with Eunomia synthetic data. | |
| # # | |
| # # To run locally: | |
| # # cd docs/screenshots && npm ci && npx playwright test | |
| # # | |
| # # Environment variables required: | |
| # # PARTHENON_URL — base URL of a running Parthenon instance | |
| # # PARTHENON_EMAIL / PARTHENON_PASSWORD — credentials for screenshot account | |
| # if: false # Enable when screenshot infrastructure is ready | |
| # runs-on: ubuntu-latest | |
| # steps: | |
| # - uses: actions/checkout@v6 | |
| # - uses: actions/setup-node@v6 | |
| # with: { node-version: "22" } | |
| # - run: npm ci | |
| # working-directory: docs/screenshots | |
| # - run: npx playwright install --with-deps chromium | |
| # working-directory: docs/screenshots | |
| # - run: npx playwright test | |
| # working-directory: docs/screenshots | |
| # env: | |
| # PARTHENON_URL: ${{ secrets.PARTHENON_URL }} | |
| # PARTHENON_EMAIL: ${{ secrets.PARTHENON_EMAIL }} | |
| # PARTHENON_PASSWORD: ${{ secrets.PARTHENON_PASSWORD }} | |
| # - uses: actions/upload-artifact@v7 | |
| # with: | |
| # name: docs-screenshots | |
| # path: docs/screenshots/screenshots/ | |
| openapi-export: | |
| name: OpenAPI Spec (Scribe) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| services: | |
| postgres: | |
| image: pgvector/pgvector:pg16 | |
| env: | |
| POSTGRES_DB: parthenon_test | |
| POSTGRES_USER: parthenon | |
| POSTGRES_PASSWORD: secret | |
| ports: | |
| - 5432:5432 | |
| options: >- | |
| --health-cmd="pg_isready -U parthenon" | |
| --health-interval=10s | |
| --health-timeout=5s | |
| --health-retries=5 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Prepare Postgres schemas | |
| env: | |
| PGPASSWORD: secret | |
| PGDATABASE: parthenon_test | |
| PGUSER: parthenon | |
| run: bash scripts/ci/prepare-postgres.sh | |
| - name: Setup PHP | |
| uses: shivammathur/setup-php@v2 | |
| with: | |
| php-version: "8.4" | |
| extensions: pdo_pgsql, pgsql, redis, bcmath, intl, mbstring, pcntl, zip | |
| coverage: none | |
| - name: Install Composer dependencies | |
| working-directory: backend | |
| run: composer install --prefer-dist --no-interaction | |
| - name: Copy env | |
| working-directory: backend | |
| run: cp .env.example .env && php artisan key:generate | |
| - name: Export OpenAPI spec with Scribe | |
| working-directory: backend | |
| env: | |
| DB_CONNECTION: pgsql | |
| DB_HOST: 127.0.0.1 | |
| DB_PORT: 5432 | |
| DB_DATABASE: parthenon_test | |
| DB_USERNAME: parthenon | |
| DB_PASSWORD: secret | |
| DB_SEARCH_PATH: app,public | |
| run: | | |
| php artisan scribe:generate --no-interaction | |
| test -s public/docs/openapi.yaml | |
| php -r ' | |
| require "vendor/autoload.php"; | |
| $spec = Symfony\Component\Yaml\Yaml::parseFile("public/docs/openapi.yaml"); | |
| if (! is_array($spec)) { | |
| fwrite(STDERR, "OpenAPI spec is not a map.\n"); | |
| exit(1); | |
| } | |
| $paths = $spec["paths"] ?? []; | |
| if (! is_array($paths) || count($paths) === 0) { | |
| fwrite(STDERR, "OpenAPI spec has no paths.\n"); | |
| exit(1); | |
| } | |
| file_put_contents("api.json", json_encode($spec, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . PHP_EOL); | |
| fwrite(STDOUT, "OpenAPI spec exported with " . count($paths) . " paths.\n"); | |
| ' | |
| - name: Validate OpenAPI spec | |
| working-directory: backend | |
| run: | | |
| python3 -m json.tool api.json >/dev/null | |
| - name: Setup Node | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: "22" | |
| cache: npm | |
| cache-dependency-path: frontend/package-lock.json | |
| - name: Install frontend dependencies | |
| working-directory: frontend | |
| run: npm ci --legacy-peer-deps | |
| - name: Generate TypeScript types | |
| working-directory: frontend | |
| run: npm run generate:api-types | |
| - name: Detect OpenAPI artifact drift | |
| run: | | |
| # Fail if regenerated OpenAPI artifacts differ from what's committed. | |
| # Contributors must run ./deploy.sh --openapi whenever backend | |
| # routes, requests, or responses change. | |
| if ! git diff --exit-code -- backend/api.json frontend/src/types/api.generated.ts; then | |
| echo "" | |
| echo "::error::OpenAPI artifact drift detected." | |
| echo "" | |
| echo "The committed OpenAPI spec or TypeScript types do not match what would be generated from the current backend routes." | |
| echo "" | |
| echo "To fix locally:" | |
| echo " ./deploy.sh --openapi" | |
| echo " git add frontend/src/types/api.generated.ts backend/api.json" | |
| echo " git commit -m 'chore: regenerate OpenAPI types'" | |
| echo "" | |
| echo "Unified diff of the drift is shown above." | |
| exit 1 | |
| fi | |
| echo "OpenAPI types are in sync with the backend spec." | |
| - name: Verify TypeScript types compile | |
| working-directory: frontend | |
| run: npx tsc --noEmit |