Automating Ruby Gem Package Releases With Github Actions

In the final part of the Ruby Gem series, we will be automating the deployment of our recently created gem using GitHub Actions.

On this page

In the series so far, we have created a gem to test locally before pushing to the remote RubyGems repository.

This time, we will be setting up an automated configuration in order to automate the process of release based on conventional commits.

It will work off the code written in the previous part "Deploying Your Gem To RubyGems".

Source code can be found here.

Prerequisites

  1. Understanding how Conventional Commits works. Not much will be covered.
  2. Catch up with the previous posts on Series: Releasing your own Ruby Gem.

Getting started

We will clone the project rspec-github-actions (if you haven't been keeping up with the previous work):

$ git clone https://github.com/okeeffed/rspec-github-actions.git
$ cd rspec-github-actions
# Checkout the starting point
$ git checkout 3-gem-deployed

At this stage, our project is now ready to continue working on.

Updating our GitHub Action for release

This post will really be a recap of the great work done on the blog post "Automating Ruby Gem Releases with GitHub Actions" at andrewm.codes.

Since that was such a straight-forward post, this will be more on the adjustments made to my own project to also automate the Ruby Gem release.

The method we are going to take makes use of Google's GitHub Action release-please-action. This action takes your commit history to help automate the package semantic version.

If you are not familiar with conventional commits, the quick summary gives a good overview of how it works.

Essentially, you prefix your git commits with a specified "type" and "optional scope" <type>[optional scope]: <description> and based on the type, it will have a different weight for the impact on the versioning.

The types fix and feat are associated with PATCH and MINOR updates respectively, while a BREAKING CHANGE or bang following the type <type>! will result in a major change.

If you look at the history of the logs I have committed for this project, you will see the following (using git log --pretty=oneline):

$ git log --pretty=oneline
0e0977aa (HEAD -> main, origin/main, origin/3-gem-deployed, 3-gem-deployed) feat: add the example folder
012bdeb7 (origin/2-gemspec, 2-gemspec) feat: add in gemspec file
3e0f797b (origin/1-rspec-with-github-actions, 1-rspec-with-github-actions) fix: README
d5fd59c2 fix: remove lcov
f639002d docs: add README
016d5ae7 feat: first attempt at post

Luckily, this bodes well for our GitHub Action.

Updating our GitHub Workflows

Let's make some adjustments to our .github/workflows/rspec.yml workflow:

name: Run RSpec tests
on:
  push:
    branches:
      - "**" # matches every branch
      - "!main" # excludes main
jobs:
  run-rspec-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Set up Ruby
        uses: ruby/setup-ruby@v1
        with:
          # Not needed with a .ruby-version file
          ruby-version: 2.7
          # runs 'bundle install' and caches installed gems automatically
          bundler-cache: true
      - name: Run tests
        run: |
          bundle exec rspec

This will not run the RSpec tests action on the main branch, which we will set up a new action for.

Next, let's create a new workflow. Create a new file with touch .github/workflows/release.yml and then add the following code:

name: Release
on:
  push:
    branches:
      - "main" # main only
jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: GoogleCloudPlatform/release-please-action@v2
        id: release
        with:
          release-type: ruby
          package-name: contrived_math
          bump-minor-pre-major: true
          version-file: "lib/release/contrived_math/version.rb"
      # Checkout code if release was created
      - uses: actions/checkout@v2
        if: ${{ steps.release.outputs.release_created }}
      # Setup ruby if a release was created
      - uses: ruby/setup-ruby@v1
        with:
          # Not needed with a .ruby-version file
          ruby-version: 2.7
          # runs 'bundle install' and caches installed gems automatically
          bundler-cache: true
        if: ${{ steps.release.outputs.release_created }}
      - name: Run tests
        run: |
          bundle exec rspec
        if: ${{ steps.release.outputs.release_created }}
      # Publish
      - name: publish gem
        run: |
          mkdir -p $HOME/.gem
          touch $HOME/.gem/credentials
          chmod 0600 $HOME/.gem/credentials
          printf -- "---\n:rubygems_api_key: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials
          gem build *.gemspec
          gem push *.gem
        env:
          # Make sure to update the secret name
          # if yours isn't named RUBYGEMS_AUTH_TOKEN
          GEM_HOST_API_KEY: "${{secrets.RUBYGEMS_AUTH_TOKEN}}"
        if: ${{ steps.release.outputs.release_created }}

This new action will run on main and will release our Ruby Gem if both testing and the release-action-please succeed.

This example is contrived and will only run things on the main branch, but adjust this for the release workflow you actually want in your own work.

Note: you will need to create a new RubyGems API key and add that to the repo as the secret named RUBYGEMS_AUTH_TOKEN.

Configuring our version for our gemspec file

Update contrived_math.gemspec to the following:

require_relative 'lib/release/contrived_math/version'

Gem::Specification.new do |s|
  s.name        = 'contrived_math'
  s.version     = ContrivedMath::VERSION
  s.summary     = 'Hello, World!'
  s.description = 'A simple hello world gem'
  s.authors     = ["Dennis O'Keeffe"]
  s.email       = 'hello@dennisokeeffe.com'
  s.files       = ['lib/contrived_math.rb']
  s.homepage    =
    'https://rubygems.org/gems/contrived_math'
  s.license = 'MIT'
end

Now we will grab the version from the file lib/release/contrived_math/version.rb.

Let's make that file and add in some content now:

module ContrivedMath
  VERSION = '0.0.0'.freeze
end

The file content comes from Google's example.

The GitHub Action at work

Assuming you already set up the secret for the RubyGems, we can push main and see the action at work.

New action running
New action running

The first time we run the action, it will note that it needs to create a release PR for us. We can see that in the action logs.

Action creating a release
Action creating a release

This release will create some release notes and a PR. The notes will followed the conventional commit types.

Release notes created
Release notes created
Created PR
Created PR

Next, we can approve and merge the PR.

PR approval
PR approval

Once merged, the bot will link the release notes for us.

PR merged and release notes added
PR merged and release notes added

The merge of the pull request will kick off another invocation of the release action. This time, the action will release the gem.

Note: The 0.1.0 release failed because I was a goose, so the following two images come from the 0.1.1 release which followed all the same previous steps after I implemented the fix.

Release action workflow running
Release action workflow running

Finally, a successful release will also show up on our live Ruby Gem.

The RubyGem release
The RubyGem release

Success! The release schedule has been automated through GitHub Actions.

Summary

Today's post demonstrated how to take our Ruby Gem repo and automate the release process based on changes noted in git history using Conventional Commits.

It is worth noting that these flows outlined today may not be appropriate to your own needs (I am only running release on the main branch will generally is not what you will want since main would be protected), but take the workflows from today and apply the changes for your own work as required.

Resources and further reading

Photo credit: pawel_czerwinski