ci: skip the opposite surface's single-surface tests - #1757
Conversation
GPT 5.6 Review — ✅ no blocking findingsGPT 5.6 completed its review of This comment is updated in place on each push. Review detailsNo findings. False positive or not applicable? A repository writer can comment: |
Design Review (Fable 5) — ✅ PASSAdvisory design-level review of Design-Verdict: PASS Deny-by-default selection with fail-closed fallbacks inverts the unsafe allowlist approach correctly; residual risks are acknowledged, tested, and cost only CI time. Suggestions
[DESIGN-REVIEWED] f8bd1a5 |
Opus 5 Review — ✅ no blocking findingsReviewed Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
73e95c8 to
427af11
Compare
|
Prior reviewed SHA
|
427af11 to
caee5b9
Compare
|
Prior reviewed SHA
|
|
Rebased onto current |
caee5b9 to
0c71fd8
Compare
0c71fd8 to
42ac0b3
Compare
|
Reworked to the A′ shape (prior SHA
|
5d436dd to
8bf09b3
Compare
|
Synced onto current |
Every PR runs the full matrix regardless of what it touches: a
frontend-only diff runs all 8 backend shards plus 4 Windows shards, and
a backend-only diff runs the whole vitest suite. Most of those tests
cannot be affected by the diff at all.
Naively gating a whole suite on changed paths is unsafe here, because a
minority of tests are cross-surface parity guards: they live in one
suite but assert against the OTHER surface's source (backend
test_redaction_mirror_parity.py reads website/src/utils/sanitize.ts;
website/electron/test/external-scheme.test.js reads
src/kiro_crew/computer_use/permissions.py). Skipping one of those is how
a drift bug ships green, and an allowlist of "paths that make a suite
skippable" has to be complete to be safe -- it never is.
So invert it. scripts/ci-surface-tests.py enumerates only the files it
can positively PROVE are single-surface, and everything else -- every
parity guard, every file it cannot classify, every file added tomorrow --
stays in the must-run set. A heuristic miss therefore costs CI time, not
a skipped guard. It fails closed on an unreadable file, and every failure
path in the workflow falls back to running the FULL suite.
A new `changes` job computes only_backend / only_frontend once. `backend`
is a CATCH-ALL filter ("every changed file that is not frontend and not
meta"), so every changed file lands in exactly one bucket and "only_X"
literally means only X changed -- a diff touching two buckets, or meta
paths, leaves both flags false and the full matrix runs. On a
single-surface diff the opposite suite narrows to its must-run set:
frontend-only diff -> backend-test + backend-test-windows run ~225 of
~1042 files (about a fifth)
backend-only diff -> frontend-test runs ~84 of ~772 spec files
(Exact counts drift as tests are added; the ratio is the point.)
The catch-all needs dorny/paths-filter >= v4.0.3 for the
'some-with-excludes' quantifier (v4.0.2 rejects the input and throws), so
the pin moves from v4.0.2 to v4.0.3 ceb8a2b8.
The selector walks every root in setup.cfg `testpaths` (test, transfer,
src/kiro_crew/apps/builtins) and every root in vitest's `test.include`
(website/src, website/integration). Those roots matter more than the
classifier: an unenumerated root is worse than an unclassified file,
because the reduced run passes explicit paths, so a root that is never
walked never runs at all instead of falling back to "keep running".
test_ci_surface_tests.py parses setup.cfg and website/vite.config.ts and
asserts every configured root is scanned, so adding a testpath or a
vitest include without adding it to the selector fails loudly.
A narrowed run collects no coverage, because a subset's coverage is not
comparable to the repo floor. Coverage Combine is therefore skipped on a
frontend-only diff, and Coverage Gate keeps its fail-closed contract as an
explicit invariant: backend-test AND frontend-test must always succeed
(only their scope narrows -- backend-test is asserted directly so a failed
reduced run cannot hide behind a by-design skipped Combine), backend
coverage is required unless the run was frontend-only (where Combine must
be exactly `skipped`), and empty surface flags -- the detection job never
reported -- fail closed.
electron-test, frontend-lint, backend-lint and the sandbox job stay
always-on: they are whole-project gates or hold cross-surface guards, and
subsetting them buys little.
test/test_ci_surface_tests.py pins the deny-by-default contract: 18 known
parity guards can never be classified single-surface (asserting they still
exist, so a rename updates the list instead of turning into a green skip),
every configured testpath AND every vitest include root is scanned, both
frontend escape styles ('../../..' literals AND path.resolve segment
lists) are detected, and an unreadable file fails closed.
Closes #1556
8bf09b3 to
f8bd1a5
Compare
|
BLOCKING — ci.yml reduced runs bypass the test-suite exclusion — FIXED (prev reviewed Confirmed reachable exactly as described: Root cause is drift: the deselect list was duplicated across the full backend + full Windows commands, and the two reduced commands silently diverged. Rather than add a 3rd/4th copy that can drift again, I hoisted the list to one workflow-level Safety verified locally: (1) a |
Every PR runs the full matrix regardless of what it touches: a
frontend-only diff runs all 8 backend shards plus 4 Windows shards, and
a backend-only diff runs the whole vitest suite. Most of those tests
cannot be affected by the diff at all.
Naively gating a whole suite on changed paths is unsafe here, because a
minority of tests are cross-surface parity guards: they live in one
suite but assert against the OTHER surface's source (backend
test_redaction_mirror_parity.py reads website/src/utils/sanitize.ts;
website/electron/test/external-scheme.test.js reads
src/kiro_crew/computer_use/permissions.py). Skipping one of those is how
a drift bug ships green, and an allowlist of "paths that make a suite
skippable" has to be complete to be safe -- it never is.
So invert it. scripts/ci-surface-tests.py enumerates only the files it
can positively PROVE are single-surface, and everything else -- every
parity guard, every file it cannot classify, every file added tomorrow --
stays in the must-run set. A heuristic miss therefore costs CI time, not
a skipped guard. It fails closed on an unreadable file, and every failure
path in the workflow falls back to running the FULL suite.
A new `changes` job computes only_backend / only_frontend once. `backend`
is a CATCH-ALL filter ("every changed file that is not frontend and not
meta"), so every changed file lands in exactly one bucket and "only_X"
literally means only X changed -- a diff touching two buckets, or meta
paths, leaves both flags false and the full matrix runs. On a
single-surface diff the opposite suite narrows to its must-run set:
frontend-only diff -> backend-test + backend-test-windows run ~225 of
~1042 files (about a fifth)
backend-only diff -> frontend-test runs ~84 of ~772 spec files
(Exact counts drift as tests are added; the ratio is the point.)
The catch-all needs dorny/paths-filter >= v4.0.3 for the
'some-with-excludes' quantifier (v4.0.2 rejects the input and throws), so
the pin moves from v4.0.2 to v4.0.3 ceb8a2b8.
The selector walks every root in setup.cfg `testpaths` (test, transfer,
src/kiro_crew/apps/builtins) and every root in vitest's `test.include`
(website/src, website/integration). Those roots matter more than the
classifier: an unenumerated root is worse than an unclassified file,
because the reduced run passes explicit paths, so a root that is never
walked never runs at all instead of falling back to "keep running".
test_ci_surface_tests.py parses setup.cfg and website/vite.config.ts and
asserts every configured root is scanned, so adding a testpath or a
vitest include without adding it to the selector fails loudly.
A narrowed run collects no coverage, because a subset's coverage is not
comparable to the repo floor. Coverage Combine is therefore skipped on a
frontend-only diff, and Coverage Gate keeps its fail-closed contract as an
explicit invariant: backend-test AND frontend-test must always succeed
(only their scope narrows -- backend-test is asserted directly so a failed
reduced run cannot hide behind a by-design skipped Combine), backend
coverage is required unless the run was frontend-only (where Combine must
be exactly `skipped`), and empty surface flags -- the detection job never
reported -- fail closed.
electron-test, frontend-lint, backend-lint and the sandbox job stay
always-on: they are whole-project gates or hold cross-surface guards, and
subsetting them buys little.
test/test_ci_surface_tests.py pins the deny-by-default contract: 18 known
parity guards can never be classified single-surface (asserting they still
exist, so a rename updates the list instead of turning into a green skip),
every configured testpath AND every vitest include root is scanned, both
frontend escape styles ('../../..' literals AND path.resolve segment
lists) are detected, and an unreadable file fails closed.
Closes kirodotdev#1556
Co-authored-by: Rohan Khanderia <51836404+RohanK6@users.noreply.github.com>
Problem
Every PR runs the full CI matrix regardless of what it touches. A frontend-only diff runs all 8 backend shards plus 4 Windows shards; a backend-only diff runs the whole vitest suite. Most of those tests cannot be affected by the diff at all (#1556).
Why it matters
Wasted compute and slower feedback on every PR, plus noise that buries the checks that actually matter, and needless fork-approval toil.
Fix (symptoms → root cause → change)
Symptom: irrelevant tests run on single-surface PRs. Root cause: jobs have no notion of which surface a diff touches.
The obvious fix — gate a whole suite on changed paths — is unsafe in this repo, and that is the crux. A minority of tests are cross-surface parity guards: they live in one suite but assert against the other surface's source. Examples: backend
test_redaction_mirror_parity.pyreadswebsite/src/utils/sanitize.ts(credential-redaction parity);website/electron/test/external-scheme.test.jsreadssrc/kiro_crew/computer_use/permissions.py. Skipping one of those is exactly how a drift bug ships green, and an allowlist of "paths that make a suite skippable" has to be complete to be safe — it never is.So the logic is inverted.
scripts/ci-surface-tests.pyenumerates only the files it can positively prove are single-surface; everything else — every parity guard, every file it cannot classify, every file added tomorrow — stays in the must-run set. A heuristic miss therefore costs CI time, never a skipped guard. It fails closed on an unreadable file, and every failure path in the workflow falls back to running the full suite.A new
changesjob computesonly_backend/only_frontendonce.backendis a catch-all filter (['**', '!website/**', '!.github/**', '!scripts/**']), so every changed file lands in exactly one bucket and "only_X" literally means only X changed — a diff touching two buckets, or meta paths, leaves both flags false and the full matrix runs. On a single-surface diff the opposite suite narrows:backend-test+backend-test-windowsrun 223 of 1033 filesfrontend-testruns 81 of 758 spec filesThe catch-all requires
dorny/paths-filter >= v4.0.3for thesome-with-excludesquantifier (v4.0.2 rejects the input and throws), so the pin moves v4.0.2 → v4.0.3ceb8a2b8.The scanned roots matter more than the classifier. An unenumerated root is worse than an unclassified file: the reduced run passes explicit paths, so a root that is never walked never runs at all, instead of falling back to "keep running". The selector therefore walks every root in
setup.cfgtestpaths(test,transfer,src/kiro_crew/apps/builtins) and every root in vitest'stest.include(website/src,website/integration) — and the test suite asserts that, so adding a testpath or vitest include without adding it here fails loudly.electron-test,frontend-lint,backend-lintand the sandbox job stay always-on: they are whole-project gates or hold cross-surface guards, and subsetting them buys little.Coverage Gate keeps its fail-closed contract as an explicit invariant. A narrowed run collects no coverage (a subset's coverage is not comparable to the repo floor), so Coverage Combine is skipped on a frontend-only diff, and the gate asserts:
backend-testandfrontend-testmust always succeed (only their scope narrows —backend-testis checked directly so a failed reduced run cannot hide behind a by-design skipped Combine); backend coverage is required unless the run was frontend-only (where Combine must be exactlyskipped); empty surface flags — the detection job never reported — fail closed.Tests
test/test_ci_surface_tests.py(new, 25 cases) pins the deny-by-default contract:setup.cfgtestpath and every vitest include root is a scanned root (the two checks that catch the "root never walked" class).'../../..'string literals andpath.resolve(__dirname, '..', '..', '..')segment lists.Also verified locally: flake8 / isort / mypy clean, YAML valid, and the reduced 223-file selection collects cleanly under pytest.
Manual verification
N/A — CI workflow change; correctness is exercised by CI on this PR. This PR touches
.github/**→meta→ it runs the full matrix on itself, which is the conservative branch. The narrowing paths are covered by the unit tests above.Known limitations
ci-full-runlabel forces the full matrix but takes effect on the next push — this workflow does not subscribe to thelabeledevent, because that would re-run everything on every bot label. Documented in-code at theFULL_RUNstep.Attribution
Supersedes #1560. Original authorship by @RohanK6 is preserved on the commit; the implementation was reworked from the original path-gating approach to the deny-by-default selector after path-allowlisting proved unsafe in review (fixes folded in as committer).
Closes #1556