{"id":47743069,"url":"https://github.com/rani367/affected","last_synced_at":"2026-04-05T02:00:29.040Z","repository":{"id":347878152,"uuid":"1195582103","full_name":"Rani367/affected","owner":"Rani367","description":"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.","archived":false,"fork":false,"pushed_at":"2026-04-04T00:18:50.000Z","size":320,"stargazers_count":6,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-04T01:02:24.549Z","etag":null,"topics":["bun","cargo","ci","cli","dart","dependency-graph","dotnet","elixir","flutter","github-actions","go","gradle","maven","monorepo","npm","python","rust","scala","swift","testing"],"latest_commit_sha":null,"homepage":"https://rani367.github.io/affected","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Rani367.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-29T20:42:56.000Z","updated_at":"2026-04-04T00:18:58.000Z","dependencies_parsed_at":"2026-04-04T01:00:53.673Z","dependency_job_id":null,"html_url":"https://github.com/Rani367/affected","commit_stats":null,"previous_names":["rani367/affected"],"tags_count":6,"template":false,"template_full_name":null,"purl":"pkg:github/Rani367/affected","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Rani367%2Faffected","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Rani367%2Faffected/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Rani367%2Faffected/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Rani367%2Faffected/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Rani367","download_url":"https://codeload.github.com/Rani367/affected/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Rani367%2Faffected/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31421869,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-05T00:25:07.052Z","status":"online","status_checked_at":"2026-04-05T02:00:05.211Z","response_time":75,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["bun","cargo","ci","cli","dart","dependency-graph","dotnet","elixir","flutter","github-actions","go","gradle","maven","monorepo","npm","python","rust","scala","swift","testing"],"created_at":"2026-04-03T00:06:03.150Z","updated_at":"2026-04-05T02:00:29.011Z","avatar_url":"https://github.com/Rani367.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n  \u003ch1 align=\"center\"\u003eaffected\u003c/h1\u003e\n  \u003cp align=\"center\"\u003eDetect affected packages. Run only what matters.\u003c/p\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://github.com/Rani367/affected/actions/workflows/ci.yml\"\u003e\u003cimg src=\"https://github.com/Rani367/affected/actions/workflows/ci.yml/badge.svg\" alt=\"CI\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://crates.io/crates/affected-cli\"\u003e\u003cimg src=\"https://img.shields.io/crates/v/affected-cli.svg\" alt=\"crates.io\"\u003e\u003c/a\u003e\n  \u003ca href=\"LICENSE\"\u003e\u003cimg src=\"https://img.shields.io/badge/license-MIT-blue.svg\" alt=\"MIT License\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://github.com/Rani367/affected/stargazers\"\u003e\u003cimg src=\"https://img.shields.io/github/stars/Rani367/affected?style=social\" alt=\"GitHub Stars\"\u003e\u003c/a\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  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.\n\u003c/p\u003e\n\n---\n\n## Demo\n\n```\n$ affected list --base main --explain\n\n3 affected package(s) (base: main, 2 files changed):\n\n  ● core       (directly changed: src/lib.rs)\n  ● api        (depends on: core)\n  ● cli        (depends on: api → core)\n```\n\n```\n$ affected run \"cargo clippy -p {package}\" --base main --jobs 4 --dry-run\n\nRunning command for 3 affected package(s) (out of 8 total, 2 files changed):\n\n  [dry-run] core: cargo clippy -p core\n  [dry-run] api: cargo clippy -p api\n  [dry-run] cli: cargo clippy -p cli\n```\n\n## Why\n\nEvery 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.\n\n`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.\n\n## Features\n\n- **Zero config** -- auto-detects your project type and dependency graph\n- **13 ecosystems** -- Cargo, npm, pnpm, Yarn, Bun, Go, Python, Maven, Gradle, .NET, Swift, Dart/Flutter, Elixir, Scala/sbt\n- **Transitive detection** -- if `core` changes and `api` depends on `core`, both are affected\n- **`affected run`** -- run *any command* on affected packages, not just tests\n- **`--explain`** -- shows *why* each package is affected with the full dependency chain\n- **Multi-CI** -- `affected ci --format github|gitlab|circleci|azure` with dynamic job matrices\n- **Watch mode** -- `affected watch test --base main` re-runs on file changes\n- **`affected init`** -- interactive setup wizard generates `.affected.toml`\n- **Dependency tree** -- `affected graph` renders a Unicode tree with affected highlighting\n- **Parallel execution** -- `--jobs 4` runs commands across multiple threads\n- **CI-first** -- `--json`, `--junit`, PR comment bot, shell completions\n- **Fast** -- written in Rust, uses libgit2 for native git operations\n\n## Install\n\n```bash\n# Homebrew (macOS/Linux)\nbrew install Rani367/tap/affected\n\n# uv\nuv tool install affected\n\n# pipx\npipx install affected\n\n# pip\npip install affected\n\n# Cargo\ncargo install affected-cli\n\n# GitHub Actions\n- uses: Rani367/setup-affected@v1\n\n# Or download a binary from Releases\n```\n\n## Quick Start\n\n```bash\n# What's affected?\naffected list --base main\n\n# See why each package is affected\naffected list --base main --explain\n\n# Run only affected tests\naffected test --base main\n\n# Run any command on affected packages\naffected run \"cargo clippy -p {package}\" --base main\n\n# Parallel execution\naffected test --base main --jobs 4\n```\n\n## Usage\n\n### `affected test`\n\nRun tests for affected packages.\n\n```bash\naffected test --base main                     # run affected tests\naffected test --base HEAD~3                   # compare vs 3 commits ago\naffected test --merge-base main               # auto-detect merge-base (best for PRs)\naffected test --base main --jobs 4            # parallel execution\naffected test --base main --timeout 300       # 5 min timeout per package\naffected test --base main --dry-run           # show what would run\naffected test --base main --json              # structured JSON output\naffected test --base main --junit results.xml # JUnit XML for CI\naffected test --base main --filter \"lib-*\"    # only test matching packages\naffected test --base main --skip \"e2e-*\"      # skip matching packages\naffected test --base main --explain           # show why each is affected\n```\n\n### `affected run`\n\nRun any command on affected packages. Use `{package}` as a placeholder.\n\n```bash\naffected run \"cargo clippy -p {package}\" --base main         # lint affected\naffected run \"cargo build -p {package}\" --base main --jobs 4  # build affected\naffected run \"npm run lint --workspace={package}\" --base main  # npm lint\naffected run \"go vet ./{package}/...\" --base main              # go vet\naffected run \"echo {package}\" --base main --dry-run            # preview\n```\n\n### `affected list`\n\nList affected packages without running anything.\n\n```bash\naffected list --base main                     # list affected packages\naffected list --base main --json              # JSON output for CI\naffected list --base main --explain           # show dependency chains\n```\n\n### `affected graph`\n\nDisplay the project dependency graph as a Unicode tree.\n\n```bash\naffected graph                                # Unicode dependency tree\naffected graph --base main                    # highlight affected packages\naffected graph --dot                          # DOT format for Graphviz\naffected graph --dot | dot -Tpng -o graph.png # render as image\n```\n\nExample output:\n\n```\nDependency Graph (5 packages, 3 affected):\n\n  cli  ●\n  └── api  ●\n      └── core  ●\n  utils\n  standalone  (no dependencies)\n```\n\n### `affected ci`\n\nOutput variables for CI systems with multi-platform support.\n\n```bash\naffected ci --base main                        # GitHub Actions (default)\naffected ci --base main --format gitlab        # GitLab CI (writes ci.env)\naffected ci --base main --format azure         # Azure Pipelines (##vso)\naffected ci --base main --format circleci      # CircleCI ($BASH_ENV)\naffected ci --base main --format generic       # plain key=value\n```\n\n### `affected init`\n\nInteractive setup wizard to generate `.affected.toml`.\n\n```bash\naffected init                                  # interactive prompts\naffected init --non-interactive                # auto-detect and use defaults\n```\n\n### `affected watch`\n\nWatch for file changes and re-run commands automatically.\n\n```bash\naffected watch test --base main                # re-run tests on changes\naffected watch list --base main                # re-list affected on changes\naffected watch run \"cargo clippy -p {package}\" --base main  # re-run command\naffected watch test --base main --debounce 1000 # 1s debounce\n```\n\n## GitHub Actions\n\n### Setup\n\n```yaml\n- uses: Rani367/setup-affected@v1\n- run: affected test --merge-base origin/main\n```\n\n### Dynamic Matrix (each package as a separate job)\n\nEach affected package runs as a **separate parallel job** in the GitHub Actions UI:\n\n```yaml\njobs:\n  detect:\n    runs-on: ubuntu-latest\n    outputs:\n      matrix: ${{ steps.affected.outputs.matrix }}\n      has_affected: ${{ steps.affected.outputs.has_affected }}\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n      - uses: Rani367/setup-affected@v1\n      - id: affected\n        run: affected ci --merge-base origin/main\n\n  test:\n    needs: detect\n    if: needs.detect.outputs.has_affected == 'true'\n    runs-on: ubuntu-latest\n    strategy:\n      matrix: ${{ fromJson(needs.detect.outputs.matrix) }}\n      fail-fast: false\n    steps:\n      - uses: actions/checkout@v4\n      - name: Test ${{ matrix.package }}\n        run: cargo test -p ${{ matrix.package }}\n```\n\n### PR Comment Bot\n\nAuto-comment on PRs showing which packages are affected and why:\n\n```yaml\non:\n  pull_request:\n    branches: [main]\n\npermissions:\n  pull-requests: write\n\njobs:\n  comment:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n      - uses: Rani367/affected-pr-comment@v1\n```\n\nThis posts a comment like:\n\n\u003e **Affected Packages (3 of 8)**\n\u003e\n\u003e | Package | Reason |\n\u003e |---------|--------|\n\u003e | **core** | directly changed: `src/lib.rs` |\n\u003e | **api** | depends on: core |\n\u003e | **cli** | depends on: api -\u003e core |\n\nThe comment updates automatically on each push (no duplicates).\n\n### `affected completions`\n\nGenerate shell completions.\n\n```bash\naffected completions bash \u003e\u003e ~/.bashrc\naffected completions zsh \u003e\u003e ~/.zshrc\naffected completions fish \u003e ~/.config/fish/completions/affected.fish\n```\n\n## Supported Ecosystems\n\n| Ecosystem | Detected By | Dependency Source |\n|-----------|------------|-------------------|\n| **Cargo** | `Cargo.toml` with `[workspace]` | `cargo metadata` JSON |\n| **npm** | `package.json` with `workspaces` | `package.json` dependencies |\n| **pnpm** | `pnpm-workspace.yaml` | `package.json` dependencies |\n| **Yarn Berry** | `.yarnrc.yml` | `package.json` dependencies |\n| **Bun** | `bun.lock` / `bunfig.toml` | `package.json` dependencies |\n| **Go** | `go.work` / `go.mod` | `go mod graph` |\n| **Python** | `pyproject.toml` | PEP 621 deps + import scanning |\n| **Poetry** | `[tool.poetry]` in pyproject.toml | Poetry path dependencies |\n| **uv** | `[tool.uv.workspace]` in pyproject.toml | Workspace member globs |\n| **Maven** | `pom.xml` with `\u003cmodules\u003e` | POM dependency declarations |\n| **Gradle** | `settings.gradle(.kts)` | `project(':...')` references |\n| **.NET/C#** | `*.sln` solution file | `\u003cProjectReference\u003e` in .csproj |\n| **Swift/SPM** | `Package.swift` (multi-target) | Target dependency declarations |\n| **Dart/Flutter** | `pubspec.yaml` workspace / `melos.yaml` | `dependencies` in pubspec.yaml |\n| **Elixir** | `mix.exs` + `apps/` (umbrella) | `in_umbrella: true` deps |\n| **Scala/sbt** | `build.sbt` | `.dependsOn()` project refs |\n\n## Configuration\n\nCreate `.affected.toml` in your project root (optional):\n\n```toml\n# Ignore files that should never trigger tests\nignore = [\"*.md\", \"docs/**\", \".github/**\"]\n\n# Custom test commands per ecosystem\n[test]\ncargo = \"cargo nextest run -p {package}\"\nnpm = \"pnpm test --filter {package}\"\ngo = \"go test -v ./{package}/...\"\npython = \"uv run --package {package} pytest\"\nmaven = \"mvn test -pl {package}\"\ngradle = \"gradle :{package}:test\"\nbun = \"bun test --filter {package}\"\ndotnet = \"dotnet test {package}\"\ndart = \"dart test -C {package}\"\nswift = \"swift test --filter {package}\"\nelixir = \"mix cmd --app {package} mix test\"\nsbt = \"sbt {package}/test\"\n\n# Per-package overrides\n[packages.slow-e2e]\ntest = \"cargo test -p slow-e2e -- --ignored\"\ntimeout = 600\n\n[packages.legacy-service]\nskip = true\n```\n\n## How It Works\n\n1. **Detect** -- scans for marker files to identify the ecosystem\n2. **Resolve** -- builds a dependency graph from project manifests\n3. **Diff** -- computes changed files using libgit2 (base ref vs HEAD + working tree)\n4. **Map** -- maps each changed file to its owning package\n5. **Traverse** -- runs reverse BFS on the dependency graph to find all transitively affected packages\n6. **Execute** -- runs commands for affected packages only\n\n## Comparison\n\n| Feature | `affected` | Nx | Turborepo | Bazel |\n|---------|-----------|-----|-----------|-------|\n| Zero config | Yes | No | No | No |\n| Standalone binary | Yes | No (Node.js) | No (Node.js) | No (JVM) |\n| Language agnostic | 13 ecosystems | JS/TS + plugins | JS/TS | Any (with rules) |\n| Setup time | 1 minute | Hours | Hours | Days-weeks |\n| `affected run \u003ccmd\u003e` | Yes | No | No | No |\n| `--explain` | Yes | No | No | No |\n| Watch mode | Yes | Yes | No | No |\n| Multi-CI support | 5 platforms | GitHub only | GitHub only | Custom |\n| Dynamic CI matrix | Yes | Plugin | No | No |\n| PR comment bot | Yes | No | No | No |\n| Interactive setup | `affected init` | `nx init` | `turbo init` | Manual |\n| Binary size | ~5MB | ~200MB+ | ~100MB+ | ~500MB+ |\n\n## Global Flags\n\n```\n-v, --verbose    Increase verbosity (-v for debug, -vv for trace)\n-q, --quiet      Suppress non-essential output\n--no-color       Disable colored output (also respects NO_COLOR env var)\n--root \u003cPATH\u003e    Path to project root (default: current directory)\n--config \u003cPATH\u003e  Path to custom config file\n```\n\n## Contributing\n\nContributions welcome! See [issues](https://github.com/Rani367/affected/issues) for ideas, or open a PR.\n\n## License\n\nMIT\n\n---\n\nIf this tool saves you CI time, consider giving it a star. It helps others find it.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frani367%2Faffected","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frani367%2Faffected","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frani367%2Faffected/lists"}