November 25, 2024 by Hugh Kaznowski7 minutes
In this post, you will learn how to release your Rust project while conveniently handling release versions.
When building software, especially in Rust, creating reliable and consistent releases is essential. In this blog, we’ll explore how to set up a robust release workflow using GitHub Actions, focusing on semantic versioning and single-click releases. While this workflow was designed for RapidRecast, a high-performance Rust-based streaming engine and gateway, it is highly practical for any Rust project.
By following these steps, you’ll ensure a smooth, automated pipeline for releasing software, making version management a breeze.
A software release tracks specific versions of your code so you can identify which features, bug fixes, or breaking changes your users are running. For example, when a user says they’re on version “1.2.3,” you instantly know the state of your software, including potential bugs or fixes.
A release workflow automates the steps required to produce valid, consistent releases of your software. It ensures every release adheres to your quality standards and is easily reproducible, reducing manual effort and potential errors.
Rust, along with many other software ecosystems, uses semantic versioning to communicate expectations about releases.
Semantic versioning breaks a version number into three parts: Major.Minor.Patch (e.g., 1.2.3).
Each part signifies a different level of change in your software.
1.2.3 → 1.2.41.2.3 → 1.3.01.2.3 → 2.0.0When retiring features, it’s best practice to provide a deprecation notice in earlier versions, giving users time to adapt to the new way of doing things.
To streamline the release pipeline of RapidRecast, I’ve designed a flexible GitHub Actions workflow that simplifies the process into a single click. This workflow is applicable to most Rust projects, not just RapidRecast, and automates versioning, building, and releasing your software.
RapidRecast currently distributes binaries rather than crates.
However, if your Rust project involves crate publishing, you can adapt the workflow by modifying the Update Cargo.toml version and push to GitHub step during the pre-release job.
The full workflow can be found on the RapidRecast Github Blog Repository.
name: Release to Github
on:
  workflow_dispatch:
    inputs:
      type:
        description: 'Type of release (major/minor/patch)'
        required: true
        default: 'minor'
      dry_run:
        description: 'Dry run (true/false)'
        required: true
        default: true
      skip_tests:
        description: 'Skip tests (true/false)'
        required: true
        default: false
env:
  CARGO_TERM_COLOR: always
