iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article

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
  • 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
  • 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 reqwest library
  • Proper management of OpenSSL dependencies
    • Compile OpenSSL from source using the vendored feature
  • 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@v2 action
  • Verification of code style with rustfmt
  • Static analysis with clippy (treating warnings as errors using the -D warnings option)

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 build and cargo test on each platform
  • 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 (.exe for Windows)
  • runner: GitHub environment to run the build
  • cross: Whether cross-compilation using cross is 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:

  1. If cross-compilation is required (e.g., building for arm64 on a Linux runner), the cross tool is used.
  2. For native builds, the target is added with rustup before running cargo 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
pull_request

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
release
https://github.com/Walkmana-25/rust-actions-example/releases/tag/v0.0.6
artifact

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:

  1. Efficient Test Automation

    • Automatic test execution on multiple OSs during Pull Requests.
    • Automation of code quality checks (rustfmt, clippy).
  2. Implementation of Cross-platform Builds

    • Builds for multiple platforms (Linux, macOS, Windows, ARM) using a matrix.
    • Construction of cross-compilation environments using the cross tool.
  3. Release Automation

    • Automatic release flow triggered by tag pushes.
    • Automatic generation and attachment of binaries for multiple platforms.
  4. 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.

References

GitHubで編集を提案

Discussion