iTranslated by AI
Building a Rust CI/CD Pipeline with GitHub Actions, Including Automated Releases
Introduction
To write high-quality code, an effective CI/CD mechanism is essential. When looking up GitHub Actions workflows on the internet, I found that many were using deprecated tools. Therefore, I reconstructed the workflow and would like to share the method here.
What can be achieved
- Automation with GitHub Actions
- Execute tests, linting, and format checks across multiple OSs during pull requests
- Allow merging only when all checks pass
- Execute tests, linting, and format checks across multiple OSs during pull requests
- Automation of testing and building for each OS
- Testing on each OS allows pre-identifying OS-dependent issues
- Automatically generate binaries for each OS during a Release
- Release automation
- Trigger tests and builds upon a GitHub Release
- Attach the built binaries as artifacts
- OpenSSL integration support
- Properly handle OpenSSL dependencies, which is often a bottleneck in Rust projects
- Broad Linux compatibility
- Build for arm64/x86_64 in a CentOS 7 environment
- Operable in most Linux environments due to low glibc version requirements
- Build acceleration
- Reduce processing time by utilizing Rust build caches
What cannot be achieved
- Fundamental improvement of Rust build speeds
- Even with parallel workflows, there's a limit to speeding up inherently time-consuming build processes
- There is inherent processing time that cannot be resolved even by utilizing caches
- In particular, Windows builds are extremely slow
- There is inherent processing time that cannot be resolved even by utilizing caches
- Even with parallel workflows, there's a limit to speeding up inherently time-consuming build processes
- Binary signing
- Since the author does not possess keys for signing, binary signing is not performed
Prerequisites
- Knowledge of GitHub and GitHub Actions
- Basic knowledge of Rust
Workflow Construction
The directory structure of the sample project used this time is as follows.
Also, the GitHub repository for this project is here:
https://github.com/Walkmana-25/rust-actions-example
sh-3.2$ tree -C -a -I "target" -I ".git"
.
|-- .github
| `-- workflows
| |-- release.yml # Workflow triggered by GitHub Release
| `-- testing.yml # Workflow executed during Pull Requests
|-- .gitignore
|-- Cargo.lock
|-- Cargo.toml
|-- Cross.toml # Configuration file for cross-compilation on Linux
|-- LICENSE
|-- README.md
|-- doc.md
`-- src # Source of the sample project used this time
|-- args.rs
|-- main.rs
|-- utils.rs
`-- weather.rs
4 directories, 13 files
Overview of the Sample Project
I have built a sample project for explanation purposes. This project is a weather information retrieval command-line application developed in Rust.
Key Features
- Retrieve weather data for specified latitude and longitude
- Process and display retrieved data
- Hourly weather information every 6 hours
- Comparison of average temperatures between today and yesterday
Technical Characteristics
- HTTP request processing using the
reqwestlibrary - Proper management of OpenSSL dependencies
- Compile OpenSSL from source using the
vendoredfeature
- Compile OpenSSL from source using the
- Implementation of simple test code
Actual Operation Example of the Sample Project
sh-3.2$ ./rust-actions-example --latitude 35.41 --longitude 139.45 # Latitude and longitude of Chiyoda-ku, Tokyo
Fetching weather data for latitude: 35.41, longitude: 139.45...
--- Hourly Weather Data (Every 6 Hours) ---
2025-05-07 00:00: 19.9°C
2025-05-07 06:00: 21.0°C
2025-05-07 12:00: 16.1°C
2025-05-07 18:00: 13.1°C
2025-05-08 00:00: 16.8°C
2025-05-08 06:00: 19.9°C
2025-05-08 12:00: 15.1°C
2025-05-08 18:00: 13.1°C
--- Average Temperatures ---
Today's average temperature: 16.13°C
Yesterday's average temperature: 17.34°C
Creating testing.yml
First, create testing.yml. This file is the workflow executed during Pull Requests.
Basic Configuration of the Testing Workflow
name: Rust Testing
on:
pull_request:
push:
branches: [ main ]
workflow_dispatch:
workflow_call:
env:
CARGO_TERM_COLOR: always
jobs:
# Define jobs below
Implementation of Code Quality Checks
In the first job, we run two linters, clippy and fmt, to perform static analysis of the code.
check:
name: Rustfmt
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Cache Rust dependencies
uses: Swatinem/rust-cache@v2
- name: Install Rust fmt
run: rustup component add rustfmt
- name: Install Rust clippy
run: rustup component add clippy
- name: Run rustfmt
run: cargo fmt --all -- --check
- name: Run clippy
run: cargo clippy --all-targets --all-features -- -D warnings
In this job, the following processes are being performed:
- Reduce build time by caching dependencies using the
Swatinem/rust-cache@v2action - Verification of code style with rustfmt
- Static analysis with clippy (treating warnings as errors using the
-D warningsoption)
Implementation of Multi-platform Tests
In the next job, we execute builds and tests on multiple OSs.
test:
name: Test
runs-on: ${{ matrix.os }}
needs: ["check"]
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@v3
- name: Cache Rust dependencies
uses: Swatinem/rust-cache@v2
- name: Test Build
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose
The features of this job are as follows:
- Execution is triggered only after the code quality check succeeds, thanks to
needs: ["check"] - Uses a matrix to run tests concurrently in three environments: Ubuntu, macOS, and Windows
- Executes
cargo buildandcargo teston each platform
- Executes
- You can change the target platforms for testing by modifying the matrix description.
Creating release.yml
name: Release
permissions:
contents: write
on:
push:
tags:
- v*
env:
CARGO_TERM_COLOR: always
This workflow is triggered when a tag starting with v (e.g., v1.0.0) is pushed. In the permissions section, the write permissions necessary to upload files to GitHub Releases are configured.
Job Coordination and Test Execution
jobs:
Test:
uses: ./.github/workflows/testing.yml
build:
needs: [Test]
# Omitted below
As the first step of the release process, the test workflow defined in a separate file (testing.yml) is executed. The needs: [Test] setting ensures that the build job is executed only if the tests succeed.
Multi-platform Support via Matrix Builds
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-unknown-linux-gnu
extension: ""
runner: ubuntu-latest
cross: true
- target: x86_64-pc-windows-msvc
extension: ".exe"
runner: windows-latest
cross: false
# Other target configurations...
The true value of the matrix strategy is demonstrated here. The following information is defined for each target environment:
- target: Rust target triple
-
extension: Executable file extension (
.exefor Windows) - runner: GitHub environment to run the build
-
cross: Whether cross-compilation using
crossis required
By setting fail-fast: false, other builds will continue even if one fails, allowing for the provision of binaries for as many platforms as possible.
Build Process Based on the Environment
steps:
# Checkout and dependency installation are omitted
- name: Install cross (if needed)
if: ${{ matrix.cross }}
run: cargo install cross --git <https://github.com/cross-rs/cross>
- name: Build Project on cross
if: ${{ matrix.cross }}
run: |
cross build --release --target ${{ matrix.target }} --verbose
- name: Build Project
if: ${{ !matrix.cross }}
run: |
rustup target add ${{ matrix.target }}
cargo build --release --target ${{ matrix.target }} --verbose
The build process varies depending on the environment:
- If cross-compilation is required (e.g., building for arm64 on a Linux runner), the
crosstool is used. - For native builds, the target is added with
rustupbefore runningcargo build.
This branching allows selecting the optimal build method for each environment.
Management and Release of Artifacts
- name: Rename Artifacts
shell: bash
run: |
mv target/${{ matrix.target }}/release/${{ env.PROJECT_NAME }}{,-${{ github.ref_name }}-${{ matrix.target }}${{ matrix.extension }}}
- name: Release Artifacts
uses: softprops/action-gh-release@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
files: |
target/${{ matrix.target }}/release/${{ env.PROJECT_NAME }}-${{ github.ref_name }}-${{ matrix.target }}${{ matrix.extension }}
In the final step, the artifacts are uploaded to GitHub Releases using the softprops/action-gh-release action.
As a result of this workflow, binaries for each platform are automatically attached to the release page, allowing users to easily download the binary that matches their environment.
cross.toml
In this setup, we use cross when building for Linux targets:
[target.x86_64-unknown-linux-gnu]
image = "ghcr.io/cross-rs/x86_64-unknown-linux-gnu:main-centos"
pre-build = [
"sed -i /etc/yum.repos.d/*.repo -e 's!^mirrorlist!#mirrorlist!' -e 's!^#baseurl=http://mirror.centos.org/!baseurl=https://vault.centos.org/!'",
"sed -i 's/enabled=1/enabled=0/' /etc/yum/pluginconf.d/fastestmirror.conf",
"yum update -y && yum install -y gcc perl make perl-IPC-Cmd"
]
# Other target definitions...
Container selection considering compatibility
For x86_64 and aarch64 targets, we specify CentOS 7-based images. Since CentOS 7 uses an older version of glibc, the generated binaries will work on both newer and older Linux distributions. This approach is also utilized for cargo's own builds.
Repository and Package Management
A point to note when using CentOS 7, which has reached EOL (End of Life), is that repository configuration changes are required:
# Change mirrorlist to Vault archive
sed -i /etc/yum.repos.d/*.repo -e 's!^mirrorlist!#mirrorlist!' -e 's!^#baseurl=http://mirror.centos.org/!baseurl=https://vault.centos.org/!'
# Disable fastestmirror plugin (to avoid issues)
sed -i 's/enabled=1/enabled=0/' /etc/yum/pluginconf.d/fastestmirror.conf
Target-Specific Settings
We take a different approach for the armv7 architecture:
[target.armv7-unknown-linux-gnueabihf]
pre-build = [
"apt-get update && apt-get install -y crossbuild-essential-armhf",
]
For this target, we use a Debian-based image and install the cross-compilation toolchain for ARM (as a CentOS 7 image for armv7 was unavailable).
Dependencies for OpenSSL Build
The reason we install packages like gcc, perl, and make for all targets is that they are required for building OpenSSL. This ensures that programs including encryption features can be cross-compiled without any issues.
Results
Here are the results when a Pull Request is created or a Release is performed on GitHub.
Pull Request
When you create a Pull Request, tests are automatically executed like this.
https://github.com/Walkmana-25/rust-actions-example/actions/runs/14812038654

Release
When you create a Release, building and releasing are performed automatically as follows.
https://github.com/Walkmana-25/rust-actions-example/actions/runs/14812067267

https://github.com/Walkmana-25/rust-actions-example/releases/tag/v0.0.6

Summary
In this article, I explained how to build a practical CI/CD pipeline using GitHub Actions for Rust projects. The key points are as follows:
-
Efficient Test Automation
- Automatic test execution on multiple OSs during Pull Requests.
- Automation of code quality checks (rustfmt, clippy).
-
Implementation of Cross-platform Builds
- Builds for multiple platforms (Linux, macOS, Windows, ARM) using a matrix.
- Construction of cross-compilation environments using the
crosstool.
-
Release Automation
- Automatic release flow triggered by tag pushes.
- Automatic generation and attachment of binaries for multiple platforms.
-
Ensuring Compatibility
- Realization of broad Linux compatibility through CentOS 7 base images.
- Proper management of OpenSSL dependencies.
I hope this workflow is helpful to someone.
Discussion