jobs:
  test:
    strategy:
      matrix:
        os: [ ubuntu-latest, macos-13, macos-14, windows-latest ]
    runs-on: ${{ matrix.os }}
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      - name: Ensure Cargo Directories Exist for Cache Restore
        run: |
          mkdir -p ~/.cargo/registry
          mkdir -p ~/.cargo/index
          mkdir -p target          
        shell: bash
      - name: Cache Cargo Registry, Index, Build
        uses: actions/cache@v3
        with:
          path: |
            ~/.cargo/registry
            ~/.cargo/index
            target            
          key: ${{ runner.os }}-${{ runner.arch }}-cargo-${{ hashFiles('**/Cargo.toml') }}
          restore-keys: |
            cargo-build-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/Cargo.toml') }}
            cargo-build-${{ runner.os }}-${{ runner.arch }}-            
      - name: Set up Rust
        uses: actions-rs/toolchain@v1
        with:
          toolchain: stable
      - name: Build
        run: cargo build --verbose
      - name: Run Tests
        if: ${{ github.event.inputs.skip_tests == 'false' }}
        run: cargo test --verbose
  prepare-release:
    needs: test
    runs-on: ubuntu-latest
    outputs:
      rr_cargo_version: ${{ steps.get-version.outputs.VERSION }}
      workflow_git_tag: ${{ steps.get-version.outputs.WORKFLOW_GIT_TAG }}
    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0
          token: ${{ secrets.RELEASE_TOKEN }}
      - name: Ensure Cargo Directories Exist for Cache Restore
        run: |
          mkdir -p ~/.cargo/registry
          mkdir -p ~/.cargo/index
          mkdir -p target          
        shell: bash
      - name: Cache Cargo Registry, Index, Build
        uses: actions/cache@v3
        with:
          path: |
            ~/.cargo/registry
            ~/.cargo/index
            target            
          key: ${{ runner.os }}-${{ runner.arch }}-cargo-${{ hashFiles('**/Cargo.toml') }}
          restore-keys: |
            cargo-build-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/Cargo.toml') }}
            cargo-build-${{ runner.os }}-${{ runner.arch }}-            
      - name: Cache Cargo Binaries
        uses: actions/cache@v3
        with:
          path: ~/.cargo/bin
          key: cargo-bin-${{ runner.os }}-${{ runner.arch }}-v1
          restore-keys: |
            cargo-bin-${{ runner.os }}-${{ runner.arch }}-            
      - name: Set up Rust
        uses: actions-rs/toolchain@v1
        with:
          toolchain: stable
      - name: Install cargo-release
        run: |
          if ! command -v cargo-release &> /dev/null; then
            echo "Installing cargo-release..."
            cargo install cargo-release
          else
            INSTALLED_VERSION=$(cargo release --version || echo "unknown")
            echo "cargo-release already installed (version: $INSTALLED_VERSION). Skipping installation."
          fi          
      - name: Configure Git
        run: |
          git config --global user.name "GitHub Action"
          git config --global user.email "action@github.com"          
      - name: Update Cargo.toml version and push to GitHub
        run: |
          REL_TYPE=${{ github.event.inputs.type }}
          DRY_RUN=${{ github.event.inputs.dry_run }}
          # Execute version update
          if [ "$DRY_RUN" = "false" ]; then
            echo "Updating version in Cargo.toml"
            cargo release --verbose --execute --no-confirm $REL_TYPE --no-publish --no-verify
          else
            echo "Dry run: showing changes without executing"
            cargo release --verbose $REL_TYPE --no-publish --no-verify
          fi          
      - name: Get Version from Cargo.toml
        id: get-version
        run: |
          VERSION=$(grep '^version = ' Cargo.toml | sed -E 's/version = "(.*)"/\1/')
          echo "VERSION=$VERSION" >> "$GITHUB_OUTPUT"
          echo "WORKFLOW_GIT_TAG=v$VERSION" >> "$GITHUB_OUTPUT"          
  release:
    needs: prepare-release
    if: ${{ github.event.inputs.dry_run == 'false' }}
    strategy:
      matrix:
        os: [ ubuntu-latest, macos-13, macos-14, windows-latest ]
    runs-on: ${{ matrix.os }}
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      - name: Ensure Cargo Directories Exist for Cache Restore
        run: |
          mkdir -p ~/.cargo/registry
          mkdir -p ~/.cargo/index
          mkdir -p target          
        shell: bash
      - name: Cache Cargo Registry, Index, Build
        uses: actions/cache@v3
        with:
          path: |
            ~/.cargo/registry
            ~/.cargo/index
            target            
          key: ${{ runner.os }}-${{ runner.arch }}-cargo-${{ hashFiles('**/Cargo.toml') }}
          restore-keys: |
            cargo-build-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/Cargo.toml') }}
            cargo-build-${{ runner.os }}-${{ runner.arch }}-            
      - name: Set up Rust
        uses: actions-rs/toolchain@v1
        with:
          toolchain: stable
      - name: Build Release
        run: cargo build --release --verbose
      - name: Copy release binary to root
        shell: bash
        run: |
          if [[ -f "target/release/rapidrecast" ]]; then
            cp target/release/rapidrecast .
          elif [[ -f "target/release/rapidrecast.exe" ]]; then
            cp target/release/rapidrecast.exe .
          else
            echo "No binary to copy for this OS."
          fi          
      - name: Get Build Info
        id: build-info
        shell: bash
        run: |
          OS=$(uname -s | tr '[:upper:]' '[:lower:]')
          ARCH=$(uname -m)
          TARGET=""
          case "$OS" in
            linux) TARGET="$ARCH-unknown-linux-gnu" ;;
            darwin) TARGET="$ARCH-apple-darwin" ;;
            msys*|cygwin*|mingw*) TARGET="$ARCH-pc-windows-msvc" ;;
          esac
          VERSION=$(grep '^version = ' Cargo.toml | sed -E 's/version = "(.*)"/\1/')
          FILENAME="rapidrecast-v${VERSION}-${TARGET}"
          echo "OS=$OS" >> "$GITHUB_ENV"
          echo "ARCH=$ARCH" >> "$GITHUB_ENV"
          echo "TARGET=$TARGET" >> "$GITHUB_ENV"
          echo "FILENAME=$FILENAME" >> "$GITHUB_ENV"          
      - name: Compress tar.gz
        uses: ksm2/archive-action@v1
        with:
          name: "${{ env.FILENAME }}"
          format: "tar.gz"
          include: "{rapidrecast,rapidrecast.exe,README.md,LICENSE}"
      - name: Compress zip
        uses: ksm2/archive-action@v1
        with:
          name: "${{ env.FILENAME }}"
          format: "zip"
          include: "{rapidrecast,rapidrecast.exe,README.md,LICENSE}"
      - name: Create or Update Release
        env:
          VERSION: ${{ needs.prepare-release.outputs.rr_cargo_version }}
          WORKFLOW_GIT_TAG: ${{ needs.prepare-release.outputs.workflow_git_tag}}
        uses: ncipollo/release-action@v1
        with:
          artifacts: "${{ env.FILENAME }}.tar.gz,${{ env.FILENAME }}.zip"
          allowUpdates: 'true'
          generateReleaseNotes: 'true'
          token: ${{ secrets.RELEASE_TOKEN }}
          tag: ${{ env.WORKFLOW_GIT_TAG }}Follow these simple steps to integrate and use the workflow:
Save the workflow file in your repository under .github/workflows/release.yml.
Create a PAT that allows the workflow to perform pushes, make tags, and perform releases for you.
Store the PAT in the project secrets as RELEASE_TOKEN.
RapidRecast is a high-performance streaming engine and gateway written in Rust. It simplifies integrating with existing streaming workflows and makes adopting streaming technology significantly easier. Given that I am a single employee of the company, I need a no-hassle way to do releases. This workflow was designed to conveniently rigorously fit the demands of RapidRecast’s release process while remaining practical for any Rust project.
A well-structured release workflow is critical for maintaining a reliable software development pipeline. By using this GitHub Actions workflow, you can automate your Rust release process, adhere to semantic versioning standards, and deliver a seamless experience for your users.
Whether you’re managing a complex product like RapidRecast or a simple Rust library, this workflow provides a solid foundation for repeatable and reliable releases.
Happy releasing!