The problem
You change one file in your monorepo. CI runs all 200 tests. 35 minutes later, you get a green checkmark for tests that had nothing to do with your change.
Every team I've seen deals with this in one of three ways:
- Ignore it — waste CI minutes and developer time
-
Hack together bash scripts —
git diff --name-only | greppiped into whatever test runner you use - Adopt Nx/Bazel/Turborepo — great tools, but they require buying into an entire build framework
I wanted option 4: a standalone CLI that just works.
What I built
affected is a Rust CLI that detects which packages in your monorepo are affected by git changes and runs only their tests.
$ affected list --base main --explain
3 affected package(s) (base: main, 2 files changed):
● core (directly changed: src/lib.rs)
● api (depends on: core)
● cli (depends on: api → core)
How it works
- Detect — scans for marker files (Cargo.toml, package.json, go.mod, pom.xml, etc.)
- Resolve — builds a dependency graph from project manifests
- Diff — computes changed files using libgit2
- Map — maps each changed file to its owning package
- Traverse — runs reverse BFS on the dependency graph to find all transitively affected packages
- Execute — runs test commands for affected packages only
What it supports
7 ecosystems out of the box, zero config:
| Ecosystem | Detection |
|---|---|
| Cargo |
Cargo.toml workspace |
| npm/pnpm |
package.json workspaces |
| Yarn Berry | .yarnrc.yml |
| Go |
go.work / go.mod
|
| Python |
pyproject.toml (Poetry, uv, generic) |
| Maven |
pom.xml with <modules>
|
| Gradle | settings.gradle(.kts) |
CI integration
This was designed for CI from day one:
# GitHub Actions
- name: Detect affected
id: affected
run: affected ci --merge-base main
- name: Run tests
if: steps.affected.outputs.has_affected == 'true'
run: affected test --merge-base main --jobs 4 --junit results.xml
It also supports:
-
--jsonfor structured output -
--junit results.xmlfor JUnit XML (Jenkins, GitLab, etc.) -
--filter "lib-*"/--skip "e2e-*"for targeting specific packages -
--explainto show the dependency chain for each affected package -
--jobs 4for parallel test execution
The --explain flag
This is my favorite feature. Instead of just listing affected packages, it tells you why:
$ affected list --base main --explain
● core (directly changed: src/lib.rs, src/utils.rs)
● api (depends on: core)
● cli (depends on: api → core)
● docs-gen (depends on: api → core)
Now you know exactly which change caused which packages to be retested.
Numbers
- ~5,000 lines of Rust
- 160+ tests (unit + integration + CLI)
- CI passes on Linux, macOS, and Windows
- MIT licensed
Try it
cargo install affected-cli
Then in any monorepo:
affected detect # see what it found
affected list --base main # see what's affected
affected test --base main # run only affected tests
GitHub: github.com/Rani367/affected
I'd love to hear what edge cases you hit, what ecosystems you'd want added, or if this actually saves you CI time. Star the repo if it's useful, it helps others find it.
Top comments (0)