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
31 changes: 28 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ cascade computes the affected-package set for a Go CI test-selection workflow: g

## Why

cascade exists because [DigitalOcean's `gta`](https://github.com/digitalocean/gta) — the 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.
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.

cascade takes a deliberately narrower path: 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.
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.

## Install

Expand Down Expand Up @@ -57,7 +57,32 @@ For callers who already have a list of changed files (e.g., a CI workflow that's
git diff --name-only origin/main..HEAD | cascade --changed-files=-
```

`cascade --help` prints the full flag reference and exit-code table.
### Flag reference

<!-- keep in sync with internal/cli/cli.go's helpText constant -->

| Flag | Default | Purpose |
|------|---------|---------|
| `--tags` | (none) | Comma-separated build tags passed to `go list -tags=`. |
| `--base` | (required unless `--changed-files`) | Base git ref (e.g. `origin/main`); cascade runs `git diff --name-only <base>..<head>` to derive the change-set. |
| `--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. |
| `--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

| Code | Meaning |
|------|---------|
| 0 | Success. Output may be empty if no Go files changed. |
| 1 | Flag-parse error, missing required flags, or stdin/file read failure. |
| 2 | `git diff` failed. The `*GitDiffError` carries the captured `git` stderr; cascade prints it on its own stderr. |
| 3 | `go list` failed. The wrapped `*golist.ExitError` carries the captured `go list` stderr. |
| 4 | Internal logic error. Should never occur — surface as a real bug. |
| 5 | Cancelled or interrupted (SIGINT, SIGTERM, or context cancellation). |

`cascade --help` prints the same flag reference and exit code table to stdout. CI workflows should branch on the specific exit code (e.g. retry the run on exit 5; fail-fast on exit 4) rather than treating all non-zero codes uniformly.

## Library

Expand Down
49 changes: 5 additions & 44 deletions cmd/cascade/main.go
Original file line number Diff line number Diff line change
@@ -1,55 +1,16 @@
// Command cascade computes the reverse-transitive closure of a Go change-set
// under the imports relation. See https://github.com/geomyidia/cascade.
//
// All orchestration lives in internal/cli; main is a single-line delegation
// so the cli.Run pipeline is testable in-process without subprocess overhead.
package main

import (
"errors"
"flag"
"fmt"
"io"
"os"

"github.com/geomyidia/cascade/internal/project"
"github.com/geomyidia/cascade/internal/cli"
)

func main() {
os.Exit(run(os.Args[1:], os.Stdout, os.Stderr))
}

// run is the entry point factored out of main so it can be exercised
// in-process by tests. It returns the desired process exit code rather
// than calling os.Exit directly.
//
// Exit-code contract:
//
// 0 --version (prints metadata to stdout) or --help
// 1 flag parse error other than flag.ErrHelp
// 2 no flags / unknown positional args (M1 placeholder; M5 will
// replace this with the real pipeline)
func run(args []string, stdout, stderr io.Writer) int {
fs := flag.NewFlagSet("cascade", flag.ContinueOnError)
fs.SetOutput(stderr)

showVersion := fs.Bool("version", false, "print version information and exit")

if err := fs.Parse(args); err != nil {
if errors.Is(err, flag.ErrHelp) {
return 0
}
return 1
}

if *showVersion {
fmt.Fprintf(stdout, "cascade %s (build %s)\n",
project.VersionString(), project.BuildString())
return 0
}

if fs.NArg() > 0 {
fmt.Fprintf(stderr, "cascade: unexpected argument %q\n", fs.Arg(0))
return 2
}

fmt.Fprintln(stderr, "cascade: not yet implemented (this is the M1 placeholder)")
return 2
os.Exit(cli.Run(os.Args[1:], os.Stdin, os.Stdout, os.Stderr))
}
Loading
Loading