{"id":381,"date":"2021-01-29T23:08:04","date_gmt":"2021-01-29T23:08:04","guid":{"rendered":"http:\/\/rainforestqa.com\/upgrading-to-rails-6\/"},"modified":"2023-02-14T01:06:15","modified_gmt":"2023-02-14T01:06:15","slug":"upgrading-to-rails-6","status":"publish","type":"post","link":"https:\/\/www.rainforestqa.com\/blog\/upgrading-to-rails-6","title":{"rendered":"Upgrading our SaaS application to Rails 6"},"content":{"rendered":"\n<p>A sustainable Rails upgrade workflow that slowly but surely brought us to the point when all our apps run on latest Rails.<\/p>\n\n\n\n<p>Rainforest main application is a Rails monolith with 160k lines of code and tests. Bumping a major version of rails in a project this big has overwhelming results, in fact no rails command would even start when we first tried to just bundle update from 4 to 5. Here\u2019s what we did it to ease the upgrade pain.<\/p>\n\n\n\n<div id=\"ez-toc-container\" class=\"ez-toc-v2_0_82_2 counter-hierarchy ez-toc-counter ez-toc-custom ez-toc-container-direction\">\n<div class=\"ez-toc-title-container\">\n<p class=\"ez-toc-title\" style=\"cursor:inherit\">Contents<\/p>\n<span class=\"ez-toc-title-toggle\"><a href=\"#\" class=\"ez-toc-pull-right ez-toc-btn ez-toc-btn-xs ez-toc-btn-default ez-toc-toggle\" aria-label=\"Toggle Table of Content\"><span class=\"ez-toc-js-icon-con\"><span class=\"\"><span class=\"eztoc-hide\" style=\"display:none;\">Toggle<\/span><span class=\"ez-toc-icon-toggle-span\"><svg style=\"fill: #999;color:#999\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" class=\"list-377408\" width=\"20px\" height=\"20px\" viewBox=\"0 0 24 24\" fill=\"none\"><path d=\"M6 6H4v2h2V6zm14 0H8v2h12V6zM4 11h2v2H4v-2zm16 0H8v2h12v-2zM4 16h2v2H4v-2zm16 0H8v2h12v-2z\" fill=\"currentColor\"><\/path><\/svg><svg style=\"fill: #999;color:#999\" class=\"arrow-unsorted-368013\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"10px\" height=\"10px\" viewBox=\"0 0 24 24\" version=\"1.2\" baseProfile=\"tiny\"><path d=\"M18.2 9.3l-6.2-6.3-6.2 6.3c-.2.2-.3.4-.3.7s.1.5.3.7c.2.2.4.3.7.3h11c.3 0 .5-.1.7-.3.2-.2.3-.5.3-.7s-.1-.5-.3-.7zM5.8 14.7l6.2 6.3 6.2-6.3c.2-.2.3-.5.3-.7s-.1-.5-.3-.7c-.2-.2-.4-.3-.7-.3h-11c-.3 0-.5.1-.7.3-.2.2-.3.5-.3.7s.1.5.3.7z\"\/><\/svg><\/span><\/span><\/span><\/a><\/span><\/div>\n<nav><ul class='ez-toc-list ez-toc-list-level-1 ' ><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-1\" href=\"https:\/\/www.rainforestqa.com\/blog\/upgrading-to-rails-6\/#Rails_at_Rainforest_QA\" >Rails at Rainforest QA<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-2\" href=\"https:\/\/www.rainforestqa.com\/blog\/upgrading-to-rails-6\/#The_Upgrade_Workflow\" >The Upgrade Workflow<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-3\" href=\"https:\/\/www.rainforestqa.com\/blog\/upgrading-to-rails-6\/#Final_Thoughts\" >Final Thoughts<\/a><\/li><\/ul><\/nav><\/div>\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"Rails_at_Rainforest_QA\"><\/span>Rails at Rainforest QA<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p>Rainforest started in 2013 as a Rails 3 application. We\u2019ve been following patches and upgraded to Rails 4 in 2014, but things started going downhill when Rails 5 was released in 2016 while Rainforest remained behind. The scope of changes required to upgrade kept growing and we didn\u2019t make any attempts until almost 3 years later. Our Rails 5 upgrade went live in July 2019 and was followed by consecutive migrations to Rails 5.1, 5.2 and then 6.0 in July 2020.<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/uploads-ssl.webflow.com\/60da68c37e5767dfb65004c0\/61f1b9c08d7362247ff7b102_timeline.png\" alt=\"A rough timeline of Rainforest Rails versions vs. Rails official releases\"\/><figcaption class=\"wp-element-caption\">A rough timeline of Rainforest Rails versions vs. Rails official releases<\/figcaption><\/figure>\n\n\n\n<p>Even though it was a conscious decision to focus on other projects in 2017-2019, allowing the app to stay so far behind accrued a lot of technical debt. In fact, the priorities haven\u2019t changed since but we found a way to upgrade that doesn\u2019t interrupt product work and helps us detect any breaking changes early. The fact that we test Rainforest extensively with both RSpec and a suite of <a href=\"https:\/\/rainforestqa.com\/\">Rainforest Tests<\/a> played a key role too.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"The_Upgrade_Workflow\"><\/span>The Upgrade Workflow<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Step 1: Avoid a long-lived upgrade branch with dual-boot<\/h3>\n\n\n\n<p>One of our first learnings was that a long-lived upgrade feature branch is not an option, being too time consuming and risky. Instead, we came up with a slower but non-disruptive upgrade process using the BUNDLE_GEMFILE option and dual-gemfile approach.<\/p>\n\n\n\n<p>Here\u2019s the setup:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># main Gemfile\ngem \"rails\", \"~> 5.2\"\n\n\n# Gemfile_NEXT\ngem \"rails\", \"~> 6.0\"<\/code><\/pre>\n\n\n\n<p>With all other gems copied to <strong>Gemfile_NEXT<\/strong>, Bundler could install all <strong>NEXT<\/strong> dependencies and save them in a new lockfile without affecting any of the original production bundle:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>BUNDLE_GEMFILE=Gemfile_NEXT bundle<\/code><\/pre>\n\n\n\n<p>Great, now we can run specs against Rails 6 with:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>BUNDLE_GEMFILE=Gemfile_NEXT bundle exec rspec<\/code><\/pre>\n\n\n\n<p>That\u2019s the idea. In practice, our application wouldn\u2019t even complete<strong> bundle install<\/strong>, because of so many gem version incompatibilities. It was the time to comment out the offending gems from <strong>Gemfile_NEXT<\/strong>. Yes, the app wasn\u2019t functional at this stage and many (actually, thousands of) specs were failing, but it can be dealt with later. We completed the first milestone: that any developer could install <strong>NEXT<\/strong> dependencies and start a functional <strong>rails console<\/strong> to test things out.<\/p>\n\n\n\n<p>This setup was soon merged without affecting any production code and lead to the benefit of no long-lived upgrade branch becoming a nightmare to merge. Then, we could work on small incremental PRs to address issues one-by-one and prepare <strong>Gemfile_NEXT<\/strong> for release.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Step 2: Wire up CI\/CD<\/h3>\n\n\n\n<p>Our dual-boot means we can test each new feature branch against both the production <strong>Gemfile<\/strong> and <strong>Gemfile_NEXT<\/strong>, to make sure we\u2019re getting closer to our upgrade goal as the team keeps working on new features day to day. Fortunately, it was easy to duplicate our existing <strong>test<\/strong> CircleCI config and just add a <strong>BUNDLE_GEMFILE<\/strong> environment variable to run tests against the new version.<\/p>\n\n\n\n<p>The optional <strong>test_rails_next<\/strong> build check verifies that future code changes are compatible with both <strong>Gemfile<\/strong> and <strong>Gemfile_NEXT<\/strong>:<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/uploads-ssl.webflow.com\/60da68c37e5767dfb65004c0\/61f1b9c06b4cfd659e2bd0ce_build-check.png\" alt=\"build check\"\/><\/figure>\n\n\n\n<p>The caveat here is that we started off far from a green <strong>test_rails_next<\/strong> build with all the failures and Rails deprecations still not fixed.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Step 3: Manage and exclude Gemfile_NEXT failures<\/h3>\n\n\n\n<p>Once we had a failing test suite we wanted to get it passing as soon possible so that we could spot new bugs introduced by our ongoing work on the product, and skip breaking changes and deprecation errors until there was time to address each one. To do this we added a <strong>Rainforest.rails_next?<\/strong> method:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># config\/application.rb\nmodule Rainforest\n  def self.rails_next?\n    Gem::Version.new(Rails.version) > Gem::Version.new('5.0.2.1')\n  end\nend<\/code><\/pre>\n\n\n\n<p><em>\u200d<\/em>This method was used by a <strong>fixme_rails_next<\/strong> RSpec tag that we added to specs failing on <strong>Gemfile_NEXT<\/strong>. We could see how many specs there are left to address by running <strong>rspec &#8211;dry-run &#8211;format=documentation &#8211;tag fixme_rails_next<\/strong> and exclude the failing ones from default spec runs with:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># spec\/spec_helper.rb\nRSpec.configure do |config|\n  config.filter_run_excluding(fixme_rails_next: true) if Rainforest.rails_next?\nend<\/code><\/pre>\n\n\n\n<p><em>\u200d<\/em>On top of that, one more build step was added to our CI that ensures any gem updates we do in <strong>Gemfile<\/strong> are reflected in <strong>Gemfile_NEXT<\/strong>. The CI script you can see below will fail with a message like <strong>ERROR: GEMFILE_NEXT needs to update grape from 1.1.0 to at least 1.3.0<\/strong> in case someone forgot to reflect an update to <strong>Gemfile<\/strong> in <strong>Gemfile_NEXT<\/strong>.<\/p>\n\n\n\n<p>This method turned out useful as we had to update other dependencies while the Rails upgrade process was in flight and here\u2019s what that script looks like:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># frozen_string_literal: true\n# rubocop:disable Lint\/NoENV\n\n# This script can take two Gemfile.locks and compare them.\n# The command will print a diff between added\/removed gems\n# and exit 1 if any of the GEMFILE_NEW gems is stale w.r.t. to GEMFILE_OLD.\n# Use EXCLUDED_GEMS (comma separated list) to ignore certain gems from validation.\n\nrequire 'bundler'\n\ndef fetch_gems_and_versions(lockfile_name)\n  lockfile = Bundler::LockfileParser.new(Bundler.read_file(lockfile_name))\n  lockfile.specs.map do |spec|\n    &#91;spec.name, spec.version]\n  end.to_h\nend\n\ndef usage\n  puts \"Validates differences between two Gemfile locks\\n\" \\\n       'Usage: ruby script\/validate_gemfile_next.rb GEMFILE_OLD.lock GEMFILE_NEW.lock'\nend\n\nunless ARGV.length == 2\n  usage\n  exit 1\nend\n\nold_bundle = fetch_gems_and_versions(ARGV&#91;0])\nnew_bundle = fetch_gems_and_versions(ARGV&#91;1])\n\nremoved_gems = old_bundle.keys - new_bundle.keys\nputs \"WARN: Removed in GEMFILE_NEW: #{removed_gems.join(', ')}\" if removed_gems.any?\nadded_gems = new_bundle.keys - old_bundle.keys\nputs \"WARN: Added in GEMFILE_NEW: #{added_gems.join(', ')}\" if added_gems.any?\n\nEXCLUDED_GEMS = ENV&#91;'EXCLUDED_GEMS'].to_s.downcase.split(',')\n\nstatus = 0\nold_bundle.each do |gem_name, old_ver|\n  new_ver = new_bundle&#91;gem_name]\n  if new_ver &amp;&amp; old_ver > new_ver\n    if EXCLUDED_GEMS.include?(gem_name)\n      puts \"WARN: excluding #{gem_name}: #{old_ver} vs #{new_ver}\"\n      next\n    end\n\n    puts \"ERROR: GEMFILE_NEW needs to update #{gem_name} from #{new_ver} \" \\\n         \"to at least #{old_ver}\"\n    status = 1\n  end\nend\n\nif status == 1\n  puts \"\\nUpdate the Gemfile_NEXT.lock file by running `BUNDLE_GEMFILE=Gemfile_NEXT bundle ...`\"\nend\n\nexit status<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Step 4:&nbsp;Config, dependencies, and deprecations<\/h3>\n\n\n\n<p>The above three steps have established a process to work on multiple short-lived changes to tick all the remaining items off an upgrade workflow checklist.<\/p>\n\n\n\n<p>Talking about the 5 to 6 migration specifically, it started by working through all config changes generated by <strong>rake app:update<\/strong>. Those included the migration from <strong>sprockets<\/strong> to <strong>zeitwerk<\/strong> for constant autoloading which only required defining a few custom constant inflectors and fixing a few namespace locations that the new autoloader found confusing.<\/p>\n\n\n\n<p>Next came dependency updates. We updated our in-house gems like <strong>queue_classic<\/strong> to support Rails 6 and bumped other gem dependencies (<strong>state_machines <\/strong>and <strong>grape<\/strong> were probably the most risky and time consuming). We also decided to drop a dependency on <strong>schema_validations<\/strong> which isn\u2019t compatible with Rails 6 and made little sense to keep. This gem dynamically infers ActiveModel validations from the database schema constraints, so in order to remove it safely we wrote a <a href=\"https:\/\/gist.github.com\/grodowski\/5f828c2ae77f401f817d2c0c3f85be9b\" target=\"_blank\" rel=\"noopener\">code generator<\/a> that stored all of these implicit validators as declarations within each class in the <strong>app\/models\/ <\/strong>directory.<\/p>\n\n\n\n<p>The dependency updates let us remove a big portion of <strong>rails_next?<\/strong> exclusions both in the codebase and specs, but those final ones required working through failures and addressing API changes and deprecations one-by-one to get to a green <strong>Gemfile_NEXT<\/strong> build with zero <strong>fixme_rails_next<\/strong> exclusions.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Step 5: Run regression tests with RainforestQA<\/h3>\n\n\n\n<p>Once the build was green, we did one more thing before <strong>Gemfile_NEXT<\/strong> finally replaced the \u201cold\u201d <strong>Gemfile<\/strong>: run the entire Rainforest test suite, which is comprised of 250+ Rainforest test scenarios ran both by crowdsourced human testers as well as our visual automation technology (learn more on our <a href=\"https:\/\/www.rainforestqa.com\/\">website<\/a>). In case of Rails upgrades, we ran the entire suite instead of a subset of about 100 tests normally included in every backend release, which ensured highest confidence of no functional regressions being present in the final upgrade PR.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"Final_Thoughts\"><\/span>Final Thoughts<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Stale dependencies are technical debt<\/h3>\n\n\n\n<p>Small changes with limited scope are easier to ship and have smaller potential blast radius if things go wrong. Once the scope becomes too big, the upgrade project suddenly turns into a nightmare: a project impossible to deliver without jeopardising other product initiatives. We\u2019ve been there: this is what blocked Rainforest from upgrading from Rails 4 to 5 for a long time. Fortunately, the upgrade process you\u2019ve just read about helped us pay back the technical debt and future-proof our codebase.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Rainforest is ready for future upgrades<\/h3>\n\n\n\n<p>Even though we\u2019re currently on the latest version of Rails, <strong>test_rails_next<\/strong> has still a place in our build pipeline and points to the <strong>6.1-stable branch<\/strong>. This way we can test every change against the cutting edge, get notified about any breaking changes quickly, and also ensure the upgrade process we described here keeps working as expected.<\/p>\n\n\n\n<p>To recap, these are the main concepts to help you ensure sustainable and stress-free upgrades:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Dual-boot with <strong>BUNDLE_GEMFILE=Gemfile_NEXT<\/strong><\/li>\n\n\n\n<li>Keep a ToDo list of breaking changes, deprecations and <strong>fixme_rails_next<\/strong> exclusions that can be addressed in small chunks<\/li>\n\n\n\n<li>Update <strong>Gemfile<\/strong> dependencies often, e.g. with automated help from Dependabot<\/li>\n\n\n\n<li>Cover your application with an adequate number of unit tests. We use <strong>rspec<\/strong> and <strong>undercover<\/strong> (<a href=\"https:\/\/github.com\/grodowski\/undercover\" target=\"_blank\" rel=\"noopener\">try it on GitHub<\/a>) for ensuring test coverage for all new changes<\/li>\n\n\n\n<li>Finally, use <a href=\"https:\/\/rainforestqa.com\/\">Rainforest<\/a> for functional regression testing on every release<\/li>\n<\/ol>\n\n\n\n<p>If you\u2019re looking for more good content about Rails upgrades, check out these links:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><a href=\"https:\/\/www.youtube.com\/watch?v=6aCfc0DkSFo\" target=\"_blank\" rel=\"noopener\">RailsConf 2018: Ten years of Rails upgrades by Jordan Raine<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/shopify.engineering\/upgrading-shopify-to-rails-5-0\" target=\"_blank\" rel=\"noopener\">Upgrading Shopify to Rails 5<\/a><\/li>\n<\/ul>\n\n\n\n<p>Thanks for reading, I hope this article helped with your Rails upgrade journey!<\/p>\n\n\n\n<p>\u200d<\/p>\n","protected":false},"excerpt":{"rendered":"<p>A sustainable Rails upgrade workflow that slowly but surely brought us to the point when all our apps run on latest Rails. Rainforest main application is a Rails monolith with 160k lines of code and tests. Bumping a major version of rails in a project this big has overwhelming results, in fact no rails command [&hellip;]<\/p>\n","protected":false},"author":4,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"content-type":"","inline_featured_image":false,"footnotes":""},"categories":[6],"tags":[],"class_list":["post-381","post","type-post","status-publish","format-standard","hentry","category-engineering"],"acf":[],"_links":{"self":[{"href":"https:\/\/www.rainforestqa.com\/blog\/wp-json\/wp\/v2\/posts\/381","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.rainforestqa.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.rainforestqa.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.rainforestqa.com\/blog\/wp-json\/wp\/v2\/users\/4"}],"replies":[{"embeddable":true,"href":"https:\/\/www.rainforestqa.com\/blog\/wp-json\/wp\/v2\/comments?post=381"}],"version-history":[{"count":3,"href":"https:\/\/www.rainforestqa.com\/blog\/wp-json\/wp\/v2\/posts\/381\/revisions"}],"predecessor-version":[{"id":836,"href":"https:\/\/www.rainforestqa.com\/blog\/wp-json\/wp\/v2\/posts\/381\/revisions\/836"}],"wp:attachment":[{"href":"https:\/\/www.rainforestqa.com\/blog\/wp-json\/wp\/v2\/media?parent=381"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.rainforestqa.com\/blog\/wp-json\/wp\/v2\/categories?post=381"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.rainforestqa.com\/blog\/wp-json\/wp\/v2\/tags?post=381"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}