feat(studies): real executor — Analysis M comorbidity matrix from CDM #2064
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, 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 | |
| # 45 (was 35): pcov coverage instrumentation adds overhead to the Pest run. | |
| timeout-minutes: 45 | |
| 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: pcov | |
| - 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 \ | |
| --coverage-clover=coverage.xml \ | |
| --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 | |
| - name: Enforce coverage floor | |
| working-directory: backend | |
| run: | | |
| # Robust, explicit floor: parse the clover report's project-level | |
| # aggregate metrics rather than relying on Pest's exit code / log text. | |
| # Floor ratchets up toward 80% as coverage grows; measured 33.59% on | |
| # 2026-06-20 (25693/76492 statements). NEVER lower this value. | |
| php -r ' | |
| $floor = 32.0; | |
| $f = "coverage.xml"; | |
| if (!is_file($f)) { fwrite(STDERR, "coverage.xml not found — coverage was not collected\n"); exit(1); } | |
| $xml = simplexml_load_file($f); | |
| $nodes = $xml->xpath("/coverage/project/metrics"); | |
| if (!$nodes) { fwrite(STDERR, "no project metrics in clover report\n"); exit(1); } | |
| $m = end($nodes); | |
| $stmts = (int) $m["statements"]; | |
| $covered = (int) $m["coveredstatements"]; | |
| $pct = $stmts > 0 ? ($covered / $stmts) * 100.0 : 0.0; | |
| printf("Line coverage: %.2f%% (%d/%d statements); floor %.1f%%\n", $pct, $covered, $stmts, $floor); | |
| if ($pct + 1e-9 < $floor) { | |
| fwrite(STDERR, sprintf("::error::Coverage %.2f%% is below the %.1f%% floor\n", $pct, $floor)); | |
| exit(1); | |
| } | |
| ' | |
| 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 |