diff --git a/.claude/rules/ci-change-detection.md b/.claude/rules/ci-change-detection.md index a5ca926c4a..7e62cabc30 100644 --- a/.claude/rules/ci-change-detection.md +++ b/.claude/rules/ci-change-detection.md @@ -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 @@ -71,9 +72,18 @@ A job composes the axes it is actually subject to: | Job family | Gate | | ---------- | ---- | | `st-sim-*`, `st-onboard-*` | `_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. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eac5897643..7390c125f0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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: @@ -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 @@ -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 @@ -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 @@ -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] diff --git a/docs/ci.md b/docs/ci.md index a565d7f87d..c59ce836ff 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -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 @@ -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.