Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions .claude/rules/ci-change-detection.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ is wrong (see §1) — fix that instead of widening the gate.
| non-code | `non_code_only` | can this diff change what the code does at all? |
| architecture | `a2a3_changed` / `a5_changed` | which silicon can it reach? |
| test category | `st_affected` / `ut_affected` | which suite can it break? |
| example corpus | `examples_only` | can it reach a job that builds the product and runs a fixed payload? |

They are layered, not redundant. **Every arch and category flag subtracts
`NON_CODE` before deciding**, so a non-code-only change already makes all four
Expand All @@ -71,9 +72,18 @@ A job composes the axes it is actually subject to:
| Job family | Gate |
| ---------- | ---- |
| `st-sim-*`, `st-onboard-*` | `<arch>_changed && st_affected` |
| `profiling-flags-smoke` | `(a2a3_changed \|\| a5_changed) && st_affected` |
| `profiling-flags-smoke` | `(a2a3_changed \|\| a5_changed) && !examples_only` |
| `ut`, `ut-a2a3`, `ut-a5` | `non_code_only != true && ut_affected` |
| `packaging-matrix` | `non_code_only != true` |
| `packaging-matrix` | `non_code_only != true && !examples_only` |

`packaging-matrix` and `profiling-flags-smoke` build and install the product and
then exercise it with a **fixed, tiny payload** — one entry-point script and one
`vector_example` respectively. They are the only jobs that consume neither test
suite as a corpus, which is why they take the example axis rather than the
category one. Note what stays in: `tests/` still triggers packaging, because
`tools/verify_packaging.sh` runs a `tests/st/` file as its entry-point smoke and
`tests/` is in the sdist `include`. Only `examples/` is provably absent from
both jobs.

The UT jobs stay off the arch axis on purpose — unit tests cover shared
contracts, so the cost of a falsely-skipped regression outweighs the minutes.
Expand Down
22 changes: 20 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ jobs:
# See docs/python-packaging.md and tools/verify_packaging.sh.
packaging-matrix:
needs: [detect-changes, pre-commit]
if: needs.detect-changes.outputs.non_code_only != 'true'
if: needs.detect-changes.outputs.non_code_only != 'true' && needs.detect-changes.outputs.examples_only != 'true'
runs-on: ${{ matrix.os }}
timeout-minutes: 60
strategy:
Expand Down Expand Up @@ -375,7 +375,7 @@ jobs:
# corresponding profiling block — no cross-contamination from cmake cache.
profiling-flags-smoke:
needs: [detect-changes, pre-commit]
if: (needs.detect-changes.outputs.a2a3_changed == 'true' || needs.detect-changes.outputs.a5_changed == 'true') && needs.detect-changes.outputs.st_affected == 'true'
if: (needs.detect-changes.outputs.a2a3_changed == 'true' || needs.detect-changes.outputs.a5_changed == 'true') && needs.detect-changes.outputs.examples_only != 'true'
runs-on: ubuntu-latest
timeout-minutes: 30

Expand Down Expand Up @@ -715,6 +715,7 @@ jobs:
non_code_only: ${{ steps.check.outputs.non_code_only }}
st_affected: ${{ steps.check.outputs.st_affected }}
ut_affected: ${{ steps.check.outputs.ut_affected }}
examples_only: ${{ steps.check.outputs.examples_only }}
steps:
- name: Checkout repository
uses: actions/checkout@v5
Expand Down Expand Up @@ -749,6 +750,7 @@ jobs:
echo "a5_changed=true" >> "$GITHUB_OUTPUT"
echo "st_affected=true" >> "$GITHUB_OUTPUT"
echo "ut_affected=true" >> "$GITHUB_OUTPUT"
echo "examples_only=false" >> "$GITHUB_OUTPUT"
exit 0
fi

Expand Down Expand Up @@ -825,6 +827,22 @@ jobs:
echo "All changes are scene-test/example-only or non-code; skipping unit tests"
fi

