Building a Fast and Reliable CI/CD Pipeline for Rust Crates
A complete guide to automating Rust crate testing, building, and publishing with optimized GitHub Actions.
Why This Matters
You’ve built a Rust crate. It works perfectly on your machine. But then comes the painful part: sharing it with the world.
Every Rust developer knows this frustrating cycle:
# The manual release dance (every single time)
cargo test # 5 minutes of waiting
cargo clippy --all-targets # 2 minutes of lint checking
cargo fmt --check # 30 seconds of format validation
cargo build --release # 8 minutes of compilation
cargo publish # Cross your fingers
git tag v0.1.2 && git push origin v0.1.2 # Manual version management
Total time: 15+ minutes of waiting, praying nothing fails.
But the real pain isn’t just the time. It’s the anxiety:
- “Did I update the version number correctly?” – You’ve tagged v1.2.3 but Cargo.toml still says 1.2.2
- “Are my examples still working?” – That README code snippet you wrote 3 months ago might not compile anymore
- “Will this break on Windows?” – Your Linux development machine doesn’t catch platform-specific issues
- “Did I introduce a security vulnerability?” – That new dependency might have known CVEs
- “What if crates.io is down?” – You’re publishing at 2 AM and the upload fails halfway through
One small mistake and you’re publishing broken code or the wrong version. Your users lose trust. You lose sleep. Your project’s reputation suffers.
The hidden costs:
- Developer time: 15+ minutes per release × 12 releases/year = 3+ hours of pure waiting
- Context switching: Breaking flow to babysit the release process
- Error recovery: Fixing mistakes takes even longer than doing it right
- Stress: The fear of breaking things makes you release less frequently
- Opportunity cost: Time not spent building features or fixing bugs
This manual process doesn’t scale. As your crate grows in popularity, the pressure to release quickly and reliably only increases. You need automation.
What We’ll Build
By the end of this guide, your workflow will look like this:
# Your new workflow
git tag v0.1.2
git push origin v0.1.2
# GitHub Actions automatically:
# ✅ Tests pass in 2 minutes (was 15 minutes)
# ✅ Publishes to crates.io
# ✅ Creates GitHub release
# ✅ Zero manual intervention
The secret: Build optimization + Smart automation
Part 1: The Speed Problem (And How to Fix It)
Why Rust CI is Slow (The Technical Reality)
Rust’s reputation for slow compilation isn’t just a meme—it’s a real productivity killer in CI/CD pipelines. To fix it, we need to understand exactly where the time goes.
Rust compilation has three distinct phases:
Phase 1: Dependency Resolution (30 seconds – 2 minutes)
# What happens here:
- Parse Cargo.toml and Cargo.lock
- Download crates from crates.io (network I/O)
- Verify checksums and signatures
- Build dependency graph
- Resolve version conflicts
Why it’s slow: Network latency and bandwidth limitations. If you have 50+ dependencies, that’s 50+ HTTP requests.
Optimization potential: High (caching eliminates this almost entirely)
Phase 2: Dependency Compilation (8-12 minutes – THE BOTTLENECK)
# What happens here:
- Compile each external crate in dependency order
- Generate machine code for your target platform
- Create .rlib files for static linking
- Apply optimizations (if release mode)
Why it’s the bottleneck:
- Volume: Modern Rust projects easily have 100+ transitive dependencies
- Complexity: Popular crates like
serde,tokio,synare large and complex - Redundancy: Same dependencies compiled from scratch every CI run
- Sequential constraints: Can’t compile
tokio-utiluntiltokiois done
The math is brutal:
serde: ~45 seconds to compilesyn: ~60 seconds to compiletokio: ~90 seconds to compileproc-macro2: ~30 seconds to compile- … × 50+ more dependencies = 8-12 minutes total
Optimization potential: Massive (99% of this can be cached)
Phase 3: Your Code Compilation (1-2 minutes)
# What happens here:
- Parse your Rust source files
- Type checking and borrow checking
- Macro expansion
- Code generation and optimization
- Link everything together
Why it’s relatively fast: Your code is typically much smaller than your dependencies.
Optimization potential: Moderate (incremental compilation helps)
The Insight That Changes Everything
Phase 2 rarely changes but gets recompiled every time.
Think about it: How often do you update your dependencies compared to how often you change your code?
- Your code changes: Every commit (multiple times per day)
- Dependencies change: Every few weeks or months
- Dependency compilation time: 8-12 minutes
- Times you recompile dependencies unnecessarily: Hundreds per month
This is pure waste. We’re spending 80% of our CI time recompiling code that hasn’t changed.
The solution isn’t faster hardware (though that helps). The solution is smart caching.
The Solution: Smart Caching (The Game Changer)
Smart caching isn’t just “make things faster”—it’s a fundamental shift in how we think about CI/CD for Rust projects.
The Traditional Approach (Wasteful):
Every CI run starts from zero, as if your dependencies have never been compiled before.
# What happens on EVERY CI run:
Downloading dependencies... 2 min
├─ serde v1.0.193 (1.2 MB)
├─ tokio v1.35.1 (2.8 MB)
├─ syn v2.0.48 (1.5 MB)
└─ ... 47 more crates
Compiling dependencies... 10 min
├─ Compiling proc-macro2 v1.0.70 ... 30s
├─ Compiling unicode-ident v1.0.12 ... 15s
├─ Compiling syn v2.0.48 ... 60s
├─ Compiling serde v1.0.193 ... 45s
├─ Compiling tokio v1.35.1 ... 90s
└─ ... 45 more crates ... 8m
Compiling your code... 2 min
├─ Type checking your 5,000 lines
├─ Borrow checking
├─ Code generation
└─ Linking
Total: 14 minutes
The Smart Caching Approach (Efficient):
Cache compiled dependencies based on Cargo.lock hash. When dependencies don’t change, reuse previous compilation results.
# First run (cache miss - expected):
Cache key: rust-deps-abc123def456 ... NOT FOUND
Downloading dependencies... 2 min
Compiling dependencies... 10 min
Compiling your code... 2 min
Saving cache: rust-deps-abc123def456
Total: 14 minutes
# Second run (cache hit - the magic):
Cache key: rust-deps-abc123def456 ... FOUND!
Restoring cached dependencies... 30 sec
├─ Extracting compiled .rlib files
├─ Restoring registry cache
└─ Updating timestamps
Compiling your code... 2 min
├─ Only YOUR code has changed
├─ Dependencies are pre-compiled
└─ Fast incremental linking
Total: 2.5 minutes (83% faster!)
The Cache Invalidation Strategy:
The cache key is based on Cargo.lock content hash. This means:
- ✅Cache hit: When you change your code but not dependencies
- ✅Cache miss: When you update dependencies (correct behavior)
- ✅Automatic cleanup: Old caches expire after 7 days
- ✅Cross-platform: Separate caches for Linux/Windows/macOS
Why This Works So Well:
In a typical Rust project lifecycle:
- 90% of commits: Change only your code (cache hit)
- 10% of commits: Update dependencies (cache miss, but necessary)
This means 90% of your CI runs complete in 2-3 minutes instead of 12-15 minutes.
The Compound Effect:
- Daily development: Fast feedback loop encourages more frequent commits
- Pull requests: Reviewers get results quickly, improving collaboration
- CI costs: 80% reduction in compute time = 80% reduction in CI bills
- Developer happiness: No more coffee breaks waiting for CI
Implementation: The Magic Configuration
Step 1: Create .cargo/config.toml
[build]
# Use default job count (all CPU cores)
# Faster linker on Linux
[target.x86_64-unknown-linux-gnu]
linker = "clang"
rustflags = ["-C", "link-arg=-fuse-ld=lld"]
# Faster linker on macOS
[target.x86_64-apple-darwin]
rustflags = ["-C", "link-arg=-fuse-ld=lld"]
# CI profile for fast builds
[profile.ci]
inherits = "dev"
opt-level = 1
debug = false
incremental = true
codegen-units = 16
Step 2: Optimized GitHub Actions
name: CI
on:
push:
branches: [ main, test-commit ]
pull_request:
branches: [ main, test-commit ]
env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: 1
RUST_BACKTRACE: 1
# Optimize for CI builds
RUSTFLAGS: "-C codegen-units=16 -C debuginfo=0"
jobs:
test:
name: Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- name: Install fast linker
run: sudo apt-get update && sudo apt-get install -y lld
- name: Setup sccache
uses: mozilla-actions/sccache-action@v0.0.4
- name: Rust Cache (Optimized)
uses: Swatinem/rust-cache@v2
with:
cache-targets: true
cache-all-crates: true
# Fastest checks first (fail fast)
- name: Check formatting
run: cargo fmt --all -- --check
- name: Run clippy
run: cargo clippy --all-targets --all-features -- -D warnings
- name: Build (optimized)
run: cargo build --profile ci
- name: Test (parallel)
run: |
cargo install cargo-nextest
cargo nextest run --profile ci
- name: Build examples
run: |
cargo build --examples --profile ci
cargo run --example simple_usage
cargo run --example configuration
- name: Show cache stats
run: sccache --show-stats
build:
name: Multi-platform Build
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
steps:
- uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Install fast linker (Linux/macOS)
if: runner.os != 'Windows'
run: |
if [ "$RUNNER_OS" = "Linux" ]; then
sudo apt-get update && sudo apt-get install -y lld
elif [ "$RUNNER_OS" = "macOS" ]; then
brew install llvm
fi
- name: Setup sccache
uses: mozilla-actions/sccache-action@v0.0.4
- name: Rust Cache
uses: Swatinem/rust-cache@v2
with:
cache-targets: true
- name: Build (optimized)
run: cargo build --profile ci
env:
CARGO_INCREMENTAL: 1
RUSTFLAGS: "-C codegen-units=16 -C debuginfo=0"
What You’ll See
First run (cache miss):
⚠️ Cache not found
🔄 Compiling dependencies... 8 min
✅ Build completed in 10 minutes
Second run (cache hit):
✅ Cache restored successfully
⚡ Using cached dependencies... 30 sec
✅ Build completed in 2 minutes
Performance improvement: 80% faster builds!
Part 2: The Automation Pipeline
Now that builds are fast, let’s automate everything. Speed without automation is just fast manual work—we want to eliminate the manual work entirely.
The Philosophy: Three Workflows, Three Purposes
Most developers try to cram everything into one giant workflow. This creates a mess: slow feedback, unclear failures, and maintenance nightmares.
Instead, we use three focused workflows, each with a single responsibility:
- Continuous Integration: Fast feedback on every change
- Security Audit: Proactive vulnerability detection
- Automated Release: Zero-error publishing
This separation provides:
- Fast feedback: CI runs in 2-3 minutes, not 15+ minutes
- Clear failures: When something breaks, you know exactly what and where
- Independent scaling: Each workflow can be optimized separately
- Maintenance simplicity: Small, focused workflows are easier to debug
The Three-Workflow System
1. Continuous Integration (ci.yml) – The Gatekeeper
Philosophy: Catch problems as early and as fast as possible.
- Triggers: Every push to any branch, every pull request
- Purpose: Prevent broken code from reaching main branch
- Target time: 2-4 minutes (with caching)
2. Security Audit (security.yml) – The Watchdog
Philosophy: Security is not optional, and vulnerabilities are discovered continuously.
- Triggers: Weekly schedule + every push to main
- Purpose: Detect vulnerable dependencies before they reach production
- Target time: 1-2 minutes
name: Security Audit
on:
schedule:
- cron: '0 0 * * 0' # Weekly on Sunday
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
security_audit:
name: Security Audit
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Install cargo-audit
run: cargo install cargo-audit
- name: Run security audit
run: cargo audit
3. Automated Release (release.yml) – The Publisher
Philosophy: Humans make mistakes. Automation doesn’t.
- Triggers: Git tags matching
v*pattern (v1.0.0, v1.2.3, etc.) - Purpose: Publish releases without human error
- Target time: 3-5 minutes
name: Release
on:
push:
tags:
- 'v*'
env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: 1
RUST_BACKTRACE: 1
RUSTFLAGS: "-C codegen-units=16 -C debuginfo=0"
jobs:
test:
name: Test before release
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- name: Install fast linker
run: sudo apt-get update && sudo apt-get install -y lld
- name: Setup sccache
uses: mozilla-actions/sccache-action@v0.0.4
- name: Rust Cache
uses: Swatinem/rust-cache@v2
with:
cache-targets: true
cache-all-crates: true
- name: Check formatting
run: cargo fmt --all -- --check
- name: Run clippy
run: cargo clippy --all-targets --all-features -- -D warnings
- name: Build (optimized)
run: cargo build --profile ci
- name: Run tests (parallel)
run: |
cargo install cargo-nextest
cargo nextest run --profile ci
- name: Show cache stats
run: sccache --show-stats
publish:
name: Publish to crates.io
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Rust Cache
uses: Swatinem/rust-cache@v2
with:
cache-targets: true
- name: Verify version matches tag
run: |
TAG_VERSION=${GITHUB_REF#refs/tags/v}
CARGO_VERSION=$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[0].version')
if [ "$TAG_VERSION" != "$CARGO_VERSION" ]; then
echo "Tag version ($TAG_VERSION) does not match Cargo.toml version ($CARGO_VERSION)"
exit 1
fi
- name: Publish to crates.io
run: cargo publish --token ${{ secrets.CRATES_IO_TOKEN }}
create-release:
name: Create GitHub Release
runs-on: ubuntu-latest
needs: publish
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- name: Create Release
run: |
gh release create ${{ github.ref_name }}
--title "Release ${{ github.ref_name }}"
--notes "## Changes
See [CHANGELOG.md](CHANGELOG.md) for details.
## Installation
```toml
[dependencies]
custom-tracing-logger = "${{ github.ref_name }}"
```"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Why this release process is bulletproof:
Version Validation: The #1 cause of release failures is version mismatches:
- Git tag says
v1.2.3 - Cargo.toml says
version = "1.2.2" - Result: Confusion, failed releases, manual cleanup
Our solution: Automatic validation that fails fast with clear error messages.
Atomic Publishing: Either everything succeeds, or nothing is published:
- Validate → If this fails, nothing happens
- Publish → If this fails, no GitHub release is created
- GitHub Release → If this fails, crate is still published (acceptable)
Part 3: The Developer Experience
Your New Workflow
Daily development:
# Write code
git add .
git commit -m "Add awesome feature"
git push
# CI runs automatically (2-3 minutes)
# ✅ All checks pass
Releasing:
# 1. Update version in Cargo.toml
version = "1.0.1"
# 2. Update CHANGELOG.md
## [1.0.1] - 2025-01-15
### Fixed
- Critical bug in authentication
# 3. Commit and tag
git add .
git commit -m "Release v1.0.1"
git tag v1.0.1
git push origin main v1.0.1
# 4. Automation takes over:
# ✅ Runs full test suite (2 minutes)
# ✅ Validates version consistency
# ✅ Publishes to crates.io
# ✅ Creates GitHub release
# ✅ Updates docs.rs
Total release time: 2 minutes (was 30+ minutes)
Local Testing (Optional but Recommended)
Create scripts/test-ci.ps1:
#!/usr/bin/env pwsh
Write-Host "⚡ Running local CI..." -ForegroundColor Blue
# Fast checks first
cargo fmt --all -- --check
if ($LASTEXITCODE -ne 0) { exit 1 }
cargo clippy --all-targets -- -D warnings
if ($LASTEXITCODE -ne 0) { exit 1 }
# Optimized build and test
$env:RUSTFLAGS = "-C codegen-units=16"
cargo build --profile ci
cargo nextest run --profile ci
Write-Host "✅ All checks passed!" -ForegroundColor Green
Run before pushing:
./scripts/test-ci.ps1 # 30 seconds locally
git push # CI passes immediately
Part 4: Setup Guide
Prerequisites
- GitHub repository with your Rust crate
- crates.io account and API token
- 5 minutes to set up
Step-by-Step Setup
1. Create the configuration (2 minutes)
Create .cargo/config.toml:
[build]
[target.x86_64-unknown-linux-gnu]
linker = "clang"
rustflags = ["-C", "link-arg=-fuse-ld=lld"]
[target.x86_64-apple-darwin]
rustflags = ["-C", "link-arg=-fuse-ld=lld"]
[profile.ci]
inherits = "dev"
opt-level = 1
debug = false
incremental = true
codegen-units = 16
2. Add GitHub Actions (2 minutes)
Create .github/workflows/ci.yml with the optimized CI from Part 1.
Create .github/workflows/security.yml with the security audit.
Create .github/workflows/release.yml with the automated release.
3. Configure secrets (1 minute)
- Go to https://crates.io/me
- Click “New Token”
- Copy the token
- In GitHub: Settings → Secrets → New repository secret
- Name:
CRATES_IO_TOKEN, Value: your token
4. Test it
git add .
git commit -m "Add optimized CI/CD pipeline"
git push
# Watch the magic happen in GitHub Actions tab
Part 5: Results and Troubleshooting
Expected Performance
| Scenario | Before | After | Improvement |
|---|---|---|---|
| First CI run | 15 min | 10 min | 33% faster |
| Subsequent runs | 15 min | 2-3 min | 80% faster |
| Release process | 30 min | 2 min | 93% faster |
What You’ll See in GitHub Actions
Cache miss (first run or after dependency changes):
⚠️ Warning: Cache not found for keys: v0-rust-test-Linux-x64-abc123
🔄 Downloading and compiling dependencies...
✅ Build completed in 10m 30s
Cache hit (normal case):
✅ Restoring cache from key: v0-rust-test-Linux-x64-abc123
✅ Cache restored successfully
⚡ Compiling only your changes...
✅ Build completed in 2m 15s
sccache stats:
Compile requests: 245
Cache hits: 198 (80.8%) ← This is what you want to see
Cache misses: 47 (19.2%)
Common Issues and Solutions
“Cache not found” every time:
- ✅ Ensure
Cargo.lockis committed to git - ✅ Check that cache key uses
Cargo.lockhash - ✅ Verify consistent OS in job matrix
Build still slow despite cache:
- ✅ Check sccache hit rate (should be >70%)
- ✅ Verify fast linker is installed
- ✅ Ensure CI profile is being used
Release fails with version mismatch:
- ✅ Update version in
Cargo.tomlbefore tagging - ✅ Ensure tag format is
v1.0.0(with ‘v’ prefix) - ✅ Check that both versions match exactly
Conclusion: The Compound Benefits
This isn’t just about speed. It’s about:
Developer Velocity:
- Push code confidently (fast feedback)
- Release frequently (no overhead)
- Focus on features (not process)
Code Quality:
- Consistent formatting (automated)
- No lint violations (enforced)
- Security vulnerabilities caught early
User Trust:
- Reliable releases (no human error)
- Faster bug fixes (easy to release)
- Professional appearance (automated releases)
Next Steps
- Implement the optimizations (start with caching)
- Measure your improvements (before/after timing)
- Share with your team (everyone benefits)
- Iterate and improve (add more optimizations over time)
The best CI/CD pipeline is one you don’t think about. It just works, fast and reliably, letting you focus on what matters: building great software.
Happy shipping! 🚀