Tests a library against the projects that depend on it. downstream clones a set of dependents, runs their tests against the published version of the library to establish a baseline, replaces the dependency with a local checkout or branch, runs the tests again, and reports which dependents the change breaks.
The dependent set can be discovered automatically from the ecosyste.ms package index or curated by hand in a downstream.toml file committed to the library's repository. Replacement is performed through the managers library, which maps the operation onto each package manager's own override mechanism.
Supported today:
- Go modules: baseline/replace/retest, plus auto-narrowing to packages whose imports reach the upstream module.
- Cargo crates: baseline/replace/retest with test commands detected by
briefor configured withtest. - npm-family package managers: baseline/replace/retest with test commands detected by
briefor configured withtest.
Bundler, uv, and Composer are handled by the underlying replace operation and will be enabled in downstream in a later release.
go install github.com/git-pkgs/downstream@latestdownstream run discovers dependents (or reads an existing downstream.toml), tests each against the patched library, and prints an aggregate report. Exit status is non-zero only when the patched run introduces failures that were not present in the baseline.
cd /path/to/your/library
downstream run --upstream-path . --limit 5downstream discover queries the ecosyste.ms dependent_packages API for the most-used packages that depend on the given package, drops forks and archived or stale repositories, shallow-clones the survivors to score them on test count and on how many source files reference the package, and writes the ranked top N to downstream.toml. Re-running against an existing file keeps entries with source = "manual" and any per-dependent overrides; previously discovered entries are rescored and new candidates appended with a (new) marker.
downstream discover --limit 5
downstream discover --package github.com/spf13/cobra --no-analyze --stdoutdownstream test runs the baseline/replace/retest loop against either a single dependent given on the command line or the set in downstream.toml.
downstream test --upstream-path . --dependent https://github.com/cli/cli
downstream test --upstream-path . --only cli/clidownstream list reads downstream.toml back as plain rows, --json, or --github-output for use in an Actions matrix step.
| flag | |
|---|---|
--upstream |
module path of the library, optionally module@ref; defaults to [package].name from config or the module in ./go.mod |
--upstream-path |
local path to the patched library; takes precedence over @ref |
--config, -c |
path to downstream.toml (default ./downstream.toml) |
--only |
filter configured dependents by name, slug, glob or substring; repeatable |
--workdir |
directory for clones (default: temp dir) |
--keep |
retain the workdir after the run |
--timeout |
per-test-run timeout (default 30m) |
--limit, -n |
number of dependents to discover (default 5) |
--no-analyze |
skip the clone-and-score phase of discover |
[package]
name = "github.com/spf13/cobra"
ecosystem = "go"
# discover: 27 files reference upstream, 412 test files, 142019 dependent repos, 38447 stars
[[dependents]]
name = "github.com/cli/cli"
repo = "https://github.com/cli/cli"
source = "discover"
# ref = "v2.40.0" # pin to a tag if the default branch is unstable
# test = "go test ./pkg/..." # override the auto-narrowed test command
# subdir = "." # for monorepos
# skip_baseline = true # use the dependent's CI status instead of running tests twice
[[dependents]]
name = "github.com/gohugoio/hugo"
repo = "https://github.com/gohugoio/hugo"
source = "manual" # kept regardless of discover rankingTest commands are resolved in this order:
- A dependent's
testfield overrides all automatic detection. - Go dependents default to
go testover packages whose imports reach the upstream module, computed viago list -test -json ./.... - Other supported ecosystems use the test command detected by
brief, such ascargo test,npm test, or a project script.
To run downstream tests as part of a library's CI, commit a downstream.toml and add a workflow that fans out one job per dependent:
name: downstream
on:
workflow_dispatch:
schedule:
- cron: "0 6 * * 1"
pull_request:
types: [labeled]
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
load:
if: github.event_name != 'pull_request' || github.event.label.name == 'test-downstream'
runs-on: ubuntu-latest
outputs:
dependents: ${{ steps.load.outputs.dependents }}
steps:
- uses: actions/checkout@v5
with: {persist-credentials: false}
- uses: actions/setup-go@v6
with: {go-version: stable}
- run: go install github.com/git-pkgs/downstream@latest
- id: load
run: downstream list --github-output >> "$GITHUB_OUTPUT"
test:
needs: load
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
dependent: ${{ fromJson(needs.load.outputs.dependents) }}
steps:
- uses: actions/checkout@v5
with: {path: upstream, persist-credentials: false}
- uses: actions/setup-go@v6
with: {go-version: stable}
- run: go install github.com/git-pkgs/downstream@latest
- env:
DEP_REPO: ${{ matrix.dependent.repo }}
DEP_TEST: ${{ matrix.dependent.test }}
run: |
downstream test \
--upstream-path ./upstream \
--dependent "$DEP_REPO" \
--test "$DEP_TEST" \
>> "$GITHUB_STEP_SUMMARY"Triggering on a test-downstream label rather than every push keeps the cost manageable; the weekly schedule catches drift in the dependents themselves. A composite action wrapping these steps will be published separately.
Replacement is delegated to managers and follows each ecosystem's native override mechanism:
- Go:
downstreamrunsgo mod edit -replace <module>=<path>followed bygo mod tidy, redirecting the module across the dependent's entire build including transitive consumers. - Cargo:
[patch.crates-io]redirects the crate across the dependent's entire build, including transitive consumers. - npm-family managers:
file:installs redirect the dependent's direct dependency; transitive consumers in the same tree keep their registry copy.
See the managers replace documentation for per-ecosystem behaviour and limitations.
| status | meaning |
|---|---|
| passed | baseline and patched runs both succeeded |
| failed | baseline succeeded, patched failed; the change introduced a regression |
| broken-baseline | baseline failed before any change was applied; reported but not held against the change |
| error | the dependent could not be set up (clone, replace, or manager detection failed) |
go test ./...
golangci-lint run ./...Tests use fixtures under internal/run/testdata so the suite is hermetic.
MIT. See LICENSE.