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
44 changes: 32 additions & 12 deletions .claude/rules/ci-change-detection.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,14 +53,14 @@ never sit behind a *more permissive* condition than a GitHub-hosted one. If you
find yourself writing a looser `if:` for the more expensive job, the vocabulary
is wrong (see §1) — fix that instead of widening the gate.

## 4. Three axes, layered — and the non-code one is subtracted first
## 4. Four axes, layered — and the non-code one is subtracted first

| Axis | Output | Answers |
| ---- | ------ | ------- |
| 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? |
| corpus | `examples_only` / `tests_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 @@ -72,24 +72,36 @@ 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) && !examples_only` |
| `profiling-flags-smoke` | `(a2a3_changed \|\| a5_changed) && !examples_only && !tests_only` |
| `ut`, `ut-a2a3`, `ut-a5` | `non_code_only != true && ut_affected` |
| `packaging-matrix` | `non_code_only != true && !examples_only` |
| `packaging-matrix` | `non_code_only != true && !examples_only && !tests_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.
then exercise it with a **fixed, tiny payload** — packaging's entry-point smoke
is one `tests/st/` file, profiling's is one `vector_example`. They are the only
jobs that consume neither test suite as a corpus, which is why they take the
corpus axis rather than the category one — and the corpus axis comes in the
same shape on both sides, so a diff confined to `examples/` or to `tests/`
cannot reach either job. That is not a gap: `wheel.packages` is
`["simpler_setup", "python/simpler"]`, so both partitions are provably absent
from the product, and a payload file changed under either is still exercised by
the scene-test job that reads the same corpus. A change to a payload file was
the historical reason `tests/` stayed in — a now-deleted claim that `tests/`
was "in the sdist include", which no configuration ever made true.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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.
That is a decision about the *arch* axis only; the category axis is a different
question, because a scene-test-only change genuinely cannot break a unit test.

The vocabulary exists twice. `ci-self-cpu.yml` runs its own lane-local
`detect-changes` with the same outputs, and its file header says the two must
be kept in sync. That has now failed twice — `examples_only` never reached the
lane (#1607), and `tests_only` did not either (#1635) — because the gate tables
here only name `ci.yml`. Any change to an axis therefore means two files:
`.github/workflows/ci.yml` and `.github/workflows/ci-self-cpu.yml`, same commit,
and the outputs-consistency grep in §7 runs against both.

### Write every axis in the same shape

The arch and category axes use one pattern: **a partition is unaffected only
Expand Down Expand Up @@ -165,7 +177,15 @@ bug shows up as a green check, so "CI passed" is not evidence.** Where the
change is to `NON_CODE` itself, replay the pattern locally against
representative file lists — a docs-only set, a `.gitignore`-only set, a
single-arch set, a `ci.yml` set — and check all four land where you intended
before pushing.
before pushing. Where the change is to an *axis*, also verify both workflow
copies expose the same output set — the lane drifted twice because only
`ci.yml` was checked:

```bash
for f in ci.yml ci-self-cpu.yml; do
echo "== $f"; grep -oE 'outputs\.[a-z_]+' ".github/workflows/$f" | sort -u
done # the two lists must be identical
```

## Relation to the other rules

Expand Down
27 changes: 25 additions & 2 deletions .github/workflows/ci-self-cpu.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ 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 }}
tests_only: ${{ steps.check.outputs.tests_only }}
steps:
- name: Checkout target PR head
uses: actions/checkout@v5
Expand All @@ -89,6 +91,8 @@ 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"
echo "tests_only=false" >> "$GITHUB_OUTPUT"
exit 0
fi

Expand Down Expand Up @@ -134,6 +138,25 @@ jobs:
echo "ut_affected=false" >> "$GITHUB_OUTPUT"
fi

# Corpus axis, mirroring ci.yml: a diff confined to examples/ or to
# tests/ cannot reach the product jobs, whose payload files live in
# those partitions but are exercised by the scene-test job instead.
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"
fi

TESTS_ONLY='^(tests/)'
NON_TEST=$(echo "$FILES" | grep -vE "$TESTS_ONLY" | grep -vE "$NON_CODE" || true)
if [ -n "$NON_TEST" ]; then
echo "tests_only=false" >> "$GITHUB_OUTPUT"
else
echo "tests_only=true" >> "$GITHUB_OUTPUT"
fi

# ---------- T1: no-hardware Linux, on the cpu runner ----------
pre-commit:
runs-on: [self-hosted, cpu]
Expand Down Expand Up @@ -228,7 +251,7 @@ jobs:

packaging:
needs: [detect-changes]
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' && needs.detect-changes.outputs.tests_only != 'true'
runs-on: [self-hosted, cpu]
timeout-minutes: 60
steps:
Expand Down Expand Up @@ -261,7 +284,7 @@ jobs:

profiling-flags-smoke:
needs: [detect-changes]
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' && needs.detect-changes.outputs.tests_only != 'true'
runs-on: [self-hosted, cpu]
timeout-minutes: 30
steps:
Expand Down
30 changes: 22 additions & 8 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' && needs.detect-changes.outputs.examples_only != 'true'
if: needs.detect-changes.outputs.non_code_only != 'true' && needs.detect-changes.outputs.examples_only != 'true' && needs.detect-changes.outputs.tests_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.examples_only != 'true'
if: (needs.detect-changes.outputs.a2a3_changed == 'true' || needs.detect-changes.outputs.a5_changed == 'true') && needs.detect-changes.outputs.examples_only != 'true' && needs.detect-changes.outputs.tests_only != 'true'
runs-on: ubuntu-latest
timeout-minutes: 30

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

Expand Down Expand Up @@ -837,12 +839,15 @@ jobs:
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.
# product and then exercise it with a fixed, tiny payload —
# profiling-flags-smoke's lives under examples/ (vector_example),
# packaging-matrix's under tests/ (one tests/st/ file is the
# entry-point smoke of tools/verify_packaging.sh). Neither job reads
# either corpus: `wheel.packages` is ["simpler_setup",
# "python/simpler"], so both partitions are provably absent from the
# product, and a payload file changed under either is exercised by
# the scene-test job that also reads it. Each partition therefore
# gets the same axis, written in the same shape.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
EXAMPLES_ONLY='^examples/'
NON_EXAMPLE=$(echo "$FILES" | grep -vE "$EXAMPLES_ONLY" | grep -vE "$NON_CODE" || true)
if [ -n "$NON_EXAMPLE" ]; then
Expand All @@ -852,6 +857,15 @@ jobs:
echo "All changes are under examples/ or non-code; skipping packaging and the profiling-flag smoke"
fi

TESTS_ONLY='^(tests/)'
NON_TEST=$(echo "$FILES" | grep -vE "$TESTS_ONLY" | grep -vE "$NON_CODE" || true)
if [ -n "$NON_TEST" ]; then
echo "tests_only=false" >> "$GITHUB_OUTPUT"
else
echo "tests_only=true" >> "$GITHUB_OUTPUT"
echo "All changes are under tests/ 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
Loading
Loading