Automating Ruby Gem Releases with GitHub Actions
Andrew Mason
Updated Jan 9th, 2022
Whether you are a gem maintaining machine or new to the world of authoring gems, this tutorial is for you. Adhereing to SemVer and keeping an updated changelog are both important components in well maintained open source, but they are also a pain at times. This tutorial will walk you through a simple way to create a release process that automates the small, but important, parts of maintaining a Ruby gem.
Release Please
Release Please Action is a GitHub action created by Google to automate releases with Conventional Commit Messages. As you merge PR’s into your main branch, the action will create/update a new release branch that automatically adds your commits to a changelog and bumps the version according to your commits. When you’re ready to release your changes, merging the PR will cause a new GitHub release to be created and released. We can even automate publishing to package registries like RubyGems!
Conventional Commits
This article will assume you are familiar with Conventional Commits. Here is a brief overview of the important prefixes, pulled from the action’s README
The most important prefixes you should have in mind are:
fix
: which represents bug fixes, and correlates to a SemVer patch.feat
: which represents a new feature, and correlates to a SemVer minor.feat!
:, orfix!:
,refactor!:
, etc., which represent a breaking change (indicated by the !) and will result in a SemVer major.
I’ve considered doing a longer article about how I use conventional commit messages in my workflow, so let me know if you’d be interested in that.
Testing it out
I’m going to create a new gem to demo this action’s functionality:
bundler gem release-please-demo --test=rspec --ci=github
cd release-please-demo
bundle install
Skip to the bottom if you’d just like to see the result!
Next we will need to update our gemspec if we want to publish the gem. I’m not going to go over this right now, but if you’re curious to learn more about how to setup a Ruby gem specification, I suggest checking out this great article by Piotr Murach.
This is what my release-please-demo.gemspec
looks like:
# frozen_string_literal: true
require_relative "lib/release/please/demo/version"
Gem::Specification.new do |spec|
spec.name = "release-please-demo"
spec.version = Release::Please::Demo::VERSION
spec.authors = ["Andrew Mason"]
spec.email = ["andrewmcodes@protonmail.com"]
spec.summary = "Demo of release-please."
spec.description = "A demo gem showing how to use release-please to automatically version gems."
spec.homepage = "https://github.com/andrewmcodes/release-please-demo"
spec.license = "MIT"
spec.required_ruby_version = Gem::Requirement.new(">= 2.4.0")
spec.metadata = {
"bug_tracker_uri" => "#{spec.homepage}/issues",
"changelog_uri" => "#{spec.homepage}/blob/main/CHANGELOG.md",
"documentation_uri" => spec.homepage.to_s,
"homepage_uri" => spec.homepage.to_s,
"source_code_uri" => spec.homepage.to_s,
}
spec.files =
Dir.chdir(File.expand_path(__dir__)) do
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
end
spec.bindir = "exe"
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
spec.require_paths = ["lib"]
end
Since the purpose of this article is to focus on the release cycle, we are just going to use the gem that Bundler scaffolded without any code changes. If you were building you own gem, this is the part where you would add functionality to the gem.
Setting up the action
Let’s build our release action:
touch .github/workflows/release.yml
Open this in your code editor of choice.
First we are going to set the name of the action, and when it should run. We only want this action to run when something is merged into the default branch, or a release branch depending on your workflow. I name my default branch main, so every time code gems pushed to main, we will run this action.
# .github/workflows/release.yml
name: release
on:
push:
branches:
- main
Next we need to setup a job for the Release Please Action. Option descriptions are annotated with comments, but please view the official configuration documentation to learn more.
jobs:
release-please:
runs-on: ubuntu-latest
steps:
- uses: GoogleCloudPlatform/release-please-action@v2
id: release
with:
# The release type
release-type: ruby
# A name for the artifact releases are being created for
# which is the name of our gem
package-name: release-please-demo
# Should breaking changes before 1.0.0 produce minor bumps?
bump-minor-pre-major: true
# Path to our version file to increment
version-file: "lib/release/please/demo/version.rb"
We are going to do some more cool things in a second but lets go ahead and see what this produces. Create a new GitHub repo, commit everything, and push it up. As a note, Bundler adds a failing test condition by default when you scaffold the gem, so if you added the --ci=github
flag when you created the gem, the generated .github/workflows/main.yml
action will fail unless you remove the failing test. I’ll let you debug that on your own for now.
The release action will run once you push your changes to the main branch. Your initial run output should look like this:
Run GoogleCloudPlatform/release-please-action@v2
✖ No merged release PR found
✖ Unable to build candidate
✔ found 4 commits since beginning of time
✖ no user facing commits found since beginning of time
This output says:
- A merged release PR was not found, which we will talk about in a moment
- There is no build candidate
- There were 4 commits found in the repo
- None of those commits were user facing, aka they weren’t features or bug fixes
Just for reference - this is the output of git log --one-line
so you can see my four commits:
9a4d62b (HEAD -> main, origin/main) build: add release action (#1)
1b0bcd4 chore: bundle install
7a30c6d chore: update gemspec
c793bca chore: initial commit
As we can see, none were features or fixes, so the action did not create a release PR.
Creating a release
I’m going to cheat and an empty commit for a feature:
git commit --allow-empty -m "feat: add a feature"
git push -u origin main
Our release action should run and this time find a user facing commit and open a new release PR. The PR will increment the version number and create a new, or edit an existing, Changelog.
Note: For a gem without prior releases, I wasn’t able to find a way to prevent a full point release. You could get around this by editing the release PR to match the inital version number you’d like before merging. This is not an issue with projects with prior releases.
Publish to RubyGems
Our current setup is great if we just want to automate changelog creation and versioning, but we would still have to publish the gem ourselves after the release was created. Fortunately, we can hook into our existing workflow to automate publishing as well!
You may have noticed we gave our first step an id of release
. By doing this, we can check the output of that step in other steps and act accordingly.
Setup Steps
If the output of our release step is release_created
, we will checkout the repo, install Ruby, and run bundle install
:
jobs:
release-please:
runs-on: ubuntu-latest
steps:
- uses: GoogleCloudPlatform/release-please-action@v2
id: release
with:
release-type: ruby
package-name: release-please-demo
bump-minor-pre-major: true
version-file: "lib/release/please/demo/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:
ruby-version: 3.0.0
if: ${{ steps.release.outputs.release_created }}
# Bundle install
- run: bundle install
if: ${{ steps.release.outputs.release_created }}
Publish Step
If a release was created, we will setup gem credentials, build the gem, and push it to RubyGems.
You will need to get an API token from RubyGems and add it to your repository secrets. I named mine RUBYGEMS_AUTH_TOKEN
but you can set the name to whatever you’d like.
- 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 }}
Note: I got this code straight from GitHub’s action documentation.
Release and Publish
Our final action:
# .github/workflows/release.yml
name: release
on:
push:
branches:
- main
jobs:
release-please:
runs-on: ubuntu-latest
steps:
- uses: GoogleCloudPlatform/release-please-action@v2
id: release
with:
release-type: ruby
package-name: release-please-demo
bump-minor-pre-major: true
version-file: "lib/release/please/demo/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:
ruby-version: 3.0.0
if: ${{ steps.release.outputs.release_created }}
# Bundle install
- run: bundle install
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 }}
Commit, push this code, and wait for your release PR to be updated by our action bot. Once the release PR has been updated, merge the PR into your main branch.
Once our release action runs, assuming it succeeds, you should see a new release in GitHub! One great feature of this action is that it will build the release notes from our changelog entries. 🚀
If we check RubyGems, we should see our new gem has been published and is ready to share!
Final Thoughts
If you followed the tutorial and don’t intend to use your new gem, you should consider yanking it to allow others to use the name in the future.
gem yank release-please-demo -v 1.0.0
One great aspect of the action is that you can use it with other languages or a .txt
file, allowing you to create consistent pattern across all of your open source. You could enhance the workflow by adding in checks to run the tests before releases and also adding a linter to ensure conventional commits are used. With this workflow, you’ll be able to make new releases without pulling down the code and never have to try and remember how you release a project again.
Give it a try and tell me what you think!
- Questions
- on GitHub Discussions↗
- Discuss
- on Twitter↗
- Updated On
- Jan 9th, 2022
- Published On
- Feb 19th, 2021
- Word Count
- 2193 words
- Reading Time
- 9 min read