Automated releases
A release is a tag
A branch moves; a tag does not. That is why a release starts from a tag
and why the pipeline can treat a tag as a promise: the code behind
v0.1.0 never changes, so the package built from it
can always be built again.
The other half of the promise is the version inside the project. It lives
in lib/version.rb, and the gemspec and the
executable both read it from there.
module CiDemo
VERSION = "0.1.0"
end
Check the tag against the version
Two places hold the same number, so they can disagree. A small Rake task compares the tag that the pipeline passes in with the constant, and it stops the release before anything is built:
require_relative "lib/version"
namespace :release do
desc "Check that the tag matches the version"
task :check do
tag = ENV.fetch("TAG", "")
version = CiDemo::VERSION
abort "set TAG=v#{version}" if tag.empty?
abort "#{tag} does not match #{version}" unless tag == "v#{version}"
puts "ok: #{version}"
end
end
Three runs show the three answers:
TAG=v0.1.0 bundle exec rake release:check
ok: 0.1.0
TAG=v0.2.0 bundle exec rake release:check
v0.2.0 does not match 0.1.0
bundle exec rake release:check
set TAG=v0.1.0
The last two exit nonzero, so a pipeline stops on them. A release job that starts with this task cannot publish a gem whose tag and version disagree.
The release job
The job runs only when a tag is pushed, checks the version, builds and publishes:
on:
push:
tags:
- "v*"
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with:
ruby-version: "3.4"
bundler-cache: true
- run: bundle exec rake release:check
env:
TAG: ${{ github.ref_name }}
- run: gem build ci-demo.gemspec
- run: gem push *.gem
env:
GEM_HOST_API_KEY: ${{ secrets.RUBYGEMS_API_KEY }}
github.ref_name is the tag without the refs/tags/ prefix, which is exactly what the task compares. On a branch push the job does not run at all.
What to watch for
Bump the version in one commit with the changelog line, then tag that
commit. A tag on a commit that changes later is not a release, and a
version bumped after the tag is a package nobody can find.
Test the release path with a dry run before the first real tag: build the
gem, install the file locally, run the executable. A release job that
fails at the push is a tag that already exists and a package that does
not.
Next: Making a pipeline fast
Article author: Arthur Isaev