Builds a CI workflow that runs only the subset of tests impacted by a PR's changes - combines a per-test → source-file dependency map (built from coverage profiles or, in build-graph projects, queried from the build system itself like Bazel `rdeps`) with the PR's `git diff --name-only`, then selects the union of (impacted by changed files + previously failing + newly added). Always pairs with a periodic full-suite run so a misconfigured map can't silently shrink coverage. Use when the regression suite is large enough that PR-time CI is the bottleneck and a full run is reserved for nightly / pre-release.
79
99%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
Test Impact Analysis (TIA) runs only the tests a change can affect: map production sources to the tests that exercise them, then intersect that map with the PR diff (tia-fowler). The map is bidirectional - one test exercises a subset of sources; one source is exercised by a subset of tests. Azure Pipelines builds it from per-test dynamic dependencies; Bazel derives it statically from the build graph (tia-azure).
This skill builds a TIA-style selector for any team - stitching coverage data, git diff, and a safe fallback policy - without vendor tooling.
lcov-analysis,
jest-coverage-analysis, etc., in the qa-test-reporting
plugin) - the per-test → source map is computable from it.If the build is a Bazel / Pants / Buck monorepo, the selection already comes from the build graph (Step 5) and this skill is mostly orchestration around it.
A robust selector runs impacted + previously-failing + newly-added tests, and falls back to the full suite for any change it can't map (tia-azure).
The selection set per PR:
impacted ∪ previously_failing ∪ newly_added ∪ (FALLBACK if any change is unmappable)Hard-coded fallback triggers (run everything):
pom.xml, package.json, Cargo.toml,
requirements.txt, Dockerfile).Azure's TIA reasons only about managed code on a single machine; it can't reason about HTML / CSS changes and falls back to running all tests (tia-azure).
Emit per-test coverage (not merged), then invert it into a file → tests map. The core inversion:
# scripts/build_test_map.py
def build_map(per_test_coverage):
"""returns {file_path: [test_id, ...]}"""
inverted = defaultdict(list)
for test_id, coverage in per_test_coverage.items():
for file_path, hits in coverage.items():
if any(h > 0 for h in hits):
inverted[file_path].append(test_id)
return dict(inverted)Persist as test-map.json, checked into the repo or stored as a CI artifact
updated on every main run. Per-framework recipes for emitting per-test
coverage (Jest, pytest + coverage.py, JaCoCo) and the Bazel / Pants / Buck
build-graph path (where rdeps gives the map directly) live in
references/instrumentation-and-ci.md.
git diff --name-only origin/${{ github.base_ref }}...HEADImportant: ... (three dots), not ... Three-dot diff is "what
changed on this branch since it diverged from main", which matches
PR semantics. Two-dot diff is "differences vs current main HEAD"
which can show changes the PR didn't make if main moved forward.
def select_tests(map, changed_files, previously_failing, newly_added):
impacted = set()
for f in changed_files:
if f in map:
impacted.update(map[f])
else:
return ('FALLBACK', f) # unknown file type
return ('SELECTED', impacted | previously_failing | newly_added)previously_failing comes from the most recent full-suite run on
main (CI artifact). newly_added comes from
git diff --diff-filter=A --name-only filtered to test files.
Pair selection with a full-suite run so a stale map can't silently shrink coverage. Azure's pattern: run selected tests (T1) and all tests (T2) in sequence and confirm T2 reports the same result as T1 (tia-azure).
CI workflow (selected run + shadow full run): references/instrumentation-and-ci.md.
PR-comment summary so reviewers know what ran:
## Test Impact Analysis - `<sha>`
**Selected:** 47 tests of 1,283 total (3.7%)
**Strategy:** impacted ∪ previously_failing ∪ newly_added
**Reason for selection:**
| Source | Tests added |
|---------------------|------------:|
| Impacted by changes | 39 |
| Previously failing | 5 |
| Newly added | 3 |
**Files driving impacted set:**
- `src/checkout/cart.ts` → 12 tests
- `src/checkout/promo.ts` → 18 tests
- `src/api/orders.ts` → 9 tests
**Last full-suite run:** 2026-05-04 22:00 UTC (12 hours ago) - passed.The team should be able to opt out for a specific build, matching Azure's
DisableTestImpactAnalysis build-variable escape hatch (tia-azure):
run-all-tests → forces full suite for that PR.tia-include → only consider TIA for changes
matching this pattern (matches Azure's TIA_IncludePathFilters
per tia-azure).[full-suite] → forces full suite.| Anti-pattern | Why it fails | Fix |
|---|---|---|
| Selection without periodic full-suite safety net | Map staleness causes missed coverage; bugs ship. | Pattern A or B (Step 5). |
git diff origin/main..HEAD (two dots) | Picks up commits that landed on main after the PR diverged; selection is wrong. | Use three dots (Step 3). |
| Treating an empty map result as "no impacted tests" → run nothing | A new file type (e.g. *.proto) isn't in the map → selector returns nothing → bugs ship. | Fallback to full suite (Step 1). |
Skipping previously_failing from the union | Flaky / known-broken tests don't run; the broken state is invisible. | Always include the previously-failing set (Step 4). |
| Map updated only on full-suite runs that succeed | A failing full-suite run doesn't update the map → next PR uses stale data. | Update the map on every full run regardless of pass/fail (the data is still valid). |
| One global map for a multi-language repo | Per-language coverage tools emit different test IDs; the map merges incorrectly. | Per-language maps + per-language selectors; combine selections, not maps. |
| Selecting only "impacted" without "newly_added" | A new test file with no map entry never runs in PR. | Detect new test files via git diff --diff-filter=A (Step 4). |
| Hard-coded 5-PR full-run cadence with no opt-out | A user with 50-PR streak runs full suite 10× even if they're trivial. | Optional [full-suite] PR title override (Step 7). |
test-coverage-targeter
for the "what to add next" side.rdeps reverse
dependency query, declared-vs-actual dependency principle.coverage-debt-tracker -
sibling skill: tracks files that lost coverage / went stale.