How to make changes to a Ruby gem (as a Rails developer)
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:
- no defined structure (neither for code nor directories)
- no autoloading of classes, i.e. you need to
requireall files yourself
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.
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.
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.
.gemspec file has a description of the gem. 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.
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
requireable to your Ruby code.
lib/$gem_name.rb is the entry point of your gem. Since
lib is already on your
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:
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
- src/ usually contains the raw "source" files, e.g. split up into many files, before minification
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
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
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.
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, 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 is special, because it's rather a collection of scripts and not a library. It has partial tests in Cucumber with Aruba.
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.
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'.
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
We like to follow a pattern called Semantic Versioning, 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 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.2are created when the gem is released, so this should give you a good overview.
There are some good practices for writing a changelog that adds value, please stick to these.
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.
When you're done, and you're working on the master branch, you can release the gem. Before you do:
- Do you have a clean commit history? Squash any intermediate commits.
- Have all tests passed?
- Have you updated the gem version?
- Have you updated the CHANGELOG?
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
- Publish the new gem version to rubygems.org.
The first time you release a gem, you'll need credentials for rubygems.org. Ask a colleague.