Graph-aware change detection for Rust monorepos. Test only what changed.
- Graph-aware, not path-only: uses
cargo-rail’s workspace graph instead of hard-codedpaths-filterrules. - Single source of truth: CI classification rules live in
rail.toml, shared with the CLI. - Shared engine: same logic as
cargo rail affected/cargo rail test, so local runs match CI.
If this action saves you CI time or complexity, consider starring the main project: cargo-rail.
- Detects which crates are affected by your changes (including transitive dependencies)
- Installs in ~3 seconds (pre-built binaries, no Rust toolchain needed)
- Works with PRs, push events, and manual runs
- Outputs
docs-onlyandrebuild-allflags for smart CI skipping
- uses: loadingalias/cargo-rail-action@v1name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Required for change detection
- uses: loadingalias/cargo-rail-action@v1
id: affected
- name: Test affected crates
if: steps.affected.outputs.count != '0' && steps.affected.outputs.docs-only != 'true'
run: cargo test ${{ steps.affected.outputs.cargo-args }}
- name: Test all (infrastructure changed)
if: steps.affected.outputs.rebuild-all == 'true'
run: cargo test --workspaceFor large monorepos, run each crate in parallel:
jobs:
detect:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.affected.outputs.matrix }}
count: ${{ steps.affected.outputs.count }}
rebuild-all: ${{ steps.affected.outputs.rebuild-all }}
docs-only: ${{ steps.affected.outputs.docs-only }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: loadingalias/cargo-rail-action@v1
id: affected
test:
needs: detect
if: |
needs.detect.outputs.count != '0' &&
needs.detect.outputs.rebuild-all != 'true' &&
needs.detect.outputs.docs-only != 'true'
strategy:
fail-fast: false
matrix:
crate: ${{ fromJson(needs.detect.outputs.matrix) }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- run: cargo test -p ${{ matrix.crate }}
test-all:
needs: detect
if: needs.detect.outputs.rebuild-all == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- run: cargo test --workspace| Input | Default | Description |
|---|---|---|
version |
latest |
cargo-rail version to install |
since |
auto | Git ref to compare against (auto-detects PR base or origin/main) |
command |
affected |
Command to run: affected or test |
args |
Additional arguments passed to cargo-rail | |
working-directory |
. |
Directory containing Cargo.toml |
token |
${{ github.token }} |
GitHub token for downloading releases |
| Output | Description |
|---|---|
crates |
Space-separated affected crates: core api cli |
cargo-args |
Cargo -p flags for direct use: -p core -p api -p cli |
matrix |
JSON array for strategy.matrix: ["core","api","cli"] |
count |
Number of affected crates |
| Output | Description |
|---|---|
rebuild-all |
true if infrastructure files changed (Cargo.lock, CI, etc.) |
docs-only |
true if only documentation changed |
Additional outputs
| Output | Description |
|---|---|
direct |
Crates with direct file changes |
transitive |
Crates affected through dependency graph |
changed-files |
Number of files changed |
infrastructure-files |
JSON array of files that triggered rebuild-all |
custom-categories |
Custom category matches from rail.toml config |
install-method |
How cargo-rail was installed (binary, binstall, cargo-install, cached) |
cargo-rail-version |
Installed version |
Optional. Generate with cargo rail init:
# .config/rail.toml
[change-detection]
infrastructure = [".github/**", "Cargo.lock", "rust-toolchain.toml"]
[change-detection.custom]
benchmarks = ["benches/**"]When infrastructure files change, rebuild-all is set to true. Use this to trigger full workspace tests.
Skip CI on docs-only changes
- uses: loadingalias/cargo-rail-action@v1
id: affected
- name: Run tests
if: steps.affected.outputs.docs-only != 'true'
run: cargo test ${{ steps.affected.outputs.cargo-args }}Run tests directly (let cargo-rail handle it)
- uses: loadingalias/cargo-rail-action@v1
with:
command: testWith cargo-nextest
- uses: loadingalias/cargo-rail-action@v1
id: affected
- name: Install nextest
if: steps.affected.outputs.count != '0'
uses: taiki-e/install-action@nextest
- name: Test affected
if: steps.affected.outputs.count != '0' && steps.affected.outputs.docs-only != 'true'
run: cargo nextest run ${{ steps.affected.outputs.cargo-args }}Custom base ref
- uses: loadingalias/cargo-rail-action@v1
with:
since: origin/developMonorepo with Rust in subdirectory
- uses: loadingalias/cargo-rail-action@v1
with:
working-directory: rust/Pin to specific version
- uses: loadingalias/cargo-rail-action@v1
with:
version: "0.2.0"Migration from dorny/paths-filter
Before (hand-maintained paths):
- uses: dorny/paths-filter@v3
id: changes
with:
filters: |
core:
- "crates/core/**"
api:
- "crates/api/**"
cli:
- "crates/cli/**"After (graph-aware, config in rail.toml):
- uses: loadingalias/cargo-rail-action@v1
id: affected
- name: Test affected crates
if: steps.affected.outputs.count != '0' && steps.affected.outputs.docs-only != 'true'
run: cargo test ${{ steps.affected.outputs.cargo-args }}This moves change classification into rail.toml and lets the dependency graph, not hand-written path lists, decide which crates are affected.
- Installs cargo-rail — Downloads pre-built binary (fast) or falls back to
cargo install - Detects base ref — Uses PR base,
origin/main, or providedsinceinput - Runs change detection — Maps changed files to crates via dependency graph
- Sets outputs — Provides
crates,matrix,count,docs-only,rebuild-all - Writes summary — Adds a summary to the GitHub Actions job
- cargo-rail — The CLI tool
- Configuration Reference
- Command Reference
MIT