Your Rails and Ruby Versioning and Gemfile Policy
November 17, 2022 📬 Get My Weekly Newsletter ☞
One of the lowest-effort, highest-value practices you can adopt for sustainable development is to keep your dependencies updates frequently. The foundation for doing so is to have a clear and reasonable versioning policy. This post includes one for Rails apps that I have used for years and has served me and my co-workers well. It has some implications for specific procedures you will adopt as well.
In a nutshell: stay no more than 2 minor versions behind Ruby and Rails, and keep everything else as up to date as you can as often as you can.
Why Have a Policy?
“Policy” sounds like a dirty word that comes from the HR or security, but it’s nothing to be alarmed about. A policy is a statement of values and intention: What do we, as a team, think is important enough to write down and follow?
A written policy prevents shadow policies which create confusion amongst the team. A written policy also provides a simple way to communicate your values to people outside the team. It gives a basis to justify spending time on something that isn’t in the product backlog.
With policies go “procedures” which are often more detailed instructions on complying with the policy. These are important, too, because it can remove a lot of ambiguity about what the policy means practically. This post will talk about both, with respect to Ruby and Rails versioning.
The Policy
Ruby Versioning Policy
- Ruby versions are
MAJOR.MINOR.PATCH
. - Develop and deploy on the same version.
- The Ruby version should be the latest
MAJOR.MINOR
or the second latestMAJOR.MINOR
. At the time of this writing that would be 3.1.x or 3.0.x. - The
PATCH
version should be the latest (see section on updating below)
Rails Versioning Policy
- Rails versions are
MAJOR.MINOR.PATCH
orMAJOR.MINOR.PATCH.SUBPATCH
(this form is usually reserved for security fixes) - Develop and deploy on the same version.
- The Rails version should be the latest
MAJOR.MINOR
or the second latestMAJOR.MINOR
. At the time of this writing that would be 7.0.x or 6.1.x. - The
PATCH
andSUBPATCH
(if applicable) versions should be the latest (see section on updating below)
Other Dependencies Versioning Policy
- Use the latest release that is compatible with your versions of Ruby and Rails.
- Pin versions only to address specific incompatibility issues, and regularly re-evaluate the need for pinning the version.
Updating Versions of Ruby and Rails
- New releases that address known security vulnerabilities should be updated within 24 hours.
- When a new
PATCH
/SUBPATCH
is released, update to that as soon as possible, ideally within 1 month of the release. - Ruby’s
MAJOR.MINOR
is updated once per year, around late December. Plan time every January to update Ruby to the newly-released version. - Rails is released less predictably. When a new version is announced, plan time within the next 3 months to upgrade to that version.
- If a new
MAJOR.MINOR
version is released causing a violation of this policy (i.e. you were on the second to latest), schedule an upgrade in the next 3 months and, if possible, upgrade to the latest within the following 6 months. - Plan to reduce these “times-to-upgrade” when possible. For example, six months from now, commit
to updating
PATCH
releases every 3 weeks, andMINOR
versions every 2 months instead of 3.
Updating Other Dependencies
- New releases that address known security vulnerabilities should be updated within 24 hours.
- All other new releases should be updated-to regularly, at least once per month.
Implementing the Policy (Procedures)
The policy says what we value and what we are going to achieve. Implementing it can take many forms, and is dependent on your team and tooling.
If you don’t have anything in place, here is what I have done that works:
-
Your
Gemfile
should specify the version of Rails using the pessimistic operator and the lowest version of Rails required by your app inMAJOR.MINOR.PATCH
form:gem "rails", "~> 7.0.4"
This ensures that
bundle update
will update the patch version of Rails, but allow you to have control over when the major or minor version changes. - No other gems should have a version specifier unless the latest version is incompatible with your app in some way. You want to be on the latest version that is compatible with Rails, and since you are specifying the Rails version,
bundle update
will sort out the rest. -
If the latest version of any gem is not compatible with your app or another dependency, pin the version of the gem in your
Gemfile
(using the pessimistic operator if possible) and add a comment:# sidekiq-scheduler is not compatible with # Sidekiq 7, so we must pin to 6 for now gem "sidekiq", "~> 6.0"
Instructions on how to resolve this will go elsewhere (see next)
- Create a script to update your dependencies, called
bin/update_dependencies
:- It should initially wrap running
bundle update
and your tests. -
If you have pinned versions, this script should include specific instructions to check to see if the pinned versions can be removed. Ideally it explains exactly what the problem is and how to check if the problem has been solved. Code review on commits to the
Gemfile
should be sufficient to make sure this happens.puts "Check to see if sidekiq-scheduler has released" puts "a new version compatible with Sidekiq 7" puts puts "https://github.com/sidekiq-scheduler/sidekiq-scheduler" puts puts "Has there been an update? (Y/N)" updated = gets if updated.downcase == "Y" puts "Remove the pin from `Gemfile` and" puts "re-run this script." exit 1 end
- It should initially wrap running
- Arrange to run this script and thus update dependencies every month or more frequently. The more frequent the better.
- Run
bundle audit
in your continuous integration build or as a gate to deploying. This will prevent deploying code that has a known vulnerability and be a good forcing-function to comply with your versioning policy. - To update Rails, modify the version in
Gemfile
, then runbin/update_dependencies
. - To update Ruby depends on how you manage Ruby, deployment, and development environments.
If you use a tool like Dependebot, that can manage the general updates for you, but it won’t create a forcing-function around checking that pinned dependencies can be unpinned. If you use a tool like Dependebot, you’ll need to figure out how to periodically check on pinned dependencies.
If You Are Way Behind
The best time to update your dependencies was a long time ago. The second best time is now. I would strongly suggest you author and adopt a policy like the one in this post, even if you are nowhere near in compliance. Your policy is a goal in this case.
Once you do that, put a person or small team on the update process. You should update one
MAJOR.MINOR
version of Rails at a time, and probably update Ruby separately. Scheduling it
as a project and once you get to compliance, maintaining your dependencies will be much simpler.
Other Things That Help
Without test coverage, this process is extremely painful and time consuming. If you don’t have good test coverage, start getting it. System tests probably provide the most confidence if you can’t get good coverage at all levels, but you need tests or you can’t really manage your app.
Using a ton of gems makes managing updates harder, so be judicious in the gems you add to your app. Gems tend to be general and flexible and solve for many more cases than the one you have. It can be better to roll your own or inline just what you need. Your app and team is a system and managing it requires trade-offs. This is one of them.
Do not treat the dependency-management process as optional or as some sort of second-class thing. It should be as or more important than new features. It is a small price to pay to allow your team to easily and quickly update a dependency to address a security issue. It is also a small price to pay for hiring and retention. No one likes working on outdated code.