Skip to content

feat(studies): real executor — Analysis M comorbidity matrix from CDM #2064

feat(studies): real executor — Analysis M comorbidity matrix from CDM

feat(studies): real executor — Analysis M comorbidity matrix from CDM #2064

Workflow file for this run

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