From 0e72313ec5e6e9d186a3771e082de9e70233e5db Mon Sep 17 00:00:00 2001 From: w4ffl35 <25737761+w4ffl35@users.noreply.github.com> Date: Sat, 19 Sep 2026 12:24:56 -0600 Subject: [PATCH] ci: path-filter a leaf module to a fast lane (hq#14) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a `changes` classifier and a torch-free `leaf` lane so a change confined to `spikeforge/observability/**` (or its tests) runs a small deterministic check set — ruff, mypy on the core package, the observability tests, and a torch-free-import proof — instead of the seventeen-job matrix on the self-hosted pool. A `push` to main, or any non-leaf file, still takes the full path. Skipped jobs report success, so the required checks the ruleset enforces stay satisfied on the leaf lane. `leaf` should be added to the ruleset's required checks once this lands so a failing leaf PR cannot merge. Refs Capsize-Games/hq#14 --- .github/workflows/ci.yml | 94 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5063db6..e7c06f1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,8 +15,47 @@ env: TORCH_INDEX_URL: https://download.pytorch.org/whl/cpu jobs: + changes: + # Classify a pull request so a leaf-module edit runs a fast, deterministic + # check set instead of the full matrix (hq#14). Runs on a GitHub-hosted + # runner so it is never itself queued behind the self-hosted pool it exists + # to shorten; a `push` to main always takes the full path. + runs-on: ubuntu-latest + outputs: + leaf: ${{ steps.classify.outputs.leaf }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - id: classify + name: Classify the changed paths + env: + EVENT: ${{ github.event_name }} + BASE: ${{ github.base_ref }} + run: | + set -eu + leaf=false + if [ "$EVENT" = "pull_request" ] && [ -n "${BASE:-}" ]; then + git fetch --no-tags origin "$BASE" + changed="$(git diff --name-only "origin/$BASE...HEAD")" + echo "changed files:" + echo "$changed" + if [ -n "$changed" ]; then + leaf=true + while IFS= read -r path; do + case "$path" in + spikeforge/observability/*|tests/test_observability_*.py) ;; + *) leaf=false; break ;; + esac + done <<< "$changed" + fi + fi + echo "leaf=$leaf" >> "$GITHUB_OUTPUT" lint: # Static checks are interpreter-version agnostic; run them once. + needs: changes + if: needs.changes.outputs.leaf != 'true' runs-on: [self-hosted, linux, x64, spikeforge-ci] steps: - uses: actions/checkout@v4 @@ -44,6 +83,8 @@ jobs: test: # The full suite across every supported interpreter (python_requires). + needs: changes + if: needs.changes.outputs.leaf != 'true' runs-on: [self-hosted, linux, x64, spikeforge-ci] strategy: fail-fast: false @@ -80,6 +121,8 @@ jobs: # SDK-backed cell must report `available: false` with a named reason — that # is honest reporting, not a build failure. The job asserts the command # exits 0 *and* that the JSON carries one cell per registered target. + needs: changes + if: needs.changes.outputs.leaf != 'true' runs-on: [self-hosted, linux, x64, spikeforge-ci] steps: - uses: actions/checkout@v4 @@ -131,6 +174,8 @@ jobs: # The `hub` core extra is gone: the hub now ships as the `spikeforge-hub` # distribution (`spikeforge_hub` import root) and this job proves it installs from # source and its offline catalog/CLI work. + needs: changes + if: needs.changes.outputs.leaf != 'true' runs-on: [self-hosted, linux, x64, spikeforge-ci] steps: - uses: actions/checkout@v4 @@ -167,6 +212,8 @@ jobs: # install-only here (the suite asserts the SDK-gated backends are # *absent*), and `docs` builds the site. `hub` is gone too: it is the # `spikeforge-hub` distribution, exercised by the `hub` job above. + needs: changes + if: needs.changes.outputs.leaf != 'true' runs-on: [self-hosted, linux, x64, spikeforge-ci] strategy: fail-fast: false @@ -229,6 +276,8 @@ jobs: # *core* degrades without the server stack, not that the server runs while # its own base deps are blocked. The 18 server-dependent test files are # enumerated with `--ignore` below; everything else runs under the blocker. + needs: changes + if: needs.changes.outputs.leaf != 'true' runs-on: [self-hosted, linux, x64, spikeforge-ci] env: PYTHONPATH: ${{ github.workspace }}/scripts/blocked_deps @@ -297,6 +346,8 @@ jobs: # A broken documentation link must fail the pull request, not just a local # `build_docs.sh --check`. This gate covers the MkDocs tree in `docs/`; # the `published-surfaces` job below covers what readers actually land on. + needs: changes + if: needs.changes.outputs.leaf != 'true' runs-on: [self-hosted, linux, x64, spikeforge-ci] steps: - uses: actions/checkout@v4 @@ -323,6 +374,8 @@ jobs: # # Deliberately torch-free: readme_renderer plus the stdlib is the whole # dependency set, so this stays one of the fastest jobs in the matrix. + needs: changes + if: needs.changes.outputs.leaf != 'true' runs-on: [self-hosted, linux, x64, spikeforge-ci] steps: - uses: actions/checkout@v4 @@ -347,6 +400,8 @@ jobs: # in capsize-games/spikeforge-dashboard (its own CI runs `npm run build`). Core keeps # only the protocol-codegen guard so a schema change without regenerated # types is still caught here. + needs: changes + if: needs.changes.outputs.leaf != 'true' runs-on: [self-hosted, linux, x64, spikeforge-ci] steps: - uses: actions/checkout@v4 @@ -373,6 +428,8 @@ jobs: # The static half of the core boundary: no module-level import of a # forbidden root, and no function-local import outside the lazy shims. # Pure stdlib, so no dependencies are installed. + needs: changes + if: needs.changes.outputs.leaf != 'true' runs-on: [self-hosted, linux, x64, spikeforge-ci] steps: - uses: actions/checkout@v4 @@ -388,6 +445,8 @@ jobs: # The artifact half of the core boundary: a core wheel built with NO extras # must install, import, and pull in none of the thirteen forbidden # distributions, and must not contain `server/`. + needs: changes + if: needs.changes.outputs.leaf != 'true' runs-on: [self-hosted, linux, x64, spikeforge-ci] steps: - uses: actions/checkout@v4 @@ -450,6 +509,8 @@ jobs: packaging-guards: # The three lightweight topology guards: disjoint import roots (PEP 420), # console-script ownership, and satellite pins == compatibility.json. + needs: changes + if: needs.changes.outputs.leaf != 'true' runs-on: [self-hosted, linux, x64, spikeforge-ci] steps: - uses: actions/checkout@v4 @@ -479,3 +540,36 @@ jobs: --targets-wheel packages/spikeforge-targets/dist/spikeforge_targets-*.whl \ --hub-wheel packages/spikeforge-hub/dist/spikeforge_hub-*.whl \ --io-wheel packages/spikeforge-io/dist/spikeforge_io-*.whl + + leaf: + # The fast lane. The leaf paths above cannot reach torch, the server stack + # or the satellite distributions, so this covers the touched module without + # the matrix. Deliberately torch-free: the package is imported from source + # via PYTHONPATH, so no ML wheel is installed. + needs: changes + if: needs.changes.outputs.leaf == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install the leaf toolchain (no torch) + run: | + python -m pip install --upgrade pip + pip install pytest pytest-cov ruff mypy "capsize-commons>=0.1.1" + + - name: Lint (ruff) + run: ruff check . + + - name: Type-check the core package + run: mypy spikeforge/ + + - name: Leaf tests + run: PYTHONPATH=. pytest tests/test_observability_logging.py + + - name: The logging surface must import without the ML stack + run: | + python -c "import sys; import spikeforge.observability.logging_setup as m; assert 'torch' not in sys.modules; print('torch-free OK:', m.LOGGER_NAME)"