# `packaging-matrix` and `profiling-flags-smoke` build and install the
# product and then exercise it with a fixed, tiny payload. Nothing
# under examples/ reaches either: it is absent from both
# `wheel.packages` and the sdist `include`, and neither job reads the
# example corpus. Everything else stays in — `tests/` in particular,
# because `tools/verify_packaging.sh` runs one `tests/st/` file as an
# entry-point smoke and `tests/` is in the sdist include.
EXAMPLES_ONLY='^examples/'
NON_EXAMPLE=$(echo "$FILES" | grep -vE "$EXAMPLES_ONLY" | grep -vE "$NON_CODE" || true)
if [ -n "$NON_EXAMPLE" ]; then
echo "examples_only=false" >> "$GITHUB_OUTPUT"
else
echo "examples_only=true" >> "$GITHUB_OUTPUT"
echo "All changes are under examples/ or non-code; skipping packaging and the profiling-flag smoke"
fi

# ---------- Unit tests (a5 hardware, Python + C++) ----------
ut-a5:
needs: [detect-changes, pre-commit]
Expand Down
7 changes: 4 additions & 3 deletions docs/ci.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ The complete test-type × hardware-tier matrix. Empty cells have no tests yet; o
```text
PullRequest
├── pre-commit (ubuntu-latest)
├── packaging-matrix (ubuntu + macOS) — [skipped iff non_code_only]
├── packaging-matrix (ubuntu + macOS) — [needs !examples_only]
├── ut (ubuntu + macOS) — Python + C++ UT, no hardware [needs ut_affected]
├── detect-changes (ubuntu-latest) — outputs non_code_only, a{2a3,5}_changed, {st,ut}_affected
├── st-sim-a2a3 (ubuntu + macOS) — a2a3_changed && st_affected
Expand Down Expand Up @@ -129,9 +129,10 @@ not need `--max-parallel` manually.

- Sim scene tests and no-hardware unit tests run on github-hosted runners (no hardware).
- `detect-changes` computes three flags (`a2a3_changed`, `a5_changed`, `non_code_only`) from the PR diff, **all three derived from one `NON_CODE` set**: `docs/`, `.docs/`, `.claude/`, `mkdocs.yml`, `.github/workflows/docs.yml`, `.gitignore`, `.pre-commit-config.yaml`, and any `*.md` file anywhere. Membership follows a file's *effect*, not its path — `mkdocs.yml` and `docs.yml` are docs tooling that happens to live outside `docs/`. An arch flag is `false` only when every changed file is in the opposite platform's tree (`src/{arch}/`, `examples/{arch}/`, `tests/{st,ut/cpp}/{arch}/`) or in `NON_CODE`. Anything else — shared C++ (`src/common/`), Python (`python/`, `simpler_setup/`), build files (`CMakeLists.txt`, `pyproject.toml`), shared test infra (`tests/ut/py/`, `tests/lint/`), tooling (`tools/`), or **`.github/workflows/ci.yml` itself** — flips both flags to `true`. `ci.yml` is deliberately excluded from `NON_CODE`: a change to the gates must run everything, including whatever it just switched off.
- **Test-category axis:** `ST_ONLY='^(tests/st/|examples/)'` and `UT_ONLY='^tests/ut/'`, applied in the same shape as the arch patterns — a category is unaffected only when *every* changed file is exclusively the other's. `st_affected` gates the four scene-test jobs and `profiling-flags-smoke`; `ut_affected` gates `ut`, `ut-a2a3`, `ut-a5`. Anything belonging to neither (root `conftest.py`, `pyproject.toml`, `simpler_setup/`, `tests/lint/`) flips both, so shared infrastructure always runs both suites.
- **Test-category axis:** `ST_ONLY='^(tests/st/|examples/)'` and `UT_ONLY='^tests/ut/'`, applied in the same shape as the arch patterns — a category is unaffected only when *every* changed file is exclusively the other's. `st_affected` gates the four scene-test jobs; `ut_affected` gates `ut`, `ut-a2a3`, `ut-a5`. Anything belonging to neither (root `conftest.py`, `pyproject.toml`, `simpler_setup/`, `tests/lint/`) flips both, so shared infrastructure always runs both suites.
- **Example-corpus axis:** `EXAMPLES_ONLY='^examples/'`, same shape again. `packaging-matrix` and `profiling-flags-smoke` build and install the product and then exercise it with a fixed, tiny payload — one entry-point script and one `vector_example` — so they read neither suite as a corpus and nothing under `examples/` can reach them: it is absent from both `wheel.packages` and the sdist `include`. `tests/` deliberately stays in, because `tools/verify_packaging.sh` runs a `tests/st/` file as its entry-point smoke.
- **Gated jobs (scene tests):** `st-sim-{a2a3,a5}`, `st-onboard-{a2a3,a5}` run iff their platform's flag **and** `st_affected` are `true`.
- **Platform-independent jobs (all UT + packaging):** `ut`, `ut-a2a3`, `ut-a5`, `packaging-matrix` ignore the *platform* flags — unit tests exercise shared contracts (nanobind bindings, RuntimeBuilder, ring buffers, etc.) and the risk of silently skipping a regression outweighs the CI minutes saved. The `tests/ut/cpp/{arch}/` entry in the gating regex only *attributes* an arch-specific C++ UT change to that platform (so it does not spuriously flip the other arch's scene-test flag); it does not gate the UT jobs themselves. The three UT jobs do respect `ut_affected`, which is a statement about test category rather than silicon. `packaging-matrix` stays on `non_code_only` alone: neither `tests/` nor `examples/` ships in the wheel (`wheel.packages = ["simpler_setup", "python/simpler"]`), so it over-runs slightly on a tests-only diff — cheap, GitHub-hosted, and not worth a fourth condition.
- **Platform-independent jobs (all UT + packaging):** `ut`, `ut-a2a3`, `ut-a5`, `packaging-matrix` ignore the *platform* flags — unit tests exercise shared contracts (nanobind bindings, RuntimeBuilder, ring buffers, etc.) and the risk of silently skipping a regression outweighs the CI minutes saved. The `tests/ut/cpp/{arch}/` entry in the gating regex only *attributes* an arch-specific C++ UT change to that platform (so it does not spuriously flip the other arch's scene-test flag); it does not gate the UT jobs themselves. The three UT jobs do respect `ut_affected`, which is a statement about test category rather than silicon. `packaging-matrix` respects `examples_only`, which is a statement about which corpus a job reads: `examples/` is in neither `wheel.packages` nor the sdist `include`, and `tools/verify_packaging.sh` never opens it.
- **`non_code_only` is the same `NON_CODE` set, not a narrower one.** It is `true` when no changed file falls outside it. Nothing in the set can change what the code does, and no workflow consumes any of it beyond its own gate: `pre-commit` is ungated so it always exercises `.pre-commit-config.yaml`, `docs.yml` is unconditional on every PR so it always exercises `mkdocs.yml` / `docs/`, and **no workflow invokes anything under `.claude/`** (`grep -rn '\.claude' .github/workflows/` finds only comments). An **empty diff short-circuits the whole step**: attribution is impossible, so a single guard sets `non_code_only=false` and every arch and category flag `true`, then returns — running the full matrix. That guard is deliberately one place; testing emptiness per flag is what previously left `non_code_only` false while both arch flags also came out false, running UT and packaging but skipping every scene test.

The arch flags subtract `NON_CODE` before deciding, so a non-code-only change already makes both `false`. An arch-gated job therefore needs no separate non-code check. See [`.claude/rules/ci-change-detection.md`](../.claude/rules/ci-change-detection.md) for the invariants these gates must keep.
Expand Down
Loading