Rust CI with GitHub Actions and cargo test
The most surprising thing about setting up CI for Rust projects is how often the simplest configuration reveals the deepest assumptions about your project’s environment.
Let’s get a basic GitHub Actions workflow running for your Rust project. We’ll focus on cargo test as the core validation step.
First, create a .github/workflows directory in your project’s root. Inside that, create a file named rust.yml (or any name you prefer, ending in .yml or .yaml).
name: Rust CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Rust
uses: dtolnay/rust-toolchain@stable
- name: Build
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose
This workflow does a few things:
name: Rust CI: Gives your workflow a human-readable name.on: [push, pull_request]: Triggers the workflow when you push to your repository or open a pull request.jobs:: Defines one or more jobs that will run. We have a single job namedbuild.runs-on: ubuntu-latest: Specifies the operating system environment for the job.ubuntu-latestis a common and well-supported choice.steps:: A sequence of tasks to perform within the job.uses: actions/checkout@v4: This action checks out your repository’s code so the workflow can access it.name: Set up Rust: This uses a popular community action (dtolnay/rust-toolchain) to install a specific Rust toolchain.stablewill install the latest stable release. You could also specify1.70.0orbetaetc.name: Build: This runscargo build --verbose.--verbosemakes the output more detailed, which can be helpful for debugging. This step checks if your code compiles.name: Run tests: This executescargo test --verbose. If any of your tests fail, this step will fail, and the entire workflow will be marked as failed.
Commit this file to your repository. Now, whenever you push or open a PR, you’ll see a "Rust CI" status check.
Let’s imagine your project has a dependency that needs a specific system library, and cargo test starts failing with an error like:
error: failed to run custom build command for `openssl-sys v0.9.78`
Caused by:
process didn't exit successfully: `.../target/release/build/openssl-sys-xxxx/build-script-main` (exit code: 1)
--- stdout
...
--- stderr
/usr/bin/ld: cannot find -lssl
/usr/bin/ld: cannot find -lcrypto
...
This is a common scenario where a Rust crate depends on underlying C libraries. The openssl-sys crate, for example, needs the OpenSSL development libraries installed on the system. GitHub Actions runners are minimal by default. To fix this, you need to install the required development headers and libraries. For Debian/Ubuntu based runners like ubuntu-latest, you’d use apt-get.
You can add a step to install these packages before building or testing:
name: Rust CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Rust
uses: dtolnay/rust-toolchain@stable
- name: Install OpenSSL dev headers
run: sudo apt-get update && sudo apt-get install -y libssl-dev
- name: Build
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose
The added step name: Install OpenSSL dev headers uses apt-get to install libssl-dev. This provides the necessary libssl.so and libcrypto.so files, along with their header files, that openssl-sys needs to link against during its build process. The sudo apt-get update is crucial to ensure the package list is current before attempting installation.
The cargo test --verbose command, when it passes, not only confirms your code compiles and your tests pass but also verifies that any external C libraries required by your Rust dependencies are correctly installed and accessible in the CI environment.
A subtle but important aspect of cargo test is its handling of test output. By default, cargo test runs tests in parallel. While this speeds up execution, it can sometimes interleave the output from different tests, making it hard to read. If you encounter a test failure where the output is garbled or hard to follow, adding the --no-fail-fast and -- --test-threads=1 flags to your cargo test command in the workflow can help. The --no-fail-fast flag ensures all tests run to completion even if one fails, and -- --test-threads=1 (the -- passes arguments to the test binary itself) serializes test execution, producing cleaner, sequential output.
- name: Run tests
run: cargo test --verbose -- --test-threads=1
This change makes the output of each test run sequentially, which is invaluable for debugging intermittent test failures or understanding complex error messages.
The next logical step is to start caching Rust dependencies to speed up subsequent CI runs.