One job, several Ruby versions

The matrix

One job tests one Ruby version. A matrix turns that job into several jobs, one per version, without copying the steps. The workflow keeps the step list and gains a list of versions:

jobs: test: runs-on: ubuntu-latest strategy: fail-fast: false matrix: ruby: ["3.3", "3.4", "4.0"] steps: - uses: actions/checkout@v4 - uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} bundler-cache: true - run: bundle exec rake ci

fail-fast: false keeps the other jobs running when one version fails. The default stops the rest as soon as the first job fails, which is faster on a broken branch and hides whether the problem is one version or all of them.

Check that it parses

The version list is data, so the YAML check can print it:

ruby -ryaml -e 'p YAML.load_file(ARGV[0])["jobs"]["test"]["strategy"]["matrix"]["ruby"]' .github/workflows/ci.yml

["3.3", "3.4", "4.0"]

Three jobs, each with the same steps and one different value. The ${{ matrix.ruby }} is replaced before the job starts, so the runner only ever sees a plain string.

The same check locally

A matrix only helps if the code actually runs on every version in it. The interpreter can tell a script which version it is:

ruby -e 'puts RUBY_VERSION'

3.4.12

A guard for a feature that needs a newer Ruby compares versions properly instead of string by string:

ruby -e 'puts Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3.0")'

true

String comparison would call "3.10" smaller than "3.9"; Gem::Version reads the dots as numbers. Put the minimum version in the gemspec with required_ruby_version and the matrix and the package then agree.

What to watch for

Every matrix entry costs a full job: install the gems, run the tests. Two versions are usually enough for a course project; three is a statement that the library supports the old line. Drop a version when it reaches end of life, and add the new one in the same commit.

A matrix that is red on the oldest version only is the useful case: the code uses something newer than the gemspec promises. Read the failing job before removing the version from the list.

Next: Artifacts against caches

Article author: Arthur Isaev