Ten Years of Rails Upgrades

Ten Years of Rails Upgrades

Presented at RailsConf 2018 in Pittsburgh, PA.

Upgrading Rails can go from easy-to-hard quickly. If you've struggled to upgrade to a new version of Rails, you're not alone. And yet, with useful deprecation warnings and extensive beta periods, Rails has never made it easier to upgrade. So what makes it hard?

By looking at the past ten years of Rails upgrades at Clio (and other notable apps), let's see what we can learn. Gain insight into the tradeoffs between different timelines and approaches and learn practical ways to keep your app up-to-date.

2c10bda8a39a60ab149e4e4ccde3e365?s=128

Jordan Raine

April 19, 2018
Tweet

Transcript

  1. Ten Years of Rails Upgrades Jordan Raine @jnraine

  2. Jordan Raine (@jnraine) I’m a Rails developer at Clio From

    Vancouver, BC ! " # Ten Years of Rails Upgrades
  3. Why is it still so tough?

  4. None
  5. Tiny Co. / Big Inc.

  6. $ Tiny Co.

  7. None
  8. % Big Inc.

  9. $ Tiny Co. % Big Inc. 1. Gems Few Many

    2. Code changes Few Many 3. Test Few failures Won’t even run 4. Deploy Easy Terrifying
  10. $ Tiny Co. % Big Inc. 1. Gems ✅ ✅

    2. Code changes ✅ ✅ 3. Test ✅ ✅ 4. Deploy ✅ ✅
  11. None
  12. 2010 2011 2012 2013 2014 2015 2009 2008 2007 2016

    2017 2018 2.0 3.0 3.1 3.2 2.1 2.2 2.3 2.0 2.1 2.2 2.3 3.0 3.1 3.2 4.0 4.2 4.1 5.0 5.1 5.2 4.0 4.1 4.2
  13. 2010 2011 2012 2013 2014 2015 2009 2008 2007 2016

    2017 2018 2.0 3.0 3.1 3.2 2.1 2.2 2.3 2.0 2.1 2.2 2.3 3.0 3.1 3.2 4.0 4.2 4.1 5.0 5.1 5.2 4.0 4.1 4.2
  14. '•More developers
 •More features
 •More customers
 •More revenue

  15. 2010 2011 2012 2013 2014 2015 2009 2008 2007 2016

    2017 2018 2.0 3.0 3.1 3.2 2.1 2.2 2.3 2.0 2.1 2.2 2.3 3.0 3.1 3.2 4.0 4.2 4.1 5.0 5.1 5.2 4.0 4.1 4.2
  16. Problems Tiny Co. / Big Inc.

  17. Gemfile

  18. Dependencies aren’t free.

  19. Find the dependencies – and eliminate them. https://www.joelonsoftware.com/2001/10/14/in-defense-of-not-invented-here-syndrome/ “ ”

    Joel On Software
  20. The Excel team’s ruggedly independent mentality also meant that they

    always shipped on time [and] their code was of uniformly high quality. Joel On Software https://www.joelonsoftware.com/2001/10/14/in-defense-of-not-invented-here-syndrome/ “ ”
  21. gem "quickbooks-ruby" gem "oauth"

  22. gem "quickbooks-ruby" gem "oauth"

  23. gem "quickbooks-ruby" gem "oauth", "0.x.x"

  24. a) Don’t upgrade any gems b) Upgrade both gems c)

    Fork a gem and downgrade
  25. a) Don’t upgrade any gems b) Upgrade both gems c)

    Fork a gem and downgrade
  26. gem "quickbooks-ruby", github: "<fork>/quickbooks-ruby", ref gem "oauth", "0.x.x"

  27. Rigid dependencies are difficult to maintain.

  28. None
  29. None
  30. None
  31. class MyController < ApplicationController - before_filter :require_login + before_action :require_login

    end
  32. class MyModel < ApplicationRecord attr_accessible :name, :description # ... end

  33. None
  34. protected_attributes 4,947,703 activerecord >= 5.0 15,350,275

  35. We like protected_attributes 1/3 as much as activerecord.

  36. None
  37. 2010 2011 2012 2013 2014 2015 2009 2008 2007 2016

    2017 2018 2.0 3.0 3.1 3.2 2.1 2.2 2.3 2.0 2.1 2.2 2.3 3.0 3.1 3.2 4.0 4.2 4.1 5.0 5.1 5.2 4.0 4.1 4.2 ( ( (
  38. Tiny Co. / Big Inc. Problems Close the gaps

  39. None
  40. ) Keep the Gemfile healthy * Outlaw deprecations + Canary

    Rails
  41. ) Keep the Gemfile healthy * Outlaw deprecations + Canary

    Rails
  42. gem "multi_json", "1.9.1" gem "mime-types", "~> 2.0"

  43. gem "multi_json", "1.9.1" gem "mime-types", "~> 2.0" Drop those version

    constraints!
  44. gem "multi_json" gem "mime-types" Drop those version constraints! ,

  45. gem "quickbooks-ruby", github: "<fork>/quickbooks-ruby",

  46. gem "quickbooks-ruby", github: "<fork>/quickbooks-ruby", Kill your forks!

  47. gem "quickbooks-ruby" , Kill your forks!

  48. Culture of updating gems $ bundle update multi_json mime-types

  49. $ bundle outdated

  50. $ bundle outdated Outdated gems included in the bundle: *

    actionmailer (newest 5.2.0, installed 5.1.4, requested ~> 5.1) in groups "default" * actionpack (newest 5.2.0, installed 5.1.4, requested ~> 5.1) in groups "default" * actionview (newest 5.2.0, installed 5.1.4, requested ~> 5.1) in groups "default" * active_model_serializers (newest 0.10.7, installed 0.8.3, requested ~> 0.8.3) in groups "default" * activejob (newest 5.2.0, installed 5.1.4) * activemodel (newest 5.2.0, installed 5.1.4, requested ~> 5.1) in groups "default" * activerecord (newest 5.2.0, installed 5.1.4, requested ~> 5.1) in groups "default"
  51. actionmailer (newest 5.2.0, installed 5.1.4, requested ~> 5.1) in groups

    "default" name newest version installed version requested version group
  52. $ bundle outdated bundle outdated Fetching gem metadata from https://rubygems.org/...............

    Fetching gem metadata from https://rubygems.org/.. Resolving dependencies........... Outdated gems included in the bundle: * actionmailer (newest 5.2.0, installed 5.1.4, requested ~> 5.1) in groups "default" * actionpack (newest 5.2.0, installed 5.1.4, requested ~> 5.1) in groups "default" * actionview (newest 5.2.0, installed 5.1.4, requested ~> 5.1) in groups "default" * active_model_serializers (newest 0.10.7, installed 0.8.3, requested ~> 0.8.3) in groups "default" * activejob (newest 5.2.0, installed 5.1.4) * activemodel (newest 5.2.0, installed 5.1.4, requested ~> 5.1) in groups "default" * activerecord (newest 5.2.0, installed 5.1.4, requested ~> 5.1) in groups "default" * activesupport (newest 5.2.0, installed 5.1.4, requested ~> 5.1) in groups "default" * addressable (newest 2.5.2, installed 2.5.1) * arel (newest 9.0.0, installed 8.0.0) * aws-partitions (newest 1.80.0, installed 1.24.0) * aws-sdk-core (newest 3.19.0, installed 3.6.0) * aws-sdk-kms (newest 1.5.0, installed 1.2.0) * aws-sdk-s3 (newest 1.9.0, installed 1.4.0) in groups "default" * barber (newest 0.12.0, installed 0.11.2) in groups "default" * better_errors (newest 2.4.0, installed 2.1.1) in groups "development" * bootsnap (newest 1.3.0, installed 1.1.8) in groups "default" * bullet (newest 5.7.5, installed 5.5.1) in groups "development" * byebug (newest 10.0.2, installed 9.0.6) in groups "test, development" * chunky_png (newest 1.3.10, installed 1.3.8) * crass (newest 1.0.4, installed 1.0.3) * ember-data-source (newest 3.0.2, installed 2.2.1) * ember-handlebars-template (newest 0.8.0, installed 0.7.5, requested = 0.7.5) in groups "default" * ember-rails (newest 0.21.0, installed 0.18.5, requested = 0.18.5) in groups "default" * ember-source (newest 2.18.2, installed 2.13.3, requested = 2.13.3) in groups "default" * erubi (newest 1.7.1, installed 1.6.1) * excon (newest 0.62.0, installed 0.56.0) in groups "default" * exifr (newest 1.3.3, installed 1.2.5) * fabrication (newest 2.20.1, installed 2.9.8, requested = 2.9.8) in groups "test, development" * faraday (newest 0.14.0, installed 0.11.0) * ffi (newest 1.9.23, installed 1.9.18) * globalid (newest 0.4.1, installed 0.4.0) * hashdiff (newest 0.3.7, installed 0.3.4) * hashie (newest 3.5.7, installed 3.5.5) * highline (newest 1.7.10, installed 1.7.8) in groups "default" * http_accept_language (newest 2.1.1, installed 2.0.5, requested ~> 2.0.5) in groups "default" * i18n (newest 1.0.0, installed 0.8.6) * in_threads (newest 1.5.0, installed 1.4.0) * jmespath (newest 1.4.0, installed 1.3.1) * jwt (newest 2.1.0, installed 1.5.6) * kgio (newest 2.11.2, installed 2.11.1) * lograge (newest 0.10.0, installed 0.7.1) in groups "default" * logstash-logger (newest 0.26.1, installed 0.25.1) in groups "default" * method_source (newest 0.9.0, installed 0.8.2) * mini_mime (newest 1.0.0, installed 0.1.3) in groups "default" * minitest (newest 5.11.3, installed 5.10.3) in groups "test" * mocha (newest 1.5.0, installed 1.2.1) in groups "test, development" * mock_redis (newest 0.18.0, installed 0.17.3) in groups "test, development" * oauth (newest 0.5.4, installed 0.5.1) * oauth2 (newest 1.4.0, installed 1.3.1) * oj (newest 3.5.1, installed 3.4.0) in groups "default" * omniauth (newest 1.8.1, installed 1.6.1) in groups "default" * omniauth-facebook (newest 5.0.0, installed 4.0.0) in groups "default" * omniauth-google-oauth2 (newest 0.5.3, installed 0.3.1) in groups "default" * omniauth-instagram (newest 1.3.0, installed 1.0.2) in groups "default" * omniauth-oauth2 (newest 1.5.0, installed 1.4.0) in groups "default" * omniauth-twitter (newest 1.4.0, installed 1.3.0) in groups "default" * parser (newest 2.5.1.0, installed 2.5.0.3) * pg (newest 1.0.0, installed 0.21.0, requested ~> 0.21.0) in groups "default" * progress (newest 3.4.0, installed 3.3.1) * pry (newest 0.11.3, installed 0.10.4) * pry-rails (newest 0.3.6, installed 0.3.4) in groups "default" * public_suffix (newest 3.0.2, installed 2.0.5) * puma (newest 3.11.4, installed 3.9.1) in groups "default" * r2 (newest 0.2.7, installed 0.2.6, requested ~> 0.2.5) in groups "default" * rack-openid (newest 1.4.2, installed 1.3.1) * rack-test (newest 1.0.0, installed 0.7.0) * railties (newest 5.2.0, installed 5.1.4, requested ~> 5.1) in groups "default" * rake (newest 12.3.1, installed 12.3.0) in groups "default" * rb-fsevent (newest 0.10.3, installed 0.9.8) in groups "test, development" * rb-inotify (newest 0.9.10, installed 0.9.8, requested ~> 0.9) in groups "test, development" * redis (newest 4.0.1, installed 3.3.5) in groups "default" * redis-namespace (newest 1.6.0, installed 1.5.3) in groups "default" * request_store (newest 1.4.1, installed 1.3.2) * rinku (newest 2.0.4, installed 2.0.2) in groups "default" * rotp (newest 3.3.1, installed 3.3.0) in groups "default" * rspec (newest 3.7.0, installed 3.6.0) in groups "test, development" * rspec-core (newest 3.7.1, installed 3.6.0) * rspec-expectations (newest 3.7.0, installed 3.6.0) * rspec-mocks (newest 3.7.0, installed 3.6.0) * rspec-rails (newest 3.7.2, installed 3.6.1) in groups "test, development" * rspec-support (newest 3.7.1, installed 3.6.0) * rubocop (newest 0.54.0, installed 0.53.0) in groups "test, development" * ruby-prof (newest 0.17.0, installed 0.16.2) in groups "development" * sass (newest 3.5.6, installed 3.4.24) * sassc (newest 1.11.4, installed 1.11.2) in groups "default" * seed-fu (newest 2.3.9, installed 2.3.7) in groups "default" * shoulda-matchers (newest 3.1.2, installed 2.8.0) * sidekiq (newest 5.1.3, installed 5.0.5) in groups "default" * slop (newest 4.6.2, installed 3.6.0) * sprockets-rails (newest 3.2.1, installed 3.2.0) in groups "default" * stackprof (newest 0.2.11, installed 0.2.10) in groups "default" * thor (newest 0.20.0, installed 0.19.4) in groups "default" * tilt (newest 2.0.8, installed 2.0.7) in groups "default" * tzinfo (newest 1.2.5, installed 1.2.3) * uglifier (newest 4.1.9, installed 3.2.0) in groups "assets" * unf_ext (newest 0.0.7.5, installed 0.0.7.4) * uniform_notifier (newest 1.11.0, installed 1.10.0) where do I start? • what is most important? • which are the oldest? • what is unmaintained?
  53. $ bin/bundle_report

  54. $ bin/bundle_report outdated

  55. $ bin/bundle_report outdated | head -n 5 rack-openid 1.3.1: released

    about 7 years ago (latest version, 1.4.2, released about 4 years ago) method_source 0.8.2: released over 4 years ago (latest version, 0.9.0, released 7 months ago) fabrication 2.9.8: released about 4 years ago (latest version, 2.20.1, released 3 months ago) slop 3.6.0: released over 3 years ago (latest version, 4.6.2, released about 1 month ago) active_model_serializers 0.8.3: released over 3 years ago (latest version, 0.10.7, released 5 months ago)
  56. rack-openid 1.3.1: released about 7 years ago (latest version, 1.4.2,

    released about 4 years ago) name installed version release date newest version release date
  57. $ bin/bundle_report compatibility --rails-version=5.2.0

  58. $ bin/bundle_report compatibility --rails-version=5.2.0

  59. $ bin/bundle_report compatibility --rails-version 5.2.0 => Incompatible with Rails 5.2.0

    (with new versions that are compatible): These gems will need to be upgraded before upgrading to Rails 5.2.0. lograge 0.7.1 - upgrade to 0.10.0 => Incompatible with Rails 5.2.0 (with no new compatible versions): These gems will need to be removed or replaced before upgrading to Rails 5.2.0. jquery-rails 4.3.1 - new version, 4.3.1, is not compatible with Rails 5.2.0 2 gems incompatible with Rails 5.2.0
  60. $ bin/bundle_report compatibility --rails-version 5.2.0 => Incompatible with Rails 5.2.0

    (with new versions that are compatible): These gems will need to be upgraded before upgrading to Rails 5.2.0. lograge 0.7.1 - upgrade to 0.10.0 => Incompatible with Rails 5.2.0 (with no new compatible versions): These gems will need to be removed or replaced before upgrading to Rails 5.2.0. jquery-rails 4.3.1 - new version, 4.3.1, is not compatible with Rails 5.2.0 2 gems incompatible with Rails 5.2.0
  61. $ bin/bundle_report compatibility --rails-version 5.2.0 => Incompatible with Rails 5.2.0

    (with new versions that are compatible): These gems will need to be upgraded before upgrading to Rails 5.2.0. lograge 0.7.1 - upgrade to 0.10.0 => Incompatible with Rails 5.2.0 (with no new compatible versions): These gems will need to be removed or replaced before upgrading to Rails 5.2.0. jquery-rails 4.3.1 - new version, 4.3.1, is not compatible with Rails 5.2.0 2 gems incompatible with Rails 5.2.0
  62. $ bin/bundle_report compatibility --rails-version 5.2.0 => Incompatible with Rails 5.2.0

    (with new versions that are compatible): These gems will need to be upgraded before upgrading to Rails 5.2.0. lograge 0.7.1 - upgrade to 0.10.0 => Incompatible with Rails 5.2.0 (with no new compatible versions): These gems will need to be removed or replaced before upgrading to Rails 5.2.0. jquery-rails 4.3.1 - new version, 4.3.1, is not compatible with Rails 5.2.0 2 gems incompatible with Rails 5.2.0
  63. ) Keep the Gemfile healthy * Outlaw deprecations + Canary

    Rails
  64. ) Keep the Gemfile healthy * Outlaw deprecations + Canary

    Rails
  65. User.find(current_user) # DEPRECATION WARNING: You are passing an instance #

    of ActiveRecord::Base to `find`. Please pass the # id of the object by calling `.id`. (called from <main> at (
  66. ActiveSupport::Deprecation.behavior = :raise

  67. if ENV["DEPRECATION_TRACKER"] DeprecationTracker.track_rspec( config, shitlist_path: "deprecation_warning.shitlist.json", mode: ENV["DEPRECATION_TRACKER"] ) end

  68. $ DEPRECATION_TRACKER=save rspec deprecations.shitlist.json $ DEPRECATION_TRACKER=compare rspec -

  69. None
  70. None
  71. { "./spec/controllers/post_controller_spec.rb": [ "DEPRECATION WARNING: env is deprecated and will

    be remov ], "./spec/controllers/session_controller_spec.rb": [ "DEPRECATION WARNING: `xhr` and `xml_http_request` are de "DEPRECATION WARNING: env is deprecated and will be remov ], "./spec/controllers/accounts_controller_spec.rb": [ "DEPRECATION WARNING: Using positional arguments in funct ] }
  72. { "./spec/controllers/post_controller_spec.rb": [ "DEPRECATION WARNING: env is deprecated and will

    be remov ], "./spec/controllers/session_controller_spec.rb": [ "DEPRECATION WARNING: `xhr` and `xml_http_request` are de "DEPRECATION WARNING: env is deprecated and will be remov ], "./spec/controllers/accounts_controller_spec.rb": [ "DEPRECATION WARNING: Using positional arguments in funct ] }
  73. { "./spec/controllers/post_controller_spec.rb": [ "DEPRECATION WARNING: env is deprecated and will

    be remov ], "./spec/controllers/session_controller_spec.rb": [ "DEPRECATION WARNING: `xhr` and `xml_http_request` are de "DEPRECATION WARNING: env is deprecated and will be remov ], "./spec/controllers/accounts_controller_spec.rb": [ "DEPRECATION WARNING: Using positional arguments in funct ] }
  74. $ bin/deprecations info

  75. $ bin/deprecations info Ten most common deprecation warnings: Occurrences: 608

    DEPRECATION WARNING: Using positional arguments in functional tests has been deprecated, in favor of keyword arguments, and will be removed in Rails 5.1. Deprecated style: get :show, { id: 1 }, nil, { notice: "This is a flash message" } New keyword style: get :show, params: { id: 1 }, flash: { notice: "This is a flash message" }, session: nil # Can safely be omitted. (called from block (3 levels) in <main> at spec/foo.rb:67) ---------- Occurrences: 553 DEPRECATION WARNING: Passing an argument to force an association to reload is now deprecated and will be removed in Rails 5.1. Please call `reload_active_al_subscription` instead. (called from reload_subscriptions at app/models/foo.rb:270) # ...
  76. $ bin/deprecations run

  77. $ bin/deprecations run --pattern "ActiveRecord::Base"

  78. $ bin/deprecations run --tracker-mode save

  79. ActiveSupport::Deprecation.behavior = :raise

  80. ) Keep the Gemfile healthy * Outlaw deprecations + Canary

    Rails
  81. ) Keep the Gemfile healthy * Outlaw deprecations + Dual-boot

    Rails next
  82. “current” Rails “next” Rails 4.2 5.0 5.0 5.1 5.2 master

  83. $ bin/next rails s $ bin/next rspec $ DEPRECATION_TRACKER=save bin/next

    rspec
  84. $ bin/next rails s $ bin/next rspec $ DEPRECATION_TRACKER=save bin/next

    rspec
  85. $ bin/next rails s $ bin/next rspec $ DEPRECATION_TRACKER=save bin/next

    rspec
  86. -old_deprecated_thing +new_thing_that_works_in_both_versions

  87. +if Rails.application.next? + do_new_thing +else + do_old_thing +end -do_old_thing

  88. Gemfile

  89. Gemfile Gemfile.next

  90. # top of Gemfile def next? File.basename(__FILE__) == "Gemfile.next" end

    # later if next? gem "rails", "5.2.0" else gem "rails", "5.1.6" end
  91. # top of Gemfile def next? File.basename(__FILE__) == "Gemfile.next" end

    # later if next? gem "rails", "5.2.0" else gem "rails", "5.1.6" end
  92. # top of Gemfile def next? File.basename(__FILE__) == "Gemfile.next" end

    # later if next? gem "rails", "5.2.0" else gem "rails", "5.1.6" end
  93. Gemfile.next Gemfile.next.lock Gemfile.lock Gemfile

  94. $ bin/next --init

  95. #!/bin/bash export BUNDLE_GEMFILE=Gemfile.next export BUNDLE_CACHE_PATH=vendor/cache.next if [[ "${@}" =~ ^bundle

    ]]; then $@ else bundle exec $@ fi
  96. #!/bin/bash export BUNDLE_GEMFILE=Gemfile.next export BUNDLE_CACHE_PATH=vendor/cache.next if [[ "${@}" =~ ^bundle

    ]]; then $@ else bundle exec $@ fi
  97. #!/bin/bash export BUNDLE_GEMFILE=Gemfile.next export BUNDLE_CACHE_PATH=vendor/cache.next if [[ "${@}" =~ ^bundle

    ]]; then $@ else bundle exec $@ fi
  98. $ bundle install # install gems $ rails s #

    run rails $ rspec # run tests
  99. $ bin/next bundle install # install gems $ bin/next rails

    s # run rails $ bin/next rspec # run tests
  100. None
  101. ) Keep the Gemfile healthy * Outlaw deprecations + Dual-boot

    Rails next
  102. Make it easy to do the right thing.

  103. # Try it out $ gem install ten_years_rails_conf_2018 # Or,

    read the code $ open https://github.com/clio/ten_years_rails_conf_2018
  104. Tiny Co. / Big Inc. Problems Close the gaps Summary

  105. $ Tiny Co. % Big Inc. 1. Gems Few Many

    2. Code changes Few Many 3. Test Few failures Won’t even run 4. Deploy Easy Terrifying
  106. $ Tiny Co. % Big Inc. 1. Gems Few Many

    Few 2. Code changes Few Many 3. Test Few failures Won’t even run 4. Deploy Easy Terrifying
  107. $ Tiny Co. % Big Inc. 1. Gems Few Many

    Few 2. Code changes Few Many Few 3. Test Few failures Won’t even run 4. Deploy Easy Terrifying
  108. $ Tiny Co. % Big Inc. 1. Gems Few Many

    Few 2. Code changes Few Many Few 3. Test Few failures Won’t even run Few failures 4. Deploy Easy Terrifying
  109. $ Tiny Co. % Big Inc. 1. Gems Few Many

    Few 2. Code changes Few Many Few 3. Test Few failures Won’t even run Few failures 4. Deploy Easy Less terrifying*
  110. Blur the lines

  111. Prepare for Rails upgrade Make codebase better

  112. None
  113. Removed 33 gems Updated 27 gems Reduced to zero deprecations

    Fixed 2,950 of 3,300 failing tests
  114. Created an upgrade process where everyone can contribute

  115. Hundreds of tiny changes.

  116. Hundreds of tiny changes. Don’t wait.

  117. Thanks Jordan Raine @jnraine