Cascade tells you which packages in your monorepo actually need rebuilding after a change.
Most CI pipelines rebuild everything on every push. That's slow and wasteful. Cascade builds a real dependency graph from your code and traces which packages are truly affected, nothing more.
change a file, cascade maps it to a package, walks the real dependency graph (parsed from actual imports, not hand-maintained config), outputs only the packages affected, directly or transitively
- Go, parses real imports via
go/parser, respects build constraints (//go:build), excludes_test.gofiles from the production dependency graph - Rust, parses
Cargo.toml[dependencies] - D, parses
dub.jsondependencies
Cascade handles the git edge cases that break naive diff-based detection:
- renamed files (
git diff --name-status, not--name-only, so both old and new paths get mapped to their packages) - merge commits (
--basedefaults toHEAD~1, which is ambiguous on a merge commit since it has two parents; cascade resolves the actual merge-base instead) - shallow clones (CI checkouts are often
--depth 1; cascade detects this and runsgit fetch --unshallowautomatically before diffing) - rebases and detached HEAD, verified safe, since a plain
git diffbetween two resolved commits doesn't care how they got there
The dependency graph is not hardcoded to this repo. modulePrefix is derived
from the target repo's own go.mod, and --packages-dir lets you point
cascade at any repo's actual package layout.
brew tap hariprakazz/cascade https://github.com/hariprakazz/cascade
brew install hariprakazz/cascade/cascadecascade --base=main
cascade --base=main --head=HEAD --format=json
cascade --base=main --packages-dir=appsor, without installing:
go run main.go --base=mainflags:
--base, base ref to diff against (defaultHEAD~1)--head, head ref (defaultHEAD)--format,plain(default),json, orgithub-matrix--packages-dir, directory containing packages, relative to repo root (defaultpackages)
--format=github-matrix outputs a plain JSON array of affected package names,
consumable directly in a matrix strategy. See
.github/workflows/affected-build.yml.example for a working setup: one job
runs cascade to generate the matrix, a second job fans out a build per
affected package.
benchmark/generate.sh builds a reproducible 11-package multi-language fixture
repo (Go, Rust, D) with real cross-package dependency chains and real commit
history. Anyone can regenerate it and verify these numbers independently:
./benchmark/generate.sh /tmp/bench-repo| change | affected | skipped | time |
|---|---|---|---|
| core package (imported by nearly everything) | 9 / 11 | 2 | 7µs |
| leaf + mid-tier package | 3 / 11 | 8 | 6µs |
A core-package change legitimately affects most of the graph, and cascade reports that honestly instead of pretending otherwise. The real savings show up on leaf and mid-tier changes, which are far more common day to day.
21 passing unit tests cover graph traversal, import parsing across all three languages, build-constraint filtering, test-file exclusion, and the git edge cases above. Building this in public alongside my GSoC 2026 work on the D language's build tooling.
- git diff-based change detection
- real dependency graph from Go imports (not hand-maintained config)
- build-constraint-aware parsing
- Rust (
Cargo.toml) support - D (
dub.json) support - rename, merge-commit, and shallow-clone correctness
- benchmark against a reproducible multi-language fixture (no real public repo fit cascade's model)
-
--format=github-matrixand GitHub Actions integration - Homebrew install (tap points at this repo directly, no separate tap repo)
-
--visualize, print the dependency tree