A start-to-finish walkthrough on creating a Ruby gem with CI and CD

Creating an open source Ruby gem is easier than you might think! In this post, we will be walking through the process of creating and publishing a Ruby gem from start to finish.

Terminology

First off, let’s define some terms and describe their relevance:

  • Ruby gem: A Ruby gem is a distributable library of Ruby code. Gems are how we share Ruby code. Learn more.
  • rubygems.org: The Ruby community’s gem hosting service. Contribute. Learn more.
  • bundler: The Ruby community’s gem manager. Learn more.
  • CI: Continuous Integration is, in the context of this blog post, the process of automatically running a test suite on every commit and ensuring those tests remain passing by preventing merges to main without a passing test suite (or as we like to call it where I work at Gusto, a “green build”). Learn more.
  • CD: Continuous Deployment is, also in the context this blog post, the process of automatically pushing new versions of a Ruby Gem to rubygems.org every time three things are true: (1) A commit is merged to main (2) that commit has a green build (3) The gem version has been updated.
  • GitHub Actions: A build system provided by GitHub that we will be using as the “compute” for our CI and CD pipelines. Learn more.

Whew! Now we can get onto the fun stuff…

Let’s Get Started

To keep things tidy, I’ll be referencing commits and pull requests. If you have a question about any part of the code, please drop an inline GitHub comment and tag my GitHub username (also @alexevanczuk). I’ve done my best to leave some inline comments, and I’d also highly encourage checking out the world-class documentation on https://guides.rubygems.org/ and https://docs.github.com/en. I’ve done my best not to repeat too much that is already discoverable in those docs.

Create the Repository

To start off, I’d highly recommend using the GitHub CLI. You can install it with brew install gh . Of course, any git and GitHub client will work just fine.

I’ll use gh repo create to create a new repository to host my gem. I use the name my_example_gem as the name of my repo and gem. Try to find a name that is meaningful to you and your stakeholders.

~/workspace - $ gh repo create
? What would you like to do? Create a new repository on GitHub from scratch
? Repository name my_example_gem
? Description An example gem set up with CI and CD.
? Visibility Public
? Would you like to add a README file? No
? Would you like to add a .gitignore? No
? Would you like to add a license? No
? This will create "my_example_gem" as a public repository on GitHub. Continue? Yes
✓ Created repository alexevanczuk/my_example_gem on GitHub
? Clone the new repository locally? Yes
Initialized empty Git repository in /Users/alex.evanczuk/workspace/my_example_gem/.git/

When I navigate to https://github.com/alexevanczuk/my_example_gem, I see this screen:

I navigate into the newly created gem folder with cd my_example_gem and follow the set of instructions labeled “create a new repository on the command line,” which created this commit:

first commit · alexevanczuk/my_example_gem@5ce5c1b
An example gem set up with CI and CD. Contribute to alexevanczuk/my_example_gem development by creating an account on…

Create the Gem

bundler makes this easy. I first cd .. to go up to the parent directory, then run:

bundle gem my_example_gem --coc --test=rspec --linter=rubocop --mit

Some notes about this:

  • I press enter when it asks me if I want to overwrite the existing README.md
  • bundler has a flag --ci=github , but we’ll be building this ourselves.
  • I use the MIT License, which is very common license used for open source software projects. If you’re affiliated with an organization or a company, you’ll want to make sure you’re familiar with its licensing requirements (i.e. talk to your legal team).

If all went to plan, you should be ready to create a PR, with these steps:

  • cd into your repo (cd my_example_gem for me)
  • Checkout a branch: git checkout -b create-gem
  • Stage all changes: git add .
  • Commit staged changes with git commit -m "bundle gem my_example_gem --coc --test=rspec --linter=rubocop --mit”
  • Create a PR: gh pr create
  • Merge the PR: gh pr merge
settings => general  main  settings => branches => Add rule

Great! We have one merged PR:

Create gem using bundler by alexevanczuk · Pull Request #1 · alexevanczuk/my_example_gem
Add this suggestion to a batch that can be applied as a single commit. This suggestion is invalid because no changes…

More Boiler Plate

If you run bundle install at this point in your repo, you may see this:

The gemspec at /Users/alex.evanczuk/workspace/my_example_gem/my_example_gem is not valid. Please fix this gemspec.
The validation error was 'metadata['homepage_uri'] has invalid link: "TODO: Put your gem's website or public repo URL here."'

I make some changes until I can bundle install. I’ve also changed some of the defaults, such as specifying spec.filesmore plainly rather than using git .

Follow the same process above to create and merge a PR. You can see the merged PR here:

Fix up gemspec so we can bundle install by alexevanczuk · Pull Request #2 ·…
Add this suggestion to a batch that can be applied as a single commit. This suggestion is invalid because no changes…

Get an API Key

We need a rubygems.org API key to push. Get one from https://rubygems.org/profile/api_keys! You’ll need to create an account and login if you haven’t, and your gem needs the “Push rubygem” API scope.

Then, add that gem as a secret to your repo. You can find that under settings => secrets => actions . Create a new secret with the name RUBYGEMS_API_KEY and set the secret value. Make sure not to put this secret anywhere that others can find it — it’s a secret after all. I like to put mine in my 1Password as a note in my rubygems.org login. If you’re doing this for a company, make sure you follow your organization’s policies for login and secrets management.

Add CI and CD

Checkout a new branch with git checkout -b add-ci-and-cd

For this, we’ll be using GitHub Actions. GitHub Actions use a special syntax to define their logic. To learn more about how these actions work, check out the PR that add them and the inline comments.

I had to do three more things to finish this:

  1. I removed a trivially failing test
  2. I ran bundle exec rubocop -a to auto fix rubocop
  3. I ran bundle lock --add-platform x86_64-linux , which lets bundle work in GitHub Actions, which uses a linux machine.

Here’s the PR:

Create CI and CD workflows by alexevanczuk · Pull Request #3 · alexevanczuk/my_example_gem
Add this suggestion to a batch that can be applied as a single commit. This suggestion is invalid because no changes…

With that, everything passes!

Once I merged with gh pr merge -d , tests were automatically kicked off on main . Once those completed, a CD action was kicked off. Note to see kicked off CD actions, you need to navigate to the “Actions” tab of your repository, which for me is here: https://github.com/alexevanczuk/my_example_gem/actions

You can also follow along with workflows locally with gh run watch

Here is the CD action:

CD · alexevanczuk/my_example_gem@0667457
An example gem set up with CI and CD. Contribute to alexevanczuk/my_example_gem development by creating an account on…

And here is the listing in rubygems.org for my new gem!

my_example_gem | RubyGems.org | your community gem host
RubyGems.org is made possible through a partnership with the greater Ruby community. Fastly provides bandwidth and CDN…

Lastly, check out this lovely autogenerated release page!

Release v0.1.0 · alexevanczuk/my_example_gem
What's Changed Create gem using bundler by @alexevanczuk in #1 Fix up gemspec so we can bundle install by @alexevanczuk…
Did you know you can link to specific lines in a GitHub Action log? For example, here's the link to the command that was used to generate the release: https://github.com/alexevanczuk/my_example_gem/actions/runs/3259482118/jobs/5352349524#step:5:1

This is a great feature if you want to share failures with others!

With this, not only do I never have to manually deploy, but other contributors can also deploy for themselves without needing to share access to API keys! Other contributors also have visibility into failed builds and deploys.

Important note: If you're using 2FA/MFA for RubyGems, you'll need to set it (here) to the "UI and gem signing level," and not "UI and API." Another option if you do not want to change this is to change CD to use the "workflow_dispatch" event instead of "workflow_run," which will let you and other contributors publish from the command line with the "gh run" command. You can then set the "otp_code" to be a user input, which can then be passed to the action, which can then be passed to the gem push command.
Important note: here UI and gem signing  UI and API workflow_dispatch  workflow_run gh run  otp_code  which can then be passed to the gem push command

Wrap Up

I think this is a good place to stop!

From here, there’s so much we can do! Besides, of course, adding behavior to your gem so its valuable for you and others to use, there are countless ways to continue to improve the development process. For example…

  • We could add Sorbet for static type checking
  • We could add Zeitwerk for autoloading (no more require statements)
  • We could make a standard CLI (command-line interface) with thor
  • We could make an interactive CLI with tty-prompt
  • …and so much more!

Share your thoughts, questions, feedback, and what you’d like to see next as a comment here, within any of the above linked pull requests, or on the Ruby/Rails Modularity Slack Server.