Remediate codebase health issues #584
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 - Continuous Integration | |
| on: | |
| push: | |
| branches: | |
| - main | |
| - develop | |
| paths-ignore: | |
| - 'docs/**' | |
| - 'mkdocs.yml' | |
| - '*.md' | |
| - 'plan/**' | |
| - 'LICENSE' | |
| - '.github/workflows/deploy-docs.yml' | |
| pull_request: | |
| branches: | |
| - main | |
| - develop | |
| paths-ignore: | |
| - 'docs/**' | |
| - 'mkdocs.yml' | |
| - '*.md' | |
| - 'plan/**' | |
| - 'LICENSE' | |
| - '.github/workflows/deploy-docs.yml' | |
| workflow_dispatch: | |
| # Cancel in-progress runs for the same workflow and ref | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| changes: | |
| name: Detect Changes | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| permissions: | |
| pull-requests: read | |
| outputs: | |
| python: ${{ steps.filter.outputs.python }} | |
| frontend: ${{ steps.filter.outputs.frontend }} | |
| docker: ${{ steps.filter.outputs.docker }} | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: dorny/paths-filter@v4 | |
| id: filter | |
| with: | |
| filters: | | |
| python: | |
| - 'phentrieve/**' | |
| - 'api/**' | |
| - 'tests/**' | |
| - 'pyproject.toml' | |
| - 'uv.lock' | |
| - '.github/workflows/ci.yml' | |
| frontend: | |
| - 'frontend/**' | |
| - '.github/workflows/ci.yml' | |
| docker: | |
| - '**/Dockerfile' | |
| - 'docker-compose*.yml' | |
| - '.github/workflows/ci.yml' | |
| python-ci: | |
| name: Python CI | |
| needs: changes | |
| if: needs.changes.outputs.python == 'true' | |
| runs-on: ubuntu-latest | |
| env: | |
| HF_HOME: ${{ github.workspace }}/.cache/huggingface | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| python-version: ['3.11', '3.12', '3.13'] | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@v7 | |
| with: | |
| version: "latest" | |
| - name: Set up Python ${{ matrix.python-version }} | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: ${{ matrix.python-version }} | |
| - name: Cache uv dependencies | |
| uses: actions/cache@v5 | |
| with: | |
| path: | | |
| ~/.cache/uv | |
| .venv | |
| key: ${{ runner.os }}-uv-${{ matrix.python-version }}-${{ hashFiles('uv.lock') }} | |
| restore-keys: | | |
| ${{ runner.os }}-uv-${{ matrix.python-version }}- | |
| ${{ runner.os }}-uv- | |
| - name: Install dependencies | |
| run: | | |
| uv sync --all-extras --dev | |
| - name: Run Ruff format check | |
| run: | | |
| uv run ruff format --check phentrieve/ api/ tests/ | |
| - name: Run Ruff linting | |
| run: | | |
| uv run ruff check phentrieve/ api/ tests/ | |
| - name: Run mypy type checking | |
| run: | | |
| uv run mypy phentrieve/ api/ | |
| - name: Validate pytest collection | |
| run: | | |
| uv run pytest -o addopts='' tests/ --collect-only -q --strict-markers -m "not slow and not e2e" | |
| - name: Run pytest | |
| env: | |
| COVERAGE_CORE: sysmon | |
| run: | | |
| uv run pytest tests/ -v -m "not e2e" --cov=phentrieve --cov=api --cov-report=xml --cov-report=term | |
| - name: Upload coverage to Codecov | |
| if: matrix.python-version == '3.11' | |
| uses: codecov/codecov-action@v6 | |
| with: | |
| files: ./coverage.xml | |
| flags: python | |
| name: python-${{ matrix.python-version }} | |
| continue-on-error: true | |
| frontend-ci: | |
| name: Frontend CI | |
| needs: changes | |
| if: needs.changes.outputs.frontend == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: '20' | |
| cache: 'npm' | |
| cache-dependency-path: frontend/package-lock.json | |
| - name: Install dependencies | |
| working-directory: frontend | |
| run: npm ci | |
| - name: Cache ESLint | |
| uses: actions/cache@v5 | |
| with: | |
| path: frontend/.eslintcache | |
| key: eslint-${{ hashFiles('frontend/package-lock.json', 'frontend/eslint.config.*') }} | |
| restore-keys: | | |
| eslint- | |
| - name: Run ESLint | |
| working-directory: frontend | |
| run: npm run lint | |
| - name: Run Prettier format check | |
| working-directory: frontend | |
| run: npm run format:check | |
| - name: Run Vitest tests (with coverage on main, no coverage on PRs) | |
| working-directory: frontend | |
| run: | | |
| if [ "${{ github.event_name }}" = "pull_request" ]; then | |
| npm run test:ci | |
| else | |
| npm run test:coverage | |
| fi | |
| - name: Upload coverage to Codecov | |
| if: github.event_name != 'pull_request' | |
| uses: codecov/codecov-action@v6 | |
| with: | |
| files: ./frontend/coverage/coverage-final.json | |
| flags: frontend | |
| name: frontend | |
| continue-on-error: true | |
| - name: Build frontend | |
| working-directory: frontend | |
| run: npm run build | |
| env: | |
| VITE_API_URL: /api/v1 | |
| docker-build-test: | |
| name: Docker Build Test | |
| needs: changes | |
| if: needs.changes.outputs.docker == 'true' | |
| runs-on: ubuntu-latest | |
| strategy: | |
| matrix: | |
| service: [api, frontend] | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| - name: Free disk space | |
| run: | | |
| sudo rm -rf /usr/share/dotnet | |
| sudo rm -rf "$AGENT_TOOLSDIRECTORY" | |
| docker system prune -af | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v4 | |
| - name: Build ${{ matrix.service }} image | |
| uses: docker/build-push-action@v7 | |
| with: | |
| context: ${{ matrix.service == 'api' && '.' || './frontend' }} | |
| file: ${{ matrix.service == 'api' && './api/Dockerfile' || './frontend/Dockerfile' }} | |
| push: false | |
| tags: ghcr.io/berntpopp/phentrieve/${{ matrix.service }}:test | |
| build-args: | | |
| ${{ matrix.service == 'frontend' && 'VITE_API_URL=/api/v1' || '' }} | |
| dependency-review: | |
| name: Dependency Review | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| if: github.event_name == 'pull_request' | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Review dependency changes via API | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| REPOSITORY: ${{ github.repository }} | |
| BASE_SHA: ${{ github.event.pull_request.base.sha }} | |
| HEAD_SHA: ${{ github.event.pull_request.head.sha }} | |
| run: | | |
| set -euo pipefail | |
| response_body="$(mktemp)" | |
| response_headers="$(mktemp)" | |
| findings_json="$(mktemp)" | |
| trap 'rm -f "$response_body" "$response_headers" "$findings_json"' EXIT | |
| attempt=1 | |
| max_attempts=3 | |
| while true; do | |
| curl -fsSL \ | |
| -D "$response_headers" \ | |
| -H "Accept: application/vnd.github+json" \ | |
| -H "Authorization: Bearer ${GH_TOKEN}" \ | |
| -H "X-GitHub-Api-Version: 2026-03-10" \ | |
| "https://api.github.com/repos/${REPOSITORY}/dependency-graph/compare/${BASE_SHA}...${HEAD_SHA}" \ | |
| -o "$response_body" | |
| snapshot_warning_header="$(awk 'BEGIN{IGNORECASE=1} /^x-github-dependency-graph-snapshot-warnings:/{sub(/^[^:]+:[[:space:]]*/, ""); print; exit}' "$response_headers")" | |
| snapshot_warning="$(printf '%s' "$snapshot_warning_header" | python -c 'import base64, sys; data = sys.stdin.read().strip(); print(base64.b64decode(data).decode("utf-8") if data else "", end="")')" | |
| if [[ -z "${snapshot_warning}" ]] || [[ "$attempt" -ge "$max_attempts" ]]; then | |
| break | |
| fi | |
| echo "Dependency graph snapshot warning detected; retrying in $((2 ** attempt))s..." >&2 | |
| sleep $((2 ** attempt)) | |
| attempt=$((attempt + 1)) | |
| done | |
| if [[ -n "${snapshot_warning:-}" ]]; then | |
| { | |
| echo "## Dependency review snapshot warning" | |
| echo | |
| echo '```' | |
| echo "${snapshot_warning}" | |
| echo '```' | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| jq ' | |
| [ | |
| .[] | |
| | select(.change_type != "removed") | |
| | select((.scope // "unknown") == "runtime") | |
| | . as $dependency | |
| | ($dependency.vulnerabilities // [])[]? | |
| | select((.severity // "" | ascii_downcase) == "high" or (.severity // "" | ascii_downcase) == "critical") | |
| | { | |
| manifest: $dependency.manifest, | |
| ecosystem: $dependency.ecosystem, | |
| name: $dependency.name, | |
| version: $dependency.version, | |
| scope: ($dependency.scope // "unknown"), | |
| severity: (.severity // "unknown"), | |
| advisory: (.advisory_ghsa_id // "unknown"), | |
| summary: (.advisory_summary // "No summary provided"), | |
| url: (.advisory_url // "") | |
| } | |
| ] | |
| ' "$response_body" > "$findings_json" | |
| if [[ "$(jq 'length' "$findings_json")" -eq 0 ]]; then | |
| { | |
| echo "## Dependency review" | |
| echo | |
| echo "No new runtime dependencies with high or critical vulnerabilities were detected." | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| exit 0 | |
| fi | |
| { | |
| echo "## Dependency review failures" | |
| echo | |
| echo "| Dependency | Version | Severity | Advisory | Manifest |" | |
| echo "|---|---|---|---|---|" | |
| jq -r '.[] | "| \(.name) | \(.version) | \(.severity) | \(.advisory) | \(.manifest) |"' "$findings_json" | |
| echo | |
| echo "High or critical vulnerabilities were introduced in runtime dependency changes." | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| jq -r '.[] | "\(.severity): \(.name)@\(.version) (\(.advisory)) - \(.summary)\n\(.url)\n"' "$findings_json" >&2 | |
| exit 1 | |
| ci-summary: | |
| name: CI Summary | |
| needs: [changes, python-ci, frontend-ci, docker-build-test, dependency-review] | |
| if: always() | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| steps: | |
| - name: Check CI results | |
| run: | | |
| echo "## CI Results Summary" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "| Job | Result |" >> $GITHUB_STEP_SUMMARY | |
| echo "|-----|--------|" >> $GITHUB_STEP_SUMMARY | |
| echo "| Python CI | ${{ needs.python-ci.result }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Frontend CI | ${{ needs.frontend-ci.result }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Docker Build | ${{ needs.docker-build-test.result }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Dependency Review | ${{ needs.dependency-review.result }} |" >> $GITHUB_STEP_SUMMARY | |
| if [[ "${{ needs.python-ci.result }}" == "failure" ]] || \ | |
| [[ "${{ needs.frontend-ci.result }}" == "failure" ]] || \ | |
| [[ "${{ needs.docker-build-test.result }}" == "failure" ]]; then | |
| echo "One or more CI jobs failed" | |
| exit 1 | |
| fi | |
| if [[ "${{ needs.dependency-review.result }}" == "failure" ]]; then | |
| echo "Dependency review found vulnerabilities" | |
| exit 1 | |
| fi |