NashTech Blog

Building a Fast and Reliable CI/CD Pipeline for Rust Crates

Table of Contents

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, syn are large and complex
  • Redundancy: Same dependencies compiled from scratch every CI run
  • Sequential constraints: Can’t compile tokio-util until tokio is done

The math is brutal:

  • serde: ~45 seconds to compile
  • syn: ~60 seconds to compile
  • tokio: ~90 seconds to compile
  • proc-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:

  1. Continuous Integration: Fast feedback on every change
  2. Security Audit: Proactive vulnerability detection
  3. 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:

  1. Validate → If this fails, nothing happens
  2. Publish → If this fails, no GitHub release is created
  3. 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

  1. GitHub repository with your Rust crate
  2. crates.io account and API token
  3. 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)

  1. Go to https://crates.io/me
  2. Click “New Token”
  3. Copy the token
  4. In GitHub: Settings → Secrets → New repository secret
  5. 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.lock is committed to git
  • ✅ Check that cache key uses Cargo.lock hash
  • ✅ 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.toml before 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

  1. Implement the optimizations (start with caching)
  2. Measure your improvements (before/after timing)
  3. Share with your team (everyone benefits)
  4. 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! 🚀


Resources:

Picture of Hoang Vo Huy

Hoang Vo Huy

Leave a Comment

Your email address will not be published. Required fields are marked *

Suggested Article

Scroll to Top