ci: run only the lanes a PR affects (python/frontend/site)

Heavy PR checks run on every PR because the workflows deliberately avoid
`on.paths` filters — a path-gated workflow leaves its required check pending
forever when no matching file changes, blocking merge. So a docs-only PR
still spins up the TypeScript matrix, the full Python suite, and ruff/ty.

Keep every workflow triggering on every PR (checks always report) but gate
the expensive *steps* on what the PR touches. Skipping a step (not the job)
leaves the job green, so required checks never hang — the same idiom already
proven in contributor-check.yml.

A classifier (scripts/ci/classify_changes.py) maps the PR diff to three
lanes — python, frontend, site — surfaced as step outputs by a composite
action (.github/actions/detect-changes). Fail-open: an empty diff or any
.github/ change runs everything; python is a denylist (skipped only when
every file is provably prose or a frontend-only package); skills/**/SKILL.md
counts as python-relevant since the skill-doc tests read that tree. Non-PR
events always run the full pipeline.
This commit is contained in:
Brooklyn Nicholson 2026-06-19 16:46:11 -05:00 committed by ethernet
parent 351afd353d
commit 45540cfb5e
7 changed files with 272 additions and 5 deletions

View file

@ -31,8 +31,18 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0 # full history so detect-changes can diff base...head
# On PRs that touch no Python, every step below is skipped and the job
# reports green. The check still runs (no `on.paths` filter), so the
# required status never hangs.
- name: Detect affected areas
id: changes
uses: ./.github/actions/detect-changes
- name: Restore duration cache
if: steps.changes.outputs.python == 'true'
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: test_durations.json
@ -44,6 +54,7 @@ jobs:
key: test-durations
- name: Install ripgrep (prebuilt binary)
if: steps.changes.outputs.python == 'true'
run: |
set -euo pipefail
RG_VERSION=15.1.0
@ -58,6 +69,7 @@ jobs:
rg --version
- name: Install uv
if: steps.changes.outputs.python == 'true'
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5
with:
# Persist uv's download/wheel cache (~/.cache/uv) across runs.
@ -71,9 +83,11 @@ jobs:
uv.lock
- name: Set up Python 3.11
if: steps.changes.outputs.python == 'true'
run: uv python install 3.11
- name: Install dependencies
if: steps.changes.outputs.python == 'true'
# `uv sync --locked` installs the exact pinned set from uv.lock (and
# fails if the lock is out of sync with pyproject.toml), giving a
# reproducible env. It also creates .venv itself, so no separate
@ -81,11 +95,13 @@ jobs:
run: uv sync --locked --python 3.11 --extra all --extra dev
- name: Minimize uv cache
if: steps.changes.outputs.python == 'true'
# Optimized for CI: prunes pre-built wheels that are cheap to
# re-download, keeping the persisted cache small and fast to restore.
run: uv cache prune --ci
- name: Run tests (slice ${{ matrix.slice }}/6)
if: steps.changes.outputs.python == 'true'
# Per-file isolation via scripts/run_tests_parallel.py: discovers
# every test_*.py file under tests/ (excluding integration/ + e2e/),
# then runs `python -m pytest <file>` in a freshly-spawned subprocess
@ -119,6 +135,7 @@ jobs:
NOUS_API_KEY: ""
- name: Upload per-slice durations
if: steps.changes.outputs.python == 'true'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: test-durations-slice-${{ matrix.slice }}
@ -164,8 +181,15 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0 # full history so detect-changes can diff base...head
- name: Detect affected areas
id: changes
uses: ./.github/actions/detect-changes
- name: Install ripgrep (prebuilt binary)
if: steps.changes.outputs.python == 'true'
run: |
set -euo pipefail
RG_VERSION=15.1.0
@ -180,6 +204,7 @@ jobs:
rg --version
- name: Install uv
if: steps.changes.outputs.python == 'true'
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5
with:
# Persist uv's download/wheel cache (~/.cache/uv) across runs.
@ -193,9 +218,11 @@ jobs:
uv.lock
- name: Set up Python 3.11
if: steps.changes.outputs.python == 'true'
run: uv python install 3.11
- name: Install dependencies
if: steps.changes.outputs.python == 'true'
# `uv sync --locked` installs the exact pinned set from uv.lock (and
# fails if the lock is out of sync with pyproject.toml), giving a
# reproducible env. It also creates .venv itself, so no separate
@ -203,16 +230,19 @@ jobs:
run: uv sync --locked --python 3.11 --extra all --extra dev
- name: Minimize uv cache
if: steps.changes.outputs.python == 'true'
# Optimized for CI: prunes pre-built wheels that are cheap to
# re-download, keeping the persisted cache small and fast to restore.
run: uv cache prune --ci
- name: Packaged-wheel i18n smoke test
if: steps.changes.outputs.python == 'true'
run: |
source .venv/bin/activate
python -m pytest -m integration tests/test_wheel_locales_e2e.py -v
- name: Run e2e tests
if: steps.changes.outputs.python == 'true'
run: |
source .venv/bin/activate
python -m pytest tests/e2e/ -v --tb=short