Regenerate COUNTS.md after adding infrastructure/rendering/chrome.py #359
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] | |
| schedule: | |
| # Weekly Sunday midnight UTC — catches newly-published CVEs without a push | |
| - cron: "0 0 * * 0" | |
| workflow_dispatch: | |
| # Global minimum permissions; individual jobs override where needed | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: ci-${{ github.ref }}-${{ github.workflow }} | |
| cancel-in-progress: true | |
| env: | |
| UV_FROZEN: "true" | |
| MPLBACKEND: Agg | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| # Job 1 — Lint & Type Check | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| jobs: | |
| # Presence-detection for OPTIONAL/rotating projects. `hashFiles()` is NOT | |
| # available in a job-level `if:` (only in step contexts) — using it there is | |
| # a workflow-validation error that makes GitHub reject the ENTIRE workflow | |
| # at startup (zero jobs run, surfaced as "workflow file issue"). This tiny | |
| # job computes the existence flags in a step (valid) and exposes them as | |
| # outputs that the optional jobs gate on via `needs.detect.outputs.*`. | |
| detect: | |
| name: Detect optional projects | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| permissions: | |
| contents: read | |
| outputs: | |
| setup_hook: ${{ steps.d.outputs.setup_hook }} | |
| fep_lean: ${{ steps.d.outputs.fep_lean }} | |
| steps: | |
| - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| - id: d | |
| shell: bash | |
| run: | | |
| shopt -s globstar nullglob | |
| hooks=(projects/**/scripts/setup_hook.py) | |
| if [ ${#hooks[@]} -gt 0 ]; then | |
| echo "setup_hook=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "setup_hook=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| if [ -f projects/fep_lean/lean/lean-toolchain ]; then | |
| echo "fep_lean=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "fep_lean=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| # Derive the public-exemplar matrix for `test-project` from the canonical | |
| # roster (`infrastructure.project.public_scope`) instead of a hard-coded list, | |
| # so adding/retiring an exemplar under projects/templates/ extends or trims the | |
| # matrix automatically (CI-MATRIX-DYNAMIC-1). The `project-names-json` command | |
| # emits a one-line JSON array which `test-project` consumes via | |
| # `fromJSON(needs.detect-projects.outputs.projects)`. No network calls — pure | |
| # local filesystem discovery. | |
| detect-projects: | |
| name: Detect public exemplars | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| permissions: | |
| contents: read | |
| outputs: | |
| projects: ${{ steps.matrix.outputs.projects }} | |
| steps: | |
| - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| - uses: ./.github/actions/setup-python-env | |
| - name: Sync dependencies | |
| run: uv sync | |
| - name: Emit exemplar matrix | |
| id: matrix | |
| run: | | |
| set -euo pipefail | |
| projects="$(uv run python -m infrastructure.project.public_scope project-names-json)" | |
| echo "projects=${projects}" >> "$GITHUB_OUTPUT" | |
| # ─────────────────────────────────────────────────────────────────────────── | |
| # Job 0b — Actionlint (workflow syntax gate; independent of project setup) | |
| # | |
| # GH-ACTIONLINT-1: catch workflow-expression and `uses:` errors early, before | |
| # a small mistake silently disables CI coverage or fails every PR. Read-only, | |
| # no `uv sync`, no project dependencies, no `needs:` — it runs in parallel with | |
| # everything else. The actionlint binary is fetched with its official installer | |
| # script pinned to a release commit SHA (immutable); the runner's preinstalled | |
| # shellcheck is picked up automatically to also lint embedded `run:` scripts. | |
| # ─────────────────────────────────────────────────────────────────────────── | |
| actionlint: | |
| name: Actionlint | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| - name: Run actionlint | |
| env: | |
| # rhysd/actionlint @ v1.7.12 — bump REF and VERSION together on upgrade | |
| ACTIONLINT_REF: 914e7df21a07ef503a81201c76d2b11c789d3fca | |
| ACTIONLINT_VERSION: "1.7.12" | |
| run: | | |
| set -euo pipefail | |
| curl -fsSL \ | |
| "https://raw.githubusercontent.com/rhysd/actionlint/${ACTIONLINT_REF}/scripts/download-actionlint.bash" \ | |
| -o "${RUNNER_TEMP}/download-actionlint.bash" | |
| bash "${RUNNER_TEMP}/download-actionlint.bash" "${ACTIONLINT_VERSION}" "${RUNNER_TEMP}" | |
| "${RUNNER_TEMP}/actionlint" -color | |
| lint: | |
| name: Lint & Type Check | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| - uses: ./.github/actions/setup-python-env | |
| - name: Sync dependencies | |
| run: uv sync | |
| - name: Resolve public CI source paths | |
| id: public-scope | |
| run: echo "paths=$(uv run python -m infrastructure.project.public_scope source-paths)" >> "$GITHUB_OUTPUT" | |
| - name: Ruff lint | |
| run: uvx ruff check ${{ steps.public-scope.outputs.paths }} | |
| - name: Ruff format check | |
| run: uvx ruff format --check ${{ steps.public-scope.outputs.paths }} | |
| - name: Type checking | |
| run: uv run mypy ${{ steps.public-scope.outputs.paths }} | |
| # MED5 gate: every re-exporting module under infrastructure/ must | |
| # declare __all__ — see docs/rules/api_design.md. Prevents regression | |
| # of the [attr-defined] mypy class of bug. | |
| - name: Audit __all__ on re-exporting modules | |
| run: uv run python -m infrastructure.skills check-all-exports | |
| - name: Reject tracked generated artifacts | |
| run: uv run python scripts/check_tracked_generated_artifacts.py | |
| - name: Confidentiality guard — only template projects tracked | |
| run: uv run python scripts/check_tracked_projects.py | |
| # Enforce the module-size composability budget as a hard gate (fails at | |
| # >=950 infra / >=250 project-script lines). It also runs inside the | |
| # informational health job below, but that job is wrapped in ``|| true`` | |
| # and never blocks — so without this step an oversized module could land | |
| # on main unnoticed. Advisory warnings (>=800) still only warn. | |
| - name: Module line-count gate — composability budget | |
| run: uv run python scripts/gates/module_line_count_check.py | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| # Job 1b — Unified health report (informational, no fail) | |
| # | |
| # MED2: ``infrastructure.core.health`` aggregates every quality gate into a | |
| # single typed ``HealthReport``. The job runs after ``lint`` and uploads the | |
| # JSON for triage; ``|| true`` keeps the workflow green even if individual | |
| # gates regress here (their dedicated jobs above are the blocking ones). | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| health: | |
| name: Unified Health Report (informational) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| needs: [lint] | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| - uses: ./.github/actions/setup-python-env | |
| - name: Sync dependencies | |
| run: uv sync | |
| - name: Run unified health checks (informational) | |
| run: | | |
| uv run python -m infrastructure.core.health --json --quiet \ | |
| > health-report.json || true | |
| - name: Upload health report | |
| if: always() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: health-report | |
| path: health-report.json | |
| if-no-files-found: warn | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| # Job 2 — No-Mocks Policy Verification | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| verify-no-mocks: | |
| name: Verify No Mocks Policy | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| needs: [lint] | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| - uses: ./.github/actions/setup-python-env | |
| - name: Sync dependencies | |
| run: uv sync | |
| - name: Verify no mock usage in tests | |
| run: uv run python scripts/verify_no_mocks.py | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| # Job 2b — Setup hook on Windows (conditional smoke) | |
| # | |
| # When any active project ships ``projects/**/scripts/setup_hook.py``, verify | |
| # hook discovery and subprocess paths on ``windows-latest`` (``.sh`` hooks are | |
| # intentionally skipped there — see TO-DO.md / infrastructure.project.setup_hook). | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| setup-hook-windows-smoke: | |
| name: Setup hook (Windows smoke) | |
| runs-on: windows-latest | |
| timeout-minutes: 15 | |
| needs: [verify-no-mocks, detect] | |
| if: needs.detect.outputs.setup_hook == 'true' | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| - uses: ./.github/actions/setup-python-env | |
| - name: Sync dependencies | |
| run: uv sync | |
| - name: Run setup_hook tests (Windows) | |
| env: | |
| PYTHONUTF8: "1" | |
| run: >- | |
| uv run pytest tests/infra_tests/project/test_setup_hook.py -v | |
| --timeout=120 | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| # Job 3 — Infrastructure Tests (matrix: ubuntu + macos × Python 3.10–3.12) | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| test-infra: | |
| name: "Infra Tests (${{ matrix.os }}, Python ${{ matrix.python-version }})" | |
| runs-on: ${{ matrix.os }} | |
| timeout-minutes: 30 | |
| needs: [verify-no-mocks] | |
| permissions: | |
| contents: read | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| # ubuntu covers all three Python versions; macOS runs only the 3.12 | |
| # smoke (macOS legs are ~10x cost and rarely surface OS-specific project | |
| # breakage beyond what the 3.12 cell catches). | |
| os: [ubuntu-latest] | |
| python-version: ["3.10", "3.11", "3.12"] | |
| include: | |
| - os: macos-latest | |
| python-version: "3.12" | |
| steps: | |
| - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| - uses: ./.github/actions/setup-python-env | |
| with: | |
| python-version: ${{ matrix.python-version }} | |
| - name: Sync dependencies (dev + optional test groups) | |
| run: uv sync --group rendering --group monitoring | |
| - name: Fix macOS socket.getfqdn timeout | |
| if: runner.os == 'macOS' | |
| run: sudo scutil --set HostName "$(hostname)" | |
| - name: Install pandoc | |
| uses: pandoc/actions/setup@86321b6dd4675f5014c611e05088e10d4939e09e # v1.1.1 | |
| # TeX Live so the xelatex/bibtex-gated rendering tests run instead of | |
| # skipping (they assert a real PDF lands on disk). Linux lanes only: | |
| # MacTeX is a multi-GB install and macOS breadth is a 3.12 smoke lane. | |
| - name: Install TeX Live (xelatex + bibtex) | |
| if: runner.os == 'Linux' | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y --no-install-recommends \ | |
| texlive-xetex texlive-latex-extra texlive-bibtex-extra \ | |
| texlive-fonts-recommended lmodern | |
| - name: Run infrastructure tests | |
| env: | |
| COVERAGE_FILE: .coverage.infra | |
| # macOS runners have socket.getfqdn timeout issues with werkzeug/httpserver | |
| continue-on-error: ${{ runner.os == 'macOS' }} | |
| # -n auto parallelizes across runner cores (pytest-xdist); the suite is | |
| # parallel-safe (no-mocks tests use per-test tmp_path + random-port | |
| # httpserver). pytest-cov combines per-worker data before the gate. | |
| run: >- | |
| uv run pytest tests/infra_tests/ | |
| -n auto | |
| --cov=infrastructure | |
| --cov-report=term-missing | |
| --cov-report=xml:coverage-infra.xml | |
| --cov-fail-under=60 | |
| --durations=10 | |
| -m "not requires_ollama and not slow and not bench" | |
| --timeout=120 | |
| - name: Upload infrastructure coverage to Codecov | |
| if: matrix.python-version == '3.12' && matrix.os == 'ubuntu-latest' | |
| uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 | |
| with: | |
| files: coverage-infra.xml | |
| flags: infrastructure | |
| name: infra-coverage | |
| fail_ci_if_error: false | |
| env: | |
| CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| # Job 4 — Project Tests (one parallel job per public exemplar) | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| test-project: | |
| name: "Project Tests (${{ matrix.project }}, py${{ matrix.python-version }})" | |
| runs-on: ubuntu-latest | |
| # Each public exemplar runs in its OWN parallel job, so wall-clock is the | |
| # slowest single project (~active_inference) instead of the ~45 min sequential | |
| # sum of all nine. Each job enforces that project's own 90% floor via | |
| # ``scripts/01_run_tests.py --project`` (authoritative per CLAUDE.md), which | |
| # also removes the old code_project/fep_lean conftest plugin-name collision | |
| # (every project is already isolated in its own job). py3.10 (floor) + py3.12 | |
| # (ceiling) give cross-version coverage; macOS breadth is handled by test-infra. | |
| # 60 min backstop — active_inference on py3.10 has exceeded 45 min on loaded runners. | |
| timeout-minutes: 60 | |
| needs: [verify-no-mocks, detect-projects] | |
| permissions: | |
| contents: read | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| python-version: ["3.10", "3.12"] | |
| # Public-exemplar list is derived at runtime from the canonical roster | |
| # (infrastructure.project.public_scope) by the detect-projects job, so | |
| # adding/retiring an exemplar under projects/templates/ extends or trims | |
| # this matrix without editing any literal here (CI-MATRIX-DYNAMIC-1). | |
| project: ${{ fromJSON(needs.detect-projects.outputs.projects) }} | |
| steps: | |
| - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| - uses: ./.github/actions/setup-python-env | |
| with: | |
| python-version: ${{ matrix.python-version }} | |
| - name: Sync dependencies (dev + optional test groups) | |
| run: uv sync --group rendering --group monitoring --group discopy | |
| # 01_run_tests.py --project runs pytest with cwd=<project> and enforces that | |
| # project's own --cov-fail-under=90 internally, writing coverage into the | |
| # project dir (projects/<name>/.coverage.project + coverage_project.json). | |
| # The gate is therefore authoritative here; no repo-root coverage step. | |
| - name: Run project tests | |
| env: | |
| PYTHONPATH: . | |
| run: | | |
| set -euo pipefail | |
| uv run python scripts/01_run_tests.py \ | |
| --project "${{ matrix.project }}" --project-only --include-slow | |
| - name: Upload project coverage to Codecov | |
| if: matrix.python-version == '3.12' | |
| uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 | |
| with: | |
| files: projects/${{ matrix.project }}/coverage_project.json | |
| flags: projects | |
| name: project-coverage | |
| fail_ci_if_error: false | |
| env: | |
| CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| # Job 4b — fep_lean (real math-inc gauss + elan lake/lean; excluded from matrix above) | |
| # This job uses projects/fep_lean/ paths. When fep_lean is not checked out | |
| # (it lives in the private repo's working/ pool), the detect job emits | |
| # fep_lean=false and this job is skipped. Check it out flat under projects/ to | |
| # activate CI: | |
| # cp -r <private-projects>/working/fep_lean projects/fep_lean | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| fep-lean: | |
| name: fep_lean (gauss + lake) | |
| if: needs.detect.outputs.fep_lean == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 60 | |
| needs: [verify-no-mocks, detect] | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| - uses: ./.github/actions/setup-python-env | |
| - name: Sync dependencies (dev + optional test groups) | |
| run: uv sync --group rendering --group monitoring | |
| - name: Sync fep_lean project (explicit venv + dev extras) | |
| run: uv sync --directory projects/fep_lean --extra dev | |
| - name: Install elan (lean + lake) | |
| run: | | |
| set -euxo pipefail | |
| curl -sSf https://raw.githubusercontent.com/leanprover/elan/master/elan-init.sh | sh -s -- -y | |
| echo "$HOME/.elan/bin" >> "$GITHUB_PATH" | |
| - name: Pin Lean toolchain and warm Lake build | |
| working-directory: projects/fep_lean/lean | |
| run: | | |
| set -euxo pipefail | |
| TOOLCHAIN=$(tr -d '[:space:]' < lean-toolchain) | |
| elan override set "$TOOLCHAIN" | |
| lean --version | |
| lake --version | |
| lake build | |
| - name: Reject sorry in Lean sketches | |
| working-directory: projects/fep_lean/lean/FepSketches | |
| run: | | |
| set -euo pipefail | |
| SORRY_LINES=$(grep -n 'sorry' fep_all.lean Basic.lean \ | |
| | grep -Ev ':[[:space:]]*--' || true) | |
| if [ -n "$SORRY_LINES" ]; then | |
| echo "ERROR: 'sorry' found in non-comment Lean lines:" | |
| echo "$SORRY_LINES" | |
| exit 1 | |
| fi | |
| echo "No sorry in Lean sketches." | |
| - name: Install Open Gauss CLI | |
| timeout-minutes: 25 | |
| env: | |
| OPEN_GAUSS_AUTO_ATTACH: "0" | |
| run: | | |
| set -euxo pipefail | |
| export PATH="$HOME/.local/bin:$PATH" | |
| git clone --depth 1 https://github.com/math-inc/OpenGauss.git "$RUNNER_TEMP/OpenGauss" | |
| cd "$RUNNER_TEMP/OpenGauss" | |
| # Pin to the reviewed HEAD SHA; update this value intentionally when upgrading. | |
| EXPECTED_SHA="f87633900ae185b8037bf451a914fe7eeae1eb08" | |
| ACTUAL_SHA="$(git rev-parse HEAD)" | |
| if [ "$ACTUAL_SHA" != "$EXPECTED_SHA" ]; then | |
| echo "ERROR: OpenGauss HEAD SHA mismatch." | |
| echo " Expected: $EXPECTED_SHA" | |
| echo " Got: $ACTUAL_SHA" | |
| echo "Update EXPECTED_SHA in .github/workflows/ci.yml after reviewing the diff." | |
| exit 1 | |
| fi | |
| ./scripts/install.sh --plain --noninteractive --skip-system-packages | |
| echo "$HOME/.local/bin" >> "$GITHUB_PATH" | |
| command -v gauss | |
| gauss doctor | |
| - name: Run fep_lean tests (real gauss doctor + lake build) | |
| working-directory: projects/fep_lean | |
| env: | |
| COVERAGE_FILE: ../../.coverage.fep_lean | |
| run: >- | |
| uv run pytest tests/ | |
| --timeout=1200 | |
| --cov=src | |
| --cov-report=term-missing | |
| --cov-fail-under=89 | |
| --durations=10 | |
| -m "not requires_ollama" | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| # Job 5 — Validate Manuscripts | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| validate: | |
| name: Validate Manuscripts | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| needs: [lint] | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| - uses: ./.github/actions/setup-python-env | |
| - name: Sync dependencies | |
| run: uv sync | |
| - name: Resolve public project names | |
| id: project-scope | |
| run: echo "names=$(uv run python -m infrastructure.project.public_scope project-names)" >> "$GITHUB_OUTPUT" | |
| - name: Validate manuscript markdown | |
| # The `markdown` subcommand takes ONE directory; project globs | |
| # shell-globs to multiple dirs (CLI then errors "unrecognized | |
| # arguments"). Validate each public project's manuscript in turn. | |
| run: | | |
| set -euo pipefail | |
| for project in ${{ steps.project-scope.outputs.names }}; do | |
| d="projects/$project/manuscript" | |
| [ -d "$d" ] || continue | |
| echo "::group::validate $d" | |
| uv run python -m infrastructure.validation.cli markdown "$d" | |
| echo "::endgroup::" | |
| done | |
| - name: Verify api-reference.md is in sync with __all__ | |
| # Auto-generated from each `infrastructure/<pkg>/__init__.py` `__all__` | |
| # by `scripts/generate_api_reference_doc.py`. Drift fails the build; | |
| # regenerate locally with `--write` and commit the result. | |
| run: uv run python scripts/generate_api_reference_doc.py --check | |
| - name: Verify exemplar_roster.md is in sync with the live roster | |
| # Auto-generated from the public exemplar roster by | |
| # `scripts/generate_exemplar_roster_doc.py`. Adding a test file to an | |
| # exemplar changes its row; without this gate the committed roster | |
| # drifted silently and was only caught by the full infra pytest run. | |
| # Drift fails the build; regenerate locally and commit the result. | |
| run: uv run python scripts/generate_exemplar_roster_doc.py --check | |
| - name: Verify COUNTS.md is in sync with the live tree | |
| # Auto-generated from live repo state by `scripts/generate_counts.py` | |
| # (tracked infrastructure .py count, project/publishing test-collection | |
| # totals, exemplar roster, module list). Replaces the formerly | |
| # hand-maintained COUNTS.md whose drift was chased across ~40 | |
| # commits. Drift fails the build; regenerate locally with `--write` and | |
| # commit the result. | |
| run: uv run python scripts/generate_counts.py --check | |
| - name: Verify project imports | |
| env: | |
| PUBLIC_PROJECTS: ${{ steps.project-scope.outputs.names }} | |
| # Public project names are now QUALIFIED (``templates/<name>``) and the | |
| # projects/ tree is not a Python package chain (no projects/__init__.py), | |
| # so a dotted ``projects.<name>.src`` import cannot resolve. Import each | |
| # project's ``src`` package in an ISOLATED subprocess with the project's | |
| # own sys.path (repo root for ``infrastructure``, project dir, and its | |
| # ``src``) — the same way pytest's pythonpath is configured — which also | |
| # avoids the top-level ``src`` name collision across projects. | |
| run: | | |
| uv run python -c " | |
| import os | |
| import subprocess | |
| import sys | |
| repo = os.getcwd() | |
| projects = os.environ['PUBLIC_PROJECTS'].split() | |
| if not projects: | |
| print('No projects found — skipping import check') | |
| sys.exit(0) | |
| failed = False | |
| for project in projects: | |
| proj_dir = os.path.join(repo, 'projects', *project.split('/')) | |
| env = dict(os.environ) | |
| env['PYTHONPATH'] = os.pathsep.join([repo, proj_dir, os.path.join(proj_dir, 'src')]) | |
| result = subprocess.run([sys.executable, '-c', 'import src'], env=env, capture_output=True, text=True) | |
| if result.returncode == 0: | |
| print(f' OK: {project} (src)') | |
| else: | |
| tail = (result.stderr.strip().splitlines() or [str(result.returncode)])[-1] | |
| print(f' FAIL: {project}: {tail}') | |
| failed = True | |
| if failed: | |
| sys.exit(1) | |
| print(f'All {len(projects)} project(s) imported successfully') | |
| " | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| # Job 6 — Security Scan | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| security: | |
| name: Security Scan | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| needs: [lint] | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| - uses: ./.github/actions/setup-python-env | |
| - name: Sync dependencies | |
| run: uv sync | |
| - name: Dependency audit (pip-audit) | |
| run: | | |
| set -euo pipefail | |
| IGNORE_FILE=".github/pip-audit-ignore.txt" | |
| PIP_AUDIT_ARGS=() | |
| while IFS= read -r raw || [ -n "$raw" ]; do | |
| [[ "$raw" =~ ^[[:space:]]*# ]] && continue | |
| line="${raw%%#*}" | |
| line="$(echo "$line" | xargs)" | |
| [ -z "$line" ] && continue | |
| PIP_AUDIT_ARGS+=(--ignore-vuln "$line") | |
| done < "$IGNORE_FILE" | |
| for attempt in 1 2 3; do | |
| if uv run pip-audit "${PIP_AUDIT_ARGS[@]}"; then | |
| exit 0 | |
| fi | |
| echo "pip-audit attempt ${attempt} failed; retrying in 15s..." >&2 | |
| sleep 15 | |
| done | |
| exit 1 | |
| - name: Code security scan (Bandit MEDIUM+ severity) | |
| run: >- | |
| uv run bandit -c bandit.yaml -r -ll | |
| infrastructure/ | |
| scripts/ | |
| projects/ | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| # Job 6b — Documentation Lint (mermaid + cross-links + consistency + doc pairs) | |
| # | |
| # Rules enforced: | |
| # 1. Every fenced ```mermaid block in long-lived docs must render with the | |
| # real `mmdc` (mermaid-cli) binary backed by chrome-headless-shell. | |
| # 2. Every relative Markdown link must resolve on disk (skipping fenced and | |
| # inline-code spans). | |
| # 3. ``N Python (sub)packages`` claims in long-lived docs must match the | |
| # live count under ``infrastructure/``; rotating project names must not | |
| # appear unconditionally hard-coded outside ``docs/_generated/``. | |
| # 4. Permanent-template content folders must carry paired ``AGENTS.md`` and | |
| # ``README.md`` files. | |
| # | |
| # This job intentionally fails LOUDLY when mmdc / chrome-headless-shell is | |
| # missing instead of skipping — see ``infrastructure/validation/docs/mermaid_lint.py``. | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| docs-lint: | |
| name: Documentation Lint | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| needs: [lint] | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| - uses: ./.github/actions/setup-python-env | |
| - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 | |
| with: | |
| node-version: "20" | |
| - name: Sync Python dependencies | |
| run: uv sync | |
| - name: Install mermaid-cli | |
| run: npm install -g @mermaid-js/mermaid-cli | |
| - name: Install chrome-headless-shell for puppeteer | |
| run: npx --yes puppeteer browsers install chrome-headless-shell | |
| - name: Locate chrome-headless-shell binary | |
| id: chrome | |
| run: | | |
| set -euo pipefail | |
| CHROME_PATH=$(find "$HOME/.cache/puppeteer/chrome-headless-shell" -type f -name 'chrome-headless-shell' | head -n1) | |
| if [ -z "$CHROME_PATH" ]; then | |
| echo "ERROR: chrome-headless-shell binary not found" | |
| exit 1 | |
| fi | |
| echo "CHROME_EXECUTABLE_PATH=$CHROME_PATH" >> "$GITHUB_ENV" | |
| echo "Resolved chrome at: $CHROME_PATH" | |
| - name: Run docs linters (mermaid + cross-links + consistency + doc pairs) | |
| run: uv run python scripts/lint_docs.py --quiet | |
| - name: Check template exemplar drift (strict — dead links gate) | |
| run: uv run python scripts/check_template_drift.py --strict | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| # Job 7 — Performance Check | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| performance: | |
| name: Performance Check | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| needs: [test-infra, test-project] | |
| permissions: | |
| actions: read | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| - uses: ./.github/actions/setup-python-env | |
| - name: Sync dependencies | |
| run: uv sync | |
| - name: Run import benchmarks | |
| run: | | |
| uv run python -c " | |
| import time | |
| import sys | |
| import importlib | |
| from pathlib import Path | |
| from infrastructure.project.public_scope import public_project_names | |
| THRESHOLD_SECONDS = 5.0 | |
| results = {} | |
| # Infrastructure core | |
| t0 = time.perf_counter() | |
| try: | |
| import infrastructure.core | |
| results['infrastructure.core'] = time.perf_counter() - t0 | |
| except ImportError as e: | |
| print(f'SKIP infrastructure.core: {e}') | |
| # Public project scope only; runtime discovery remains broader. | |
| # project names contain '/' (e.g. 'templates/template_code_project'), | |
| # which makes them invalid Python module identifiers. Use a subprocess | |
| # with PYTHONPATH set to the project dir so 'import src' resolves | |
| # correctly and wall-clock time is actually recorded. | |
| import subprocess | |
| import os | |
| projects = public_project_names(Path('.')) | |
| for project in projects: | |
| project_dir = str(Path('.') / 'projects' / project) | |
| label = f'projects/{project}/src' | |
| env = {**os.environ, 'PYTHONPATH': project_dir} | |
| t0 = time.perf_counter() | |
| proc = subprocess.run( | |
| [sys.executable, '-c', 'import src'], | |
| env=env, capture_output=True, | |
| ) | |
| elapsed = time.perf_counter() - t0 | |
| if proc.returncode == 0: | |
| results[label] = elapsed | |
| else: | |
| print(f'SKIP {label}: {proc.stderr.decode().strip()}') | |
| total = sum(results.values()) | |
| print('Import timing:') | |
| for name, elapsed in results.items(): | |
| print(f' {elapsed:.3f}s {name}') | |
| print(f'Total: {total:.3f}s (threshold: {THRESHOLD_SECONDS}s)') | |
| if total > THRESHOLD_SECONDS: | |
| print('ERROR: Import time exceeds threshold') | |
| sys.exit(1) | |
| print('Performance check passed') | |
| " | |
| # Informational microbench harness for setup_hook + analysis_pipeline (MED6). | |
| # `|| true` keeps this strictly informational — a slow bench will never | |
| # fail the build. Results are uploaded as a CI artifact for trend analysis. | |
| - name: Run setup_hook + analysis_pipeline microbenches (informational) | |
| run: | | |
| uv run pytest tests/infra_tests/benchmark/ -m bench --benchmark-only \ | |
| --benchmark-min-rounds=3 --benchmark-json=bench-results.json \ | |
| --timeout=180 || true | |
| - name: Upload microbench results | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: bench-results | |
| path: bench-results.json | |
| if-no-files-found: warn | |
| # MED3 — coverage trend dashboard. Pulls last 30 days of CI artefacts | |
| # via `gh run download` and renders ``docs/_generated/coverage_history.md``. | |
| # Strictly informational (`|| true`); the result is uploaded as the | |
| # ``coverage-history`` artefact for trend review. | |
| - name: Generate coverage history dashboard (informational) | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| uv run python scripts/generate_coverage_history.py --from-gh --days=30 || true | |
| - name: Upload coverage history dashboard | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: coverage-history | |
| path: docs/_generated/coverage_history.md | |
| if-no-files-found: warn |