Github Actions have been a great advantage since they were added, and have only grown in value as the number of good quality building blocks increases. At the same time, Rust has a great toolchain and I know I copied some example Rust action and with a little editing I had the following.

name: Rust

on: [push]

jobs:
  build:
    strategy:
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]

    runs-on: ${{ matrix.os }}

    steps:
      - uses: actions/checkout@v1

      - name: Install dependencies
        run: rustup component add rustfmt

      - name: Format
        run: cargo fmt -- --check

      - name: Build
        run: cargo build --verbose

      - name: Run tests
        run: cargo test --all-features --verbose

      - name: Docs
        run: cargo doc --no-deps

Fantastic, so I copied that into a bunch of projects and was pretty happy. But felt like there was some complexity missing, for a start what tests am I running, am I running benchmarks and/or examples? Do I run coverage at the same time or separately from my main tests? I spent some time on a couple of projects evolving my Rust workflow (which I still call pipelines), using jobs, dependencies, and some inputs/outputs between jobs to get too this:

Action Screenshot

This new workflow is clearly a lot more complex but it has some nice separation of concerns and allows quite a bit of parallelism. My test matrix for a start has become a lot more interesting, I now combine not just the platforms, but toolchain channel and test features. This last addition was useful in finding a stupid bug that only showed up with --no-default-features.

matrix:
  os: ["ubuntu-latest", "windows-latest", "macos-latest"]
  rust: ["stable", "beta", "nightly"]
  test-features: ["", "--all-features", "--no-default-features"]

Another feature that took a little time to figure out was the turning on/off the execution of examples and benchmarks. I want them to run if the corresponding directory exists, but don’t want to have all manner of conditionals in scripts that fake success if there are no tests. So, I added the following simple job which generates a flag for each directory and can then gate subsequent tests based on these flags. Specifically the two downstream jobs have the following: if: needs.check_tests.outputs.has_benchmarks or ...has_examples.

check_tests:

  name: Check for test types
  runs-on: ubuntu-latest
  
  outputs:
    has_benchmarks: ${{ steps.check_benchmarks.outputs.has_benchmarks }}
    has_examples: ${{ steps.check_examples.outputs.has_examples }}
    
  steps:
    - name: Check for benchmarks
      id: check_benchmarks
      run: test -d benchmarks && echo "has_benchmarks=1" || echo "has_benchmarks=" >> $GITHUB_OUTPUT
      shell: bash
      
    - name: Check for examples
      id: check_examples
      run: test -d examples && echo "has_examples=1" || echo "has_examples=" >> $GITHUB_OUTPUT
      shell: bash

I’m sure there will be more iterations, but this was a pretty good step forward and it will be the new baseline to add to my project template and slowly add back into existing projects.

You can find this workflow at rust.yml, along with my security-audit.yml (which includes a schedule to be notified of issues), and release.yml workflows. I also use the cool typos.yml workflow on pull requests.