Skip to content

Repository files navigation

cascade

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.

how it works

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

supported languages

  • Go, parses real imports via go/parser, respects build constraints (//go:build), excludes _test.go files from the production dependency graph
  • Rust, parses Cargo.toml [dependencies]
  • D, parses dub.json dependencies

correctness

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 (--base defaults to HEAD~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 runs git fetch --unshallow automatically before diffing)
  • rebases and detached HEAD, verified safe, since a plain git diff between 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.

install

brew tap hariprakazz/cascade https://github.com/hariprakazz/cascade
brew install hariprakazz/cascade/cascade

usage

cascade --base=main
cascade --base=main --head=HEAD --format=json
cascade --base=main --packages-dir=apps

or, without installing:

go run main.go --base=main

flags:

  • --base, base ref to diff against (default HEAD~1)
  • --head, head ref (default HEAD)
  • --format, plain (default), json, or github-matrix
  • --packages-dir, directory containing packages, relative to repo root (default packages)

--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

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.

status

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.

roadmap

  • 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-matrix and GitHub Actions integration
  • Homebrew install (tap points at this repo directly, no separate tap repo)
  • --visualize, print the dependency tree

About

Find what changed, build only what matters.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages