Skip to content

Merge PR #20: Active Inference EFE decomposition figure #284

Merge PR #20: Active Inference EFE decomposition figure

Merge PR #20: Active Inference EFE decomposition figure #284

Workflow file for this run

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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- 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
# ───────────────────────────────────────────────────────────────────────────
# 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
enable-cache: true
cache-dependency-glob: "**/uv.lock"
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.12"
- 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
enable-cache: true
cache-dependency-glob: "**/uv.lock"
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.12"
- 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
enable-cache: true
cache-dependency-glob: "**/uv.lock"
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.12"
- 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
enable-cache: true
cache-dependency-glob: "**/uv.lock"
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.12"
- 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
enable-cache: true
cache-dependency-glob: "**/uv.lock"
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
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
- 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' }}
run: >-
uv run pytest tests/infra_tests/
--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@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
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.
# 45 min is a generous backstop — only active_inference approaches it.
timeout-minutes: 45
needs: [verify-no-mocks]
permissions:
contents: read
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.12"]
project:
- templates/template_active_inference
- templates/template_autoresearch_project
- templates/template_autoscientists
- templates/template_code_project
- templates/template_newspaper
- templates/template_prose_project
- templates/template_sia
- templates/template_template
- templates/template_textbook
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
enable-cache: true
cache-dependency-glob: "**/uv.lock"
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
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@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
enable-cache: true
cache-dependency-glob: "**/uv.lock"
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.12"
- 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
enable-cache: true
cache-dependency-glob: "**/uv.lock"
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.12"
- 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 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
enable-cache: true
cache-dependency-glob: "**/uv.lock"
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.12"
- 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
enable-cache: true
cache-dependency-glob: "**/uv.lock"
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.12"
- 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
enable-cache: true
cache-dependency-glob: "**/uv.lock"
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.12"
- 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/bench/ -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