Private Ruby Gem Versioning
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:
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:
# 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 Gemfile
s 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:
# 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.