camdez.com

Rule #1: There are no rules.

Private Ruby Gem Versioning

| Comments

TLDR Don’t use version specifiers when referencing gems from git in your Gemfile. Use tags or refs instead.

Most of the time when declaring our Ruby projects’ dependencies via a Gemfile, we pull our gems from a source like RubyGems.org:

1
2
source 'https://rubygems.org'
gem 'rails', '4.1.0'

In this case you nearly always want to specify a version number for each gem to keep bundle update sane and safe.

But when you have your own branch of a public gem, or an internal project that can’t live on the public web, the typical solution is to pull it from git via a :git or :github specification:

1
2
# my (fictitious) fork of rails
gem 'rails', '4.1.0', git: 'https://github.com/camdez/rails.git'

Now, one would probably think that both of these Gemfiles precisely reference a single release of the rails gem (for their respective sources), but there turns out to be a nasty catch:

The RubyGems version always refers to the version of rails pushed as 4.1.0, but the GitHub version refers to the latest commit containing a Gemspec declaring version 4.1.0.

If you’re like most people, happily developing on master, and you’ve released version 4.1.0 of your gem and now you’re pushing changes which will eventually become 4.1.1, 4.2.0, or even 5.0.0 (you’ll decide later), all of those commits are getting pulled into projects where devs have referenced version 4.1.0 of your project via git, every time they bundle.

I nearly got burned by this because I was about to (unknowingly) push a pre-release version of a gem into production, but I happened to notice a difference in gem SHAs in the Gemfile.lock for two branches with the same Gemfile, making it clear that I didn’t have an immutable reference to a release.

So, what can you do? Well, first of all, stop using version specifications on your gems pulled via git. They’re misleading.

Next, you could go full paranoia and start referencing particular git commits via the :ref specifier. Another approach would be to change your development workflow and only merge releases into master1, but I find that to be a lot of ceremony for simple projects, and it could still support the bad habit of using unsafe version specifiers. Personally, I think the best approach is to tag all of your releases and reference them via their tags:

1
2
# my (fictitious) fork of rails
gem 'rails', git: 'https://github.com/camdez/rails.git', tag: 'v4.1.0'

This still gives you the ability to modify what commit is considered to be the release2, but affords dramatically more stability than the moving target that a version specification provides.


  1. Probably best done with git merge --no-ff so that there are never any non-release-ready commits on the branch.

  2. Obviously this should be used sparingly (if ever), like git push --force.

Comments