DEV Community

Rani
Rani

Posted on

I built a CLI that stops your CI from running tests it doesn't need to

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:

  1. Ignore it — waste CI minutes and developer time
  2. Hack together bash scriptsgit diff --name-only | grep piped into whatever test runner you use
  3. 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)
Enter fullscreen mode Exit fullscreen mode

How it works

  1. Detect — scans for marker files (Cargo.toml, package.json, go.mod, pom.xml, etc.)
  2. Resolve — builds a dependency graph from project manifests
  3. Diff — computes changed files using libgit2
  4. Map — maps each changed file to its owning package
  5. Traverse — runs reverse BFS on the dependency graph to find all transitively affected packages
  6. 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
Enter fullscreen mode Exit fullscreen mode

It also supports:

  • --json for structured output
  • --junit results.xml for JUnit XML (Jenkins, GitLab, etc.)
  • --filter "lib-*" / --skip "e2e-*" for targeting specific packages
  • --explain to show the dependency chain for each affected package
  • --jobs 4 for 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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)