https://github.com/rani367/affected
Run only the tests that matter. Language-agnostic affected package detection for monorepos. Supports Cargo, npm, pnpm, Yarn, Bun, Go, Python, Maven, Gradle, .NET, Swift, Dart/Flutter, Elixir, sbt.
https://github.com/rani367/affected
bun cargo ci cli dart dependency-graph dotnet elixir flutter github-actions go gradle maven monorepo npm python rust scala swift testing
Last synced: 3 months ago
JSON representation
Run only the tests that matter. Language-agnostic affected package detection for monorepos. Supports Cargo, npm, pnpm, Yarn, Bun, Go, Python, Maven, Gradle, .NET, Swift, Dart/Flutter, Elixir, sbt.
- Host: GitHub
- URL: https://github.com/rani367/affected
- Owner: Rani367
- License: mit
- Created: 2026-03-29T20:42:56.000Z (3 months ago)
- Default Branch: main
- Last Pushed: 2026-04-04T00:18:50.000Z (3 months ago)
- Last Synced: 2026-04-04T01:02:24.549Z (3 months ago)
- Topics: bun, cargo, ci, cli, dart, dependency-graph, dotnet, elixir, flutter, github-actions, go, gradle, maven, monorepo, npm, python, rust, scala, swift, testing
- Language: Rust
- Homepage: https://rani367.github.io/affected
- Size: 313 KB
- Stars: 6
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE
Awesome Lists containing this project
README
affected
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.
---
## Demo
```
$ 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
```
## Why
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.
## Features
- **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 `core` changes and `api` depends on `core`, 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|azure` with dynamic job matrices
- **Watch mode** -- `affected watch test --base main` re-runs on file changes
- **`affected init`** -- interactive setup wizard generates `.affected.toml`
- **Dependency tree** -- `affected graph` renders a Unicode tree with affected highlighting
- **Parallel execution** -- `--jobs 4` runs commands across multiple threads
- **CI-first** -- `--json`, `--junit`, PR comment bot, shell completions
- **Fast** -- written in Rust, uses libgit2 for native git operations
## Install
```bash
# 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
```
## Quick Start
```bash
# 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 4
```
## Usage
### `affected test`
Run tests for affected packages.
```bash
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 affected
```
### `affected run`
Run any command on affected packages. Use `{package}` as a placeholder.
```bash
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 # preview
```
### `affected list`
List affected packages without running anything.
```bash
affected list --base main # list affected packages
affected list --base main --json # JSON output for CI
affected list --base main --explain # show dependency chains
```
### `affected graph`
Display the project dependency graph as a Unicode tree.
```bash
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 image
```
Example output:
```
Dependency Graph (5 packages, 3 affected):
cli ●
└── api ●
└── core ●
utils
standalone (no dependencies)
```
### `affected ci`
Output variables for CI systems with multi-platform support.
```bash
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=value
```
### `affected init`
Interactive setup wizard to generate `.affected.toml`.
```bash
affected init # interactive prompts
affected init --non-interactive # auto-detect and use defaults
```
### `affected watch`
Watch for file changes and re-run commands automatically.
```bash
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
```
## GitHub Actions
### Setup
```yaml
- uses: Rani367/setup-affected@v1
- run: affected test --merge-base origin/main
```
### Dynamic Matrix (each package as a separate job)
Each affected package runs as a **separate parallel job** in the GitHub Actions UI:
```yaml
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 }}
```
### PR Comment Bot
Auto-comment on PRs showing which packages are affected and why:
```yaml
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@v1
```
This posts a comment like:
> **Affected Packages (3 of 8)**
>
> | Package | Reason |
> |---------|--------|
> | **core** | directly changed: `src/lib.rs` |
> | **api** | depends on: core |
> | **cli** | depends on: api -> core |
The comment updates automatically on each push (no duplicates).
### `affected completions`
Generate shell completions.
```bash
affected completions bash >> ~/.bashrc
affected completions zsh >> ~/.zshrc
affected completions fish > ~/.config/fish/completions/affected.fish
```
## Supported Ecosystems
| 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 `` | POM dependency declarations |
| **Gradle** | `settings.gradle(.kts)` | `project(':...')` references |
| **.NET/C#** | `*.sln` solution file | `` 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 |
## Configuration
Create `.affected.toml` in your project root (optional):
```toml
# 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
```
## How It Works
1. **Detect** -- scans for marker files to identify the ecosystem
2. **Resolve** -- builds a dependency graph from project manifests
3. **Diff** -- computes changed files using libgit2 (base ref vs HEAD + working tree)
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 commands for affected packages only
## Comparison
| 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 ` | 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+ |
## Global Flags
```
-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 to project root (default: current directory)
--config Path to custom config file
```
## Contributing
Contributions welcome! See [issues](https://github.com/Rani367/affected/issues) for ideas, or open a PR.
## License
MIT
---
If this tool saves you CI time, consider giving it a star. It helps others find it.