How to make changes to a Ruby gem (as a Rails developer)

Updated . Posted . Visible to the public. Repeats.

At makandra, we've built a few gems over the years. Some of these are quite popular: spreewald (> 1M downloads), active_type (> 1M downloads), and geordi (> 200k downloads)

Developing a Ruby gem is different from developing Rails applications, with the biggest difference: there is no Rails. This means:

Also, their scopes differ. A Rails application usually combines many libraries with custom code. It runs on a given version of Rails and Ruby, and it is tailored to a certain domain.
In contrast, a gem usually:

  • serves a single purpose
  • supports several Ruby versions (some of our gems still support Ruby 1.8.7)
  • supports several Rails versions (some of our gems still support Rails 2.3)
  • cannot and should not know in which domains it will be used

All this influences your development.

Development cycle (TOC)

The gem development cycle looks like this:

  • Get authorized for change
  • Get a copy of the repository
  • Understand the gem
  • Make your changes
  • Bump the version number
  • Update the changelog
  • Run tests and commit
  • Make a pull request, or release the gem

Let's look at these steps in detail.

Authorization

When you plan to make changes to a gem, make sure to get authorized. This can be as simple as getting instructions from the gem owner.

If you initiate the change, make sure the gem owner will accept it. Ask him, e.g. by creating an issue in the gem's issue tracker, and make sure your work won't be for nothing.

Getting a local copy of a gem

How you're retrieving a gem depends on how close you're to its owners.

For a makandra gem you'd usually clone the repository, make a feature branch and do your work there.

After gathering some experience, and when there is no code review required, you'll be allowed to work on the master branch.

When making changes to other people's gems, you'd usually first fork their gem, clone your fork, make a feature branch and do your work there.

Understanding a gem

As stated above, there is no defined structure for a gem. Still there are some conventions many people adhere to. When you're getting started with a gem, the following steps will help you understand the gem's architecture.

The .gemspec file has a description of the gem Show archive.org snapshot . It usually states name, version, author, description and license. It always lists some kinds of files. Notably, the files attribute is an exhaustive list of files included in the gem when it is packaged. Leave this list empty and your gem will be empty. Finally, the Gemspec may define dependencies on gems and a Ruby version.

The require_paths attribute in the Gemspec defines what paths are added to Ruby's $LOAD_PATH when the gem is activated. The default value is lib, making files in the gem's lib/ directory requireable to your Ruby code.

The file lib/$gem_name.rb is the entry point of your gem. Since lib is already on your $LOAD_PATH, a require '$gem_name' will require that file. Any other needed files need to be referenced there.

In order to keep a gem's other files nice an tidy, they are usually stored in lib/$gem_name/. This keeps your files namespaced to your already unique gem name, avoiding collisions and clarifying require statements to readers of your code. Example: require '$gem_name/ext/some_file'.

Other files and directories in the gem only have loose conventions.

  • bin/ usually contains "binaries" that will be installed to your system when installing the gem (this must be configured in the Gemspec, though)
  • a CONTRIBUTING.md file describes how the gem author wishes contributions to the gem
  • dist/ usually contains files rendered ready for "distribution", e.g. minified Javascript files
  • src/ usually contains the raw "source" files, e.g. split up into many files, before minification

Making changes

When working on a gem, first make sure you have understood its structure. When it's a makandra gem, you may ask one of its contributors for a quick intro; when it's someone else's gem, stick to the above guide and make sure you first read the CONTRIBUTING.md file, if it exists. Also, the README may hold contribution hints at its bottom.

You should adhere to whatever coding style is already present in the gem. Try to write your code so that everyone believes it has been there from the beginning.

When adding files, you need to require them yourself. Look around how similar files are being required, and try to find the best spot. Your last resort is lib/$gem_name.rb.

One thing to keep in mind is that you'll need to support multiple versions of Ruby and Rails. Look at .github/workflows/test.yml to find the supported versions. In some makandra gems you find the supported Ruby versions buried in the Rakefile.

Developing with multiple Ruby versions in mind means you can only use the features of the least supported version. For example, when you need to support Ruby 1.8.7, you need to write hashes :like => this. When you need to support Ruby 1.9, you cannot use keyword arguments, etc.

Developing for multiple Rails versions is a bit harder at times, because Rails maintains less backwards compatibility. Try to use general methods and pattern that work in all versions. However, sometimes you cannot avoid a version switch, an "if Rails version < 4 do this, else do that". Gemika (see Tests below) can help you with that, but still keep these parts as short as possible.

Tests

Write tests, write them first if you can. We will never accept untested changes, and other gem owners will neither. Specifically, we have automated tests for all Ruby/Rails combinations as well.

Tests usually live in spec/ (RSpec) or test/ (used by multiple test frameworks) or features/ (Cucumber). Most gems only need unit tests (as they're code libraries, not applications) which we mostly write in RSpec.

The test setups in our gems have evolved over time, and are a bit scattered at the moment:

  • New gems use Gemika Show archive.org snapshot , a gem we built to "test a Ruby gem against multiple versions of everything". We aim to move all gems to Gemika eventually.
  • Many gems have continuous integration tests running on Github Actions.
  • A few gems still use the precursor of gemika, a set of Rake tasks that will run tests in multiple Ruby and Rails combinations, locally without Github Actions.
  • geordi Show archive.org snapshot is special, because it's rather a collection of scripts and not a library. It has partial tests in Cucumber with Aruba.

Github Actions

Github Actions is a free continuous integration service offered by Github. It runs your test suite in the background and reports test results. It is best to always make a merge request, and Github will automatically run test for that request.

The advantage of Github Actions is that it will run your tests in all combinations of Ruby and Rails, and you don't need to worry about that.

Local testing

Note that you don't need to release a gem to try it locally. Running rake install will install the gem at its current local state into the current Ruby version.

When you're using Bundler, you could also use the gem by specifying a relative path in the Gemfile of another project. E.g. gem 'my_local_gem', path: '../my_local_gem'.

Versioning

Choosing and increasing version numbers is an important part of updating a gem. While you can push anytime to Github, making it available to the world, you cannot update the current version of your gem. A new gem release requires a new version number.

The gem version is usually set in lib/$gem_name/version.rb.

We like to follow a pattern called Semantic Versioning Show archive.org snapshot , which translates to "choosing version numbers that have a meaning". It defines a three-part version number in the format x.y.z with rules on when to increase which number. In short:

  • Increase the major version (x) when you're breaking existing API
  • Increase the minor version (y) when you're adding features that are backwards compatible
  • Increase the patch version (z) when you're fixing bugs without adding features or breaking API

This helps users of your gem to understand the consequences of an update. For example, when I'm using a gem at 1.5.7 and want to make use of some features added, I would not expect an update to 1.7.2 to break anything. In contrast, an update to 3.0.0 skips two major versions, so I'd expect some trouble during update. Also, the new version hasn't received any patches yet, so I'd expect it to still have some smaller issues that will be found in the first days or weeks. I'd rather wait for 3.0.2 or so.

Options to find existing gem versions

  • Go to https://rubygems.org Show archive.org snapshot and search for the gem. Versions will be listed.
  • On Github, click "releases" next to "commits" and "branches". This nav item does not always exist, however.
  • On Github, above the file list, open the "branch" dropdown and switch to "tags". Tags in the format v0.3.2 are created when the gem is released, so this should give you a good overview.

Changelog

There are some good practices for writing a changelog that adds value, please stick to these.

Pull request

When you're done, and you're not allowed to push to master, you need to prepare a merge request (aka pull request).

Push your branch, open the target repository on Github and press "New pull request". Select your branch, choose a short, descriptive title and provide any details in the description. Assign the pull request to someone that should check and merge it.

Release

When you're done, and you're working on the master branch, you can release the gem. Before you do:

When you're ready to release, run rake release. This will:

  • Tag the latest commit with the set gem version
  • Push commits and tags to origin
  • Publish the new gem version to rubygems.org.
Dominik Schöler
Last edit
Dennis
License
Source code in this card is licensed under the MIT License.
Posted by Dominik Schöler to makandra dev (2018-07-19 06:59)