If you maintain a Ruby gem, you are definitely familiar with the recurring manual tasks surrounding the release of a new version. After doing this for a while, you inevitably start thinking that some of these steps could be automated. They can! With a few lines of code, you can bring the amazing world of continuous delivery to your project and increase the reliability of the whole process while freeing up some of your time. Double win!
In this article, I will demonstrate how to create a whole process around a simple gem so you can see some of the possibilities.
The case for automation
There are several reasons to bring a continuous delivery (CD) process to any professional or passion project. I will start with probably the most obvious one: it saves you time. Sure, the initial setup will need some upfront investment while you research and try things out until everything falls into place. After that, though, the benefits will stack up throughout the life of your project.
Another great advantage is the reproducibility it brings. Automation implies predictability and that can be expressed through configuration files and commands that will always run in the same order. In turn, this makes debugging easier, in case you run into an issue during deployment.
Finally, the determinism mentioned above will definitely lead to fewer bugs and mishaps during release phases. Instead of trying to remember the right order of commands to run when you are ready to deploy a new version of a gem, you just rely on the tried and tested process you had previously set up to do that for you. It is documented and you know it will be executed flawlessly.
Building a demo
In this tutorial, I will lead you through creating a small project to use as a publishable gem. We will work with something simple: a Ruby gem that adds the method zigzagcase
to the String object. It will return a copy of the string alternating the case of the characters: LiKe tHiS
. For simplicity’s sake, we will keep our code plain Ruby, which is simple to code and test. You can save writing something that would need to work with Rails, Sinatra, or other Ruby frameworks for another project.
Prerequisites
You will need a few things to follow along with this tutorial:
- A GitHub account. You can create one here.
- A Rubygems account to publish our gem
- A CircleCI account. You can create one here. To easily connect your GitHub projects, you can sign up with your GitHub account.
Note: Make sure that Bundler is installed. You will use it to simplify gem creation. Using Bundler is the main way people in the Ruby community manage their gems, so it makes sense to use it to create one.
Setting up the project
Begin by running:
$ bundle gem zigzagcase --test=minitest
This simple command creates a scaffolding folder and also initializes a git repository. If this is the first time you have ever used Bundler 2.1.4 to run bundle gem
, you will be asked whether you want to include a code of conduct and a license. I recommend that you add one; MIT is appropriate for this project. The test
flag we passed will automatically pick minitest as our testing framework.
You can read more about the scaffolding created by Bundler (there are quite a few files), but the main points are:
zigzagcase/lib/zigzagcase.rb
is used as a placeholder to import other files with our codezigzagcase/lib/zigzagcase/version.rb
holds the current version of the gemzigzagcase/zigzagcase.gemspec
defines the gem: what’s in it, who made it, license, and other technical details
Before going forward, there is a bit of housekeeping to be done. Go to the cd zigzagcase
folder and run grep TODO zigzagcase.gemspec
. Bundler has left a few things we need to take care of ourselves. If we do not, we will have trouble running other commands, so we might as well address this now. Dig into that file and make the changes shown in the next code block.
require_relative 'lib/zigzagcase/version'
Gem::Specification.new do |spec|
spec.name = "zgzgcase"
spec.version = Zigzagcase::VERSION
spec.authors = ["Gonçalo Morais"]
spec.email = ["author.email@example.com"]
spec.summary = %q{MaKe sTrInGs lOoK LiKe tHiS}
spec.description = %q{Alternates upper case and lower case across letters}
spec.homepage = "https://github.com/CIRCLECI-GWP/zigzagcase"
spec.license = "MIT"
spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
spec.metadata["allowed_push_host"] = "https://rubygems.org"
spec.metadata["homepage_uri"] = spec.homepage
spec.metadata["source_code_uri"] = "https://github.com/CIRCLECI-GWP/zigzagcase"
spec.metadata["changelog_uri"] = "https://github.com/CIRCLECI-GWP/zigzagcase/blob/main/CHANGELOG.md"
# Specify which files should be added to the gem when it is released.
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
end
spec.bindir = "exe"
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
spec.require_paths = ["lib"]
end
A gem with the name zigzagcase
already exists in rubygems. For this tutorial, we have used the name zgzgcase
instead. It is the name assigned to spec.name
in the previous code block. For you to successfully publish your own gem, you need to choose an alternative name and confirm that there is no gem with that name. Search at rubygems and if there is no gem with the name you chose, you can proceed with the tutorial. Replace every instance of zgzgcase
with your particular name.
Our gem depends on a few other gems out of the box. You can check the Gemfile
to see which ones. Install them by running bundle install
and commit the Gemfile.lock
that results from it.
It is a good practice to update the README
file as well. First, in the Installation section, update the gem name. Update the URL of the Contributing section with the URL of your GitHub repo. We will create the repo next.
Save this starting structure on GitHub first before moving into writing our code. Make sure you create an empty repo. Bundler initialized a local git repository for you and it is easier to simply send what you have straight to GitHub if the project is empty. If you are unfamiliar with this process or new to GitHub, follow this tutorial.
Testing as a feature
Part of a healthy continuous delivery workflow is setting up a reliable test suite that gives you confidence in what you release. Since it’s such a crucial part of it, let’s make sure we add a few tests that will maintain the integrity of our gem. During our scaffolding, we picked minitest as our testing framework — but you are free to pick any other, the important bit is to write meaningful tests.
If you check the Rakefile
created by Bundler, you’ll see that Rake’s default task will be to run tests, so we can give it a spin right now without making any changes yet by executing bundle exec rake
.
Run options: --seed 21588
# Running:
.F
Finished in 0.001040s, 1923.0769 runs/s, 1923.0769 assertions/s.
1) Failure:
ZigzagcaseTest#test_it_does_something_useful [zigzagcase/test/zigzagcase_test.rb:9]:
Expected false to be truthy.
2 runs, 2 assertions, 1 failures, 0 errors, 0 skips
rake aborted!
Command failed with status (1)
/Users/circleci/.rbenv/versions/2.6.5/bin/bundle:23:in `load'
/Users/circleci/.rbenv/versions/2.6.5/bin/bundle:23:in `<main>'
Tasks: TOP => default => test
(See full trace by running task with --trace)
Open test/zigzagcase_test.rb
and replace it with a couple of simple tests for our method:
require "test_helper"
class ZigzagcaseTest < Minitest::Test
def test_alternates_upcase_and_downcase
assert_equal "AdVeNtUrE TiMe!", "Adventure Time!".zigzagcase
end
def test_keeps_original_string_untouched
original_string = "Adventure Time!"
expected_string = original_string.dup
original_string.zigzagcase
assert_equal expected_string, original_string
end
end
If you happen to try to run these tests, you will get an error:
NoMethodError: undefined method 'zigzagcase' for "Adventure Time!":String
.
We are getting this error because we have not written any code for our method yet.
Create a string.rb
file inside the lib/zigzagcase
folder. We will add our code there (lib/zigzagcase/string.rb
). For simplicity, this gem will automatically add a zigzagcase
method to Ruby’s String class.
class String
def zigzagcase
modifiers = %i[upcase downcase].cycle
self.chars.map{ |char| char.send(modifiers.next) }.join
end
end
In a nutshell, this code is getting an array of all the characters in the string, returning a copy of that array while alternating between calling upcase
or downcase
on each character, and finally joining it all back together to return a new string. This new file is not used anywhere yet, so the code we just wrote is not being picked up. We will address that by importing it through the main gem class (lib/zigzagcase.rb
). Replace the contents of that file to this:
require "zigzagcase/version"
require "zigzagcase/string"
Now that we have our code in order, we can run out test cases again just to see them pass with flying colors.
Run options: --seed 48145
# Running:
..
Finished in 0.000964s, 2074.6888 runs/s, 2074.6888 assertions/s.
2 runs, 2 assertions, 0 failures, 0 errors, 0 skips
Automating the process
We have run this locally, and now we want to make sure ‘robots’ do the work for us. We will use this example as a reference, but our configuration will be much simpler (.circleci/config.yml
):
version: 2.1
orbs:
ruby: circleci/ruby@1.0.4
jobs:
test:
docker:
- image: cimg/ruby:2.7
steps:
- checkout
- ruby/install-deps
- run:
name: Run tests
command: bundle exec rake
workflows:
version: 2
deploy:
jobs:
- test
After adding it, make sure you commit the above .circleci/config.yml
and push it to GitHub. This will automatically trigger your project’s first build on CircleCI.
Preparing for a release
We will follow semantic versioning to release this gem. Hopefully, you are following a similar practice. When correctly employed, this is a quick and easy way to give a clear heads-up to people about the changes that happened.
Using a versioning policy will allow us to implement the crucial difference between continuous delivery and a continuous deployment process. While building a web application, you might want to make every single code change available to the user as soon as possible. There are no release dates because shipping means getting new features and changes to the hands of users without human intervention or approval. With continuous delivery, you want to retain the manual process of release. While working on a gem, it is possible you want all your latest code to land on your main branch whenever it is merged, but you do not want to release a new version every time that happens.
As an extra detail, it is useful to have a single place to quickly scan the changes between versions, and a changelog file is the perfect spot for it. We will create one and add it to our scaffolding in the next section.
Now we will set up something cool on CircleCI: the ability to run a deployment step if we push a git tag to GitHub. Whenever something gets merged into our main branch, we only want to run our tests. But when we tag something for release, we want CircleCI to do the hard work. After our tests pass, we want it to automatically build a new version of our gem and publish it on Rubygems.
We will set up a way of authenticating on Rubygems, so it can publish gems in our name. After checking the documentation, we know that we can set a GEM_HOST_API_KEY
in our server with a key from our Rubygems’ API keys page. Click New API key to create one. Enter a name to identify the API key, select the desired scope, and then click Create. In our example, only the Push rubygems
scope is checked.
Set that variable in your project.
With the credentials in place, we need to change the CircleCI configuration to include this additional step. We are adding a deploy job that will run only for tags. Here is the updated config file (.circleci/config.yml
):
version: 2.1
orbs:
ruby: circleci/ruby@1.0.4
jobs:
test:
docker:
- image: cimg/ruby:2.7
steps:
- checkout
- ruby/install-deps
- run:
name: Run tests
command: bundle exec rake
deploy:
docker:
- image: cimg/ruby:2.7
steps:
- checkout
- ruby/install-deps
- run:
name: Publish gem to Rubygems
command: bundle exec rake release
workflows:
version: 2
test-and-deploy:
jobs:
- test:
filters:
tags:
only: /.*/
- deploy:
requires:
- test
filters:
tags:
only: /^v.*/
branches:
ignore: /.*/
The two big differences here:
- Our new job is responsible for building and publishing our gem using Bundler
- Our workflows section changed to accommodate a bit of logic for filtering the events that trigger the deploy job
In this example, we are matching with any branch (thus ignoring all branches) and matching only with tags starting with a v (v0.0.1 and so on).
Wrapping it all up
Now it is time to set our workflow in action. Whenever you are happy with the changes you merged into the main branch and you are ready to cut out a new version of our app, you only need to tag it correctly and push it to GitHub.
If you want to keep your project stellar, consider keeping a record of the changes the gem goes through. This record can serve as a timeline of its evolution. We will write a summary of the changes in our gem on CHANGELOG.md
. Here is an example.
# Change Log
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).
## [0.1.0] - 2020-09-19
### Added
- First release
Edit yours according to your experience. Keep this file up to date, because this is where people will find a summary and useful links about the changes that happened between versions. It is especially important to double-check for possible regressions and security fixes.
Also make sure to tag your gem with as much information as possible, by using annotated tags. This will help you to keep a useful and rich git history.
Commit the changes and tag the commit:
git add .
git commit -m "Update CircleCI config & add CHANGELOG"
git tag -a v0.1.0 -m "First release"
There is one detail here that you must always check to make sure everything will run smoothly: keep the git tag and the gem version in sync. In this case, we are tagging our gem with 0.1.0
. That is the same version we find at lib/zigzagcase/version
and Gemfile.lock
. Whenever you are ready to update your gem version (because you are releasing a bug fix, for example) make sure to:
- Update it at
lib/zigzagcase/version
- Run
bundle
in the console afterwards
Your Gemfile.lock
will reflect that version increment, so do not forget to git commit
it.
We are now ready to send off our shiny new version into the world by running:
git push origin main
git push origin --tags
Go back to CircleCI dashboard to review your successful publish.
Conclusion
You have now automated gem publishing! You can find the new version of your published gem on the Rubygems website. Whenever you tag your gem for release, the process you put in place with CircleCI will make sure your fresh code is available to the Ruby community.
I hope this tutorial inspires you to automate your own Rubygems deployments and sparks your curiosity about what else you can automate. You could apply a similar process to NPM modules, iOS pods, or Unix tarballs. Why not CD all the things?