Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 8 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,19 @@

cascade computes the affected-package set for a Go CI test-selection workflow: given a base ref and a head ref, it prints the set of packages that need re-testing — the changed packages plus everything that imports them, transitively. The intended use is dropping the full-suite test bill on a typical PR by 3-10× while keeping merge-queue runs honest.

## Status

**Pre-v0.1.0; under active development.** The high-level project plan and per-milestone design docs live in [`docs/design/01-draft/`](docs/design/01-draft/). The CLI binary builds today but does not yet compute anything — see the [milestone status](#milestones) below for what's wired up. API and CLI surfaces will iterate freely until v1.0; once tagged, v1.x will commit to standard Go module compatibility, with breaking changes thereafter following Go's `vN/` directory convention.

`cascade --version` reports the embedded module version from [`project/VERSION`](project/VERSION) and, for `go install`-built binaries (where the Makefile's `-ldflags` injection isn't applied), auto-detects commit/dirty/timestamp metadata via `runtime/debug.ReadBuildInfo`. Full Makefile builds inject branch + git-describe summary on top of that.

## Why

cascade exists because [DigitalOcean's `gta`](https://github.com/digitalocean/gta) — an established Go affected-package tool — silently fails on Go 1.25.x. The failure surfaces in `golang.org/x/tools/go/packages.Load`, which gta uses; that loader's stricter module resolution emits "go: updates to go.mod needed" against modules the regular `go list` family considers tidy, and gta swallows the error and exits 0 with an empty package list. An empty list means CI runs zero tests; zero tests means a green build that proved nothing.

Do the the Go language's tendency to swap out libraries for changes in tooling, treating the tool as "the API" is often the safer, more stable approach. Thus, cascade takes a deliberately narrower path than gta: shell out to `go list -deps -json` directly (verified to work on Go 1.25 and 1.26), parse the stream into typed values, build the import DAG, reverse the edges, and compute the closure of the change-set. Every io error is returned and surfaced — silent-failure mode is structurally impossible.
Go's helper libraries — especially `golang.org/x/tools/go/packages` — evolve faster than the `go` command itself; depending on the CLI tool is the more stable bet. cascade takes a deliberately narrower path than gta: shell out to `go list -deps -json` directly (verified on Go 1.25 and 1.26), parse the stream into typed values, build the import DAG, reverse the edges, and compute the closure of the change-set. Every I/O error is returned and surfaced — silent-failure mode is structurally impossible.

## Install

```bash
go install github.com/geomyidia/cascade/cmd/cascade@latest
```

Requires Go ≥ 1.25.3. CI tests against the floor and the latest currently-supported Go major (1.26.x as of this writing).
Requires Go ≥ 1.25.3. CI tests against the floor and the latest currently-supported Go major; the matrix advances with each Go release.

## CLI usage

Expand Down Expand Up @@ -68,7 +62,7 @@ git diff --name-only origin/main..HEAD | cascade --changed-files=-
| `--head` | `HEAD` | Head git ref. |
| `--changed-files` | (none) | Path to a file with one change-set entry per line. `-` reads from stdin. When set, `--base` is not required and `git diff` is not invoked. |
| `--root` | `.` | Working directory for `go list` and module-root for `changeset.Resolve`. |
| `--version` | false | Print version metadata (Version / GitCommit / GitBranch / BuildDate) and exit. |
| `--version` | false | Print `cascade <Version> (build <Branch>@<Commit>, <BuildDate>)` and exit. (Branch is empty for `go install` builds — the module proxy doesn't carry branch metadata.) |
| `--help` | false | Print usage and exit. Routes to stdout per GNU convention; flag-parse errors route help to stderr per stdlib `flag` default. |

### Exit codes
Expand Down Expand Up @@ -107,13 +101,15 @@ if err != nil { /* handle, never swallow */ }
g := depgraph.Build(pkgs)

// Map changed file paths onto their containing packages' import paths.
seeds := changeset.Resolve(changedFiles, pkgs)
// repoRoot is typically `git rev-parse --show-toplevel` from the caller;
// when omitted, changeset.Resolve falls back to os.Getwd.
seeds := changeset.Resolve(changedFiles, pkgs, changeset.WithModuleRoot(repoRoot))

// Compute the reverse-transitive closure (the "cascade").
affected := g.RevDepClosure(seeds)
```

The pure packages (`golist`, `depgraph`, `changeset`) have no io between them and are 100% test-covered. Errors from the io shell (`golist.Run`) wrap their causes with `%w`, so callers can use `errors.Is`/`errors.As` to triage.
The pure packages (`pkg/golist`, `pkg/depgraph`, `pkg/changeset`) compose without adapter glue and are 100% test-covered. Errors from the I/O edges (`golist.Run`'s `go list` invocation; `changeset.Resolve`'s optional `os.Getwd` fallback) wrap their causes with `%w`, so callers can use `errors.Is`/`errors.As` to triage.

## How it works

Expand All @@ -123,20 +119,7 @@ The pure packages (`golist`, `depgraph`, `changeset`) have no io between them an
4. **Map the changed file paths to the packages that contain them.** These become the seed set.
5. **BFS from the seeds over the reversed graph**; emit the union (seeds included), sorted lexicographically for determinism.

Every step is small, typed, and tested. The only io is the two `os/exec` calls in steps 1 and 2 — both are isolated in the io shell, so the algorithmic core has no error-swallowing surface area.

## Milestones

| | Milestone | Status |
|---|---|---|
| M1 | Repo scaffold + CI baseline | completed |
| M2 | `go list` adapter (shell-out + streaming JSON parser) | completed |
| M3 | Dep graph + reverse-dep index + closure | in progress |
| M4 | Changed-files-to-packages mapping | not started |
| M5 | CLI + main wiring | not started |
| M6 | `v0.1.0` release | not started |

Detailed plans live in [`docs/design/01-draft/`](docs/design/01-draft/). The high-level plan is [`0001-cascade-high-level-project-plan.md`](docs/design/01-draft/0001-cascade-high-level-project-plan.md); each milestone has (or will have) its own design doc.
Every step is small, typed, and tested. The only I/O is the two `os/exec` calls in steps 1 and 2 — both are isolated in the I/O shell, so the algorithmic core has no error-swallowing surface area.

## Development

Expand Down
78 changes: 76 additions & 2 deletions docs/dev/0012-implementation-retrospective-m5-cli-main-wiring.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,8 +210,82 @@ The five-seam pattern is now established convention but isn't yet mentioned in `

- **The trio composes without adapter code: structurally verified.** M3's design claim → M4's retro claim → M5's `runPipeline` four-liner. The chain holds. **For M6, if any v0.1.0 release-prep work surfaces an integration-layer adapter need, that's the signal to revisit one of the pure packages' API.** Otherwise, the trio's API stability claim is settled for v1.0.

## F-18 / F-19 post-merge follow-up evidence

Appended after the M5 PR (#10) merged to `main` at commit `78cc7ae`. Both deferrals close out here; M5's ledger reaches **22/22 done**.

### F-18: `go install …@<sha>` against `proxy.golang.org`

Same M1 F-12 pattern. Fresh `GOBIN`, default `GOPROXY=https://proxy.golang.org,direct`:

```
$ GOBIN=$tmp GOPROXY=https://proxy.golang.org,direct \
go install github.com/geomyidia/cascade/cmd/cascade@78cc7ae
go: downloading github.com/geomyidia/cascade v0.0.0-20260507180335-78cc7ae2dac9
$ echo $?
0

$ $tmp/cascade --version
cascade 0.1.0 (build @78cc7ae, 2026-05-07T18:03:35Z)

$ $tmp/cascade --help | head -8
cascade — reverse-transitive closure of a Go change-set under the imports relation

Usage:
cascade --base <ref> [--head <ref>] [--tags <tags>] [--root <dir>]
cascade --changed-files=- [--tags <tags>] [--root <dir>] (read from stdin)
cascade --changed-files=<path> [--tags <tags>] [--root <dir>]
cascade --version
cascade --help
```

Notes:
- `proxy.golang.org` indexed the merge SHA `78cc7ae` and resolved it to pseudo-version `v0.0.0-20260507180335-78cc7ae2dac9` (timestamp + 12-char SHA prefix). The pseudo-version-extraction logic from M1.5 (commit + build-date from `Main.Version` via `runtime/debug.ReadBuildInfo`) is what populates the `--version` output's `@78cc7ae` and `2026-05-07T18:03:35Z` fields. Branch is empty for proxy installs (proxy doesn't carry branch metadata) — same as M1's F-12 closing evidence.
- Binary builds clean for `darwin/arm64` (3.8 MB).
- The `--help` output's first 8 lines confirm the inline `helpText` constant from `internal/cli/cli.go` is the source-of-truth that ships in the released binary.

**F-18 verified mergeable: install path is clean, version metadata threads correctly.**

### F-19: real-codebase sanity check (cascade-against-cascade)

Per the M2 F-18 lineage. Full pipeline (`git diff` → `go list` → `depgraph.Build` + `changeset.Resolve` → `RevDepClosure`) against the merged-to-`main` cascade repo at `cf38732` (head of `main` at follow-up time).

The `gta`-target codebase from M2 F-18 is private; this evidence uses cascade-against-cascade as the public sanity-check substitute. Cascade's own dep graph has 5 packages (`pkg/golist`, `pkg/depgraph`, `pkg/changeset`, `internal/project`, `internal/cli`) plus `cmd/cascade`; the topology is small but each affected-set prediction is hand-derivable, which makes it the right shape for a structural smoke test.

**Six cases run; six expected affected-sets observed:**

| # | Change-set (stdin) | Expected affected-set (hand-derived) | Observed | Wall-clock |
|---|---|---|---|---|
| 1 | `pkg/golist/golist.go` (root of dep tree) | golist, depgraph, changeset, internal/cli, cmd/cascade | match (5 packages, sorted) | sub-second |
| 2 | `pkg/changeset/changeset.go` (mid; only cli + cmd import it) | changeset, internal/cli, cmd/cascade | match (3 packages) | sub-second |
| 3 | `cmd/cascade/main.go` (leaf; nothing imports `main`) | cmd/cascade only | match (1 package) | sub-second |
| 4 | `internal/cli/cli.go` (mid; only cmd/cascade imports) | internal/cli, cmd/cascade | match (2 packages) | sub-second |
| 5 | (empty stdin) | (none) | empty stdout, exit 0 | sub-second |
| 6 | real `--base=6ab23c5 --head=HEAD` (mostly docs commits) | empty (no Go files in diff) | empty stdout, exit 0 | **134ms wall-clock** |

Case 6 measures the full pipeline against a real `git diff --name-only` invocation: `git diff` runs as a subprocess; `go list -deps -json ./...` runs as a subprocess (the heavier of the two for cascade's small repo); both `depgraph.Build` and `changeset.Resolve` run on the parsed result; `RevDepClosure` walks the (empty) seed set. **134ms wall-clock end-to-end** is well under the M5 spec's "milliseconds-to-tens-of-milliseconds + go list's seconds = under 5s for a typical PR" budget.

The M2 retro's structural claim — *"the gta failure mode (silent zero-package output from `packages.Load`) is structurally absent"* — extends here: every case's observed output matches the hand-derived prediction, including the empty cases (case 5, case 6). Cascade does not silently emit zero packages when there's real input; cascade does not crash, hang, or exit non-zero when there's empty input. Each affected-set is what a human walking the dep graph would predict.

**Generic-framing note:** the `gta`-target codebase from M2 F-18 was a private, large-corpus (~2400 packages) module the maintainer can rerun cascade against off-record. The cascade-against-cascade evidence here is the public-PR-safe substitute that closes F-19's letter and spirit: a real-Go-module sanity check producing predicted output. The maintainer's off-record run against the private corpus is the load-bearing scale evidence; both pieces together close M5's loop on the original gta-replacement promise.

**F-19 verified mergeable: cascade produces correct affected-sets on a real Go module across the topology; real `git diff` integration takes 134ms wall-clock.**

### Updated closure summary

After the F-18 + F-19 follow-up commits land, the M5 ledger sits at:

| Status | Count |
|--------|-------|
| Done | **22** |
| Deferred | 0 |
| No-op | 0 |
| Open at close | 0 |

**22/22 done.** Both formerly-deferred rows close cleanly. The two-deferral pattern (F-18 + F-19 require merged-commit prerequisites) worked exactly as documented: deferral with explicit re-entry conditions, evidence appended once those conditions were met. Future milestones with merge-prerequisite verification rows should follow the same pattern.

## Closure

Closing report submitted with the M5 PR; CDC verification pending. **20 of 22 ledger rows reach a final status of `done` in this PR; 2 (F-18, F-19) deferred to post-merge follow-ups with documented reasons and re-entry conditions.** Zero deferrals beyond merge-prerequisite items. Zero no-ops. Zero open at close.
Closing report submitted with the M5 PR (#10) and the F-18/F-19 follow-up PR. **All 22 ledger rows reach a final status of `done`.** Zero deferrals at final close. Zero no-ops. Zero open at close.

Total rows: 22. Done in PR: 20. Deferred to post-merge follow-up: 2. No-op: 0.
Total rows: 22. Done: 22. Deferred: 0. No-op: 0.
Loading