Detect affected packages. Run only what matters.
A standalone, language-agnostic CLI that detects which packages in your monorepo are affected by git changes, then runs tests, lints, builds, or any command on only those packages. No framework, no config files, no lock-in.
$ 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)
$ affected run "cargo clippy -p {package}" --base main --jobs 4 --dry-run
Running command for 3 affected package(s) (out of 8 total, 2 files changed):
[dry-run] core: cargo clippy -p core
[dry-run] api: cargo clippy -p api
[dry-run] cli: cargo clippy -p cli
Every monorepo team hacks together bash scripts with git diff | grep to avoid running all tests on every PR. Tools like Nx, Turborepo, and Bazel solve this but require buying into an entire build system.
affected is a single binary you install and run. It auto-detects your project type, builds a dependency graph, and figures out what's affected. Zero config to start.
- Zero config -- auto-detects your project type and dependency graph
- 13 ecosystems -- Cargo, npm, pnpm, Yarn, Bun, Go, Python, Maven, Gradle, .NET, Swift, Dart/Flutter, Elixir, Scala/sbt
- Transitive detection -- if
corechanges andapidepends oncore, both are affected affected run-- run any command on affected packages, not just tests--explain-- shows why each package is affected with the full dependency chain- Multi-CI --
affected ci --format github|gitlab|circleci|azurewith dynamic job matrices - Watch mode --
affected watch test --base mainre-runs on file changes affected init-- interactive setup wizard generates.affected.toml- Dependency tree --
affected graphrenders a Unicode tree with affected highlighting - Parallel execution --
--jobs 4runs commands across multiple threads - CI-first --
--json,--junit, PR comment bot, shell completions - Fast -- written in Rust, uses libgit2 for native git operations
# Homebrew (macOS/Linux)
brew install Rani367/tap/affected
# uv
uv tool install affected
# pipx
pipx install affected
# pip
pip install affected
# Cargo
cargo install affected-cli
# GitHub Actions
- uses: Rani367/setup-affected@v1
# Or download a binary from Releases# What's affected?
affected list --base main
# See why each package is affected
affected list --base main --explain
# Run only affected tests
affected test --base main
# Run any command on affected packages
affected run "cargo clippy -p {package}" --base main
# Parallel execution
affected test --base main --jobs 4Run tests for affected packages.
affected test --base main # run affected tests
affected test --base HEAD~3 # compare vs 3 commits ago
affected test --merge-base main # auto-detect merge-base (best for PRs)
affected test --base main --jobs 4 # parallel execution
affected test --base main --timeout 300 # 5 min timeout per package
affected test --base main --dry-run # show what would run
affected test --base main --json # structured JSON output
affected test --base main --junit results.xml # JUnit XML for CI
affected test --base main --filter "lib-*" # only test matching packages
affected test --base main --skip "e2e-*" # skip matching packages
affected test --base main --explain # show why each is affectedRun any command on affected packages. Use {package} as a placeholder.
affected run "cargo clippy -p {package}" --base main # lint affected
affected run "cargo build -p {package}" --base main --jobs 4 # build affected
affected run "npm run lint --workspace={package}" --base main # npm lint
affected run "go vet ./{package}/..." --base main # go vet
affected run "echo {package}" --base main --dry-run # previewList affected packages without running anything.
affected list --base main # list affected packages
affected list --base main --json # JSON output for CI
affected list --base main --explain # show dependency chainsDisplay the project dependency graph as a Unicode tree.
affected graph # Unicode dependency tree
affected graph --base main # highlight affected packages
affected graph --dot # DOT format for Graphviz
affected graph --dot | dot -Tpng -o graph.png # render as imageExample output:
Dependency Graph (5 packages, 3 affected):
cli ●
└── api ●
└── core ●
utils
standalone (no dependencies)
Output variables for CI systems with multi-platform support.
affected ci --base main # GitHub Actions (default)
affected ci --base main --format gitlab # GitLab CI (writes ci.env)
affected ci --base main --format azure # Azure Pipelines (##vso)
affected ci --base main --format circleci # CircleCI ($BASH_ENV)
affected ci --base main --format generic # plain key=valueInteractive setup wizard to generate .affected.toml.
affected init # interactive prompts
affected init --non-interactive # auto-detect and use defaultsWatch for file changes and re-run commands automatically.
affected watch test --base main # re-run tests on changes
affected watch list --base main # re-list affected on changes
affected watch run "cargo clippy -p {package}" --base main # re-run command
affected watch test --base main --debounce 1000 # 1s debounce- uses: Rani367/setup-affected@v1
- run: affected test --merge-base origin/mainEach affected package runs as a separate parallel job in the GitHub Actions UI:
jobs:
detect:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.affected.outputs.matrix }}
has_affected: ${{ steps.affected.outputs.has_affected }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: Rani367/setup-affected@v1
- id: affected
run: affected ci --merge-base origin/main
test:
needs: detect
if: needs.detect.outputs.has_affected == 'true'
runs-on: ubuntu-latest
strategy:
matrix: ${{ fromJson(needs.detect.outputs.matrix) }}
fail-fast: false
steps:
- uses: actions/checkout@v4
- name: Test ${{ matrix.package }}
run: cargo test -p ${{ matrix.package }}Auto-comment on PRs showing which packages are affected and why:
on:
pull_request:
branches: [main]
permissions:
pull-requests: write
jobs:
comment:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: Rani367/affected-pr-comment@v1This posts a comment like:
Affected Packages (3 of 8)
Package Reason core directly changed: src/lib.rsapi depends on: core cli depends on: api -> core
The comment updates automatically on each push (no duplicates).
Generate shell completions.
affected completions bash >> ~/.bashrc
affected completions zsh >> ~/.zshrc
affected completions fish > ~/.config/fish/completions/affected.fish| Ecosystem | Detected By | Dependency Source |
|---|---|---|
| Cargo | Cargo.toml with [workspace] |
cargo metadata JSON |
| npm | package.json with workspaces |
package.json dependencies |
| pnpm | pnpm-workspace.yaml |
package.json dependencies |
| Yarn Berry | .yarnrc.yml |
package.json dependencies |
| Bun | bun.lock / bunfig.toml |
package.json dependencies |
| Go | go.work / go.mod |
go mod graph |
| Python | pyproject.toml |
PEP 621 deps + import scanning |
| Poetry | [tool.poetry] in pyproject.toml |
Poetry path dependencies |
| uv | [tool.uv.workspace] in pyproject.toml |
Workspace member globs |
| Maven | pom.xml with <modules> |
POM dependency declarations |
| Gradle | settings.gradle(.kts) |
project(':...') references |
| .NET/C# | *.sln solution file |
<ProjectReference> in .csproj |
| Swift/SPM | Package.swift (multi-target) |
Target dependency declarations |
| Dart/Flutter | pubspec.yaml workspace / melos.yaml |
dependencies in pubspec.yaml |
| Elixir | mix.exs + apps/ (umbrella) |
in_umbrella: true deps |
| Scala/sbt | build.sbt |
.dependsOn() project refs |
Create .affected.toml in your project root (optional):
# Ignore files that should never trigger tests
ignore = ["*.md", "docs/**", ".github/**"]
# Custom test commands per ecosystem
[test]
cargo = "cargo nextest run -p {package}"
npm = "pnpm test --filter {package}"
go = "go test -v ./{package}/..."
python = "uv run --package {package} pytest"
maven = "mvn test -pl {package}"
gradle = "gradle :{package}:test"
bun = "bun test --filter {package}"
dotnet = "dotnet test {package}"
dart = "dart test -C {package}"
swift = "swift test --filter {package}"
elixir = "mix cmd --app {package} mix test"
sbt = "sbt {package}/test"
# Per-package overrides
[packages.slow-e2e]
test = "cargo test -p slow-e2e -- --ignored"
timeout = 600
[packages.legacy-service]
skip = true- Detect -- scans for marker files to identify the ecosystem
- Resolve -- builds a dependency graph from project manifests
- Diff -- computes changed files using libgit2 (base ref vs HEAD + working tree)
- 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 commands for affected packages only
| Feature | affected |
Nx | Turborepo | Bazel |
|---|---|---|---|---|
| Zero config | Yes | No | No | No |
| Standalone binary | Yes | No (Node.js) | No (Node.js) | No (JVM) |
| Language agnostic | 13 ecosystems | JS/TS + plugins | JS/TS | Any (with rules) |
| Setup time | 1 minute | Hours | Hours | Days-weeks |
affected run <cmd> |
Yes | No | No | No |
--explain |
Yes | No | No | No |
| Watch mode | Yes | Yes | No | No |
| Multi-CI support | 5 platforms | GitHub only | GitHub only | Custom |
| Dynamic CI matrix | Yes | Plugin | No | No |
| PR comment bot | Yes | No | No | No |
| Interactive setup | affected init |
nx init |
turbo init |
Manual |
| Binary size | ~5MB | ~200MB+ | ~100MB+ | ~500MB+ |
-v, --verbose Increase verbosity (-v for debug, -vv for trace)
-q, --quiet Suppress non-essential output
--no-color Disable colored output (also respects NO_COLOR env var)
--root <PATH> Path to project root (default: current directory)
--config <PATH> Path to custom config file
Contributions welcome! See issues for ideas, or open a PR.
MIT
If this tool saves you CI time, consider giving it a star. It helps others find it.