Ruby 2.2.3 で動いている Rails アプリケーションを Ruby 2.6.2 にアップデートした話

Ruby 2.2.3 で動いている Rails アプリケーションの Ruby バージョンを 2.6.2 まで上げました。対応の多くがアプリケーションが依存する gem によってしまうので、全ての対応が適用できる訳ではありませんが、少しでも参考になれば幸いです。

# 背景
Ruby 2.2 は2018年3月31日をもってサポートが終了しています。

* [Support of Ruby 2.2 has ended](https://www.ruby-lang.org/en/news/2018/06/20/support-of-ruby-2-2-has-ended/)

しかし、対象となったアプリケーションはテストコードが書かれておらず、バージョンアップを行なってもアプリケーションの動作担保が難しい状態でした。

一方で、機能開発を止めることも難しかったため、機能追加を行うたびにテストコードを少しずつ増やしてきました。そして、ようやく最低限のテストコードが揃ったと判断し、今回の Ruby のバージョンアップに踏み切りました。

# TL;DR
ruby と、依存する **最低限の** gem 10個をアップデートしました。

* ruby を 2.2.3 -> 2.6.2 にアップデート
* delayed_job を 4.1.2 -> 4.1.5 にアップデート
* fog を 1.33.0 -> 1.41.0 にアップデート
* therubyracer を 0.12.2 -> 0.12.3 にアップデート
* rails を 4.2.6 -> 4.2.11.1 にアップデート
* sass-rails を 5.0.4 -> 5.0.7 にアップデート
* activerecord-session_store を 0.1.1 -> 1.1.3 にアップデート
* devise を 3.5.10 -> 4.6.2 にアップデート
* devise_invitable を 1.5.5 -> 1.7.5 にアップデート
* capistrano を 3.4.0 -> 3.11.0 にアップデート
* rubocop を 0.60.0 -> 0.62.0 にアップデート

# rails server を立ち上げる
まずはローカル環境で rails server が立ち上げることを目指します。以下、発生したエラーとその原因、対応です。

## delayed_job
### ERROR メッセージ
```
Bundler::GemRequireError: There was an error while trying to load the gem 'delayed_job_active_record'.
Gem Load Error is: undefined method `yaml_as' for ActiveRecord::Base:Class
```

### 原因
* psych v3.0.0.beta2 で `yaml_as` が削除された
  * [Remove deprecated methods by hsbt · Pull Request #312 · ruby/psych](https://github.com/ruby/psych/pull/312)
* ruby では 2.5.0-rc1 で psych のアップデートが行われた
  * [Merge psych-3.0.0.beta2 from https://github.com/ruby/psych · ruby/ruby@5a13c6b](https://github.com/ruby/ruby/commit/5a13c6bcd9c7c138fdbedb92a56efea6f9535416#diff-8770f33594539b98c12dfe78afc951a9)

### 対応
* delayed_job は 4.1.4 で `yaml_as` から `yaml_tag` に変更された
  * [Use `yaml_tag` instead of deprecated `yaml_as` by y-yagi · Pull Request #996 · collectiveidea/delayed_job](https://github.com/collectiveidea/delayed_job/pull/996)
* アプリケーションが依存している delayed_job は 4.1.2
* delayed_job を最新版 (4.1.5) にアップデートする

## fog
### ERROR メッセージ
```
LoadError: cannot load such file -- xmlrpc/client
<略>
  /Users/tetsuya/dev/app_name/vendor/bundle/ruby/2.6.0/gems/fog-1.33.0/lib/fog/xenserver/core.rb:23:in `<class:Connection>'
  /Users/tetsuya/dev/app_name/vendor/bundle/ruby/2.6.0/gems/fog-1.33.0/lib/fog/xenserver/core.rb:22:in `<module:XenServer>'
  /Users/tetsuya/dev/app_name/vendor/bundle/ruby/2.6.0/gems/fog-1.33.0/lib/fog/xenserver/core.rb:5:in `<module:Fog>'
  /Users/tetsuya/dev/app_name/vendor/bundle/ruby/2.6.0/gems/fog-1.33.0/lib/fog/xenserver/core.rb:4:in `<top (required)>'
```

### 原因
* Ruby 2.4.0-preview2 から XMLRPC が gem として提供されるようになった
  * [ruby/ChangeLog at v2_4_0_preview2 · ruby/ruby](https://github.com/ruby/ruby/blob/v2_4_0_preview2/ChangeLog#L3570-L3575)
  * [Feature #12160: Extract XMLRPC library to bundled gem - Ruby trunk - Ruby Issue Tracking System](https://bugs.ruby-lang.org/issues/12160%5Bruby-core:74239%5D)

### 対応
* fog-xenserver は 0.3.0 で Ruby 2.4 以上であれば前述の gem を使うように修正
  * [Fixnum deprecated in Ruby 2.4 by HashNotAdam · Pull Request #65 · fog/fog-xenserver](https://github.com/fog/fog-xenserver/pull/65)
* fog は 1.35.0 より fog-xenserver に依存するように修正されている
  * [Remove Xenserver (2.0.0) by plribeiro3000 · Pull Request #3497 · fog/fog](https://github.com/fog/fog/pull/3497)
* アプリケーションが依存している fog は 1.33.0
* fog を最新版 (1.41.0) にアップデートする

## therubyracer
### ERROR メッセージ
```
Bundler::GemRequireError: There was an error while trying to load the gem 'therubyracer'.
Gem Load Error is: wrong argument type Class (expected Module)
```

### 原因
* Ruby 2.4.0-rc1 で Fixnum と Bignum が Integer に統一されたため
  * [Feature #12005: Unify Fixnum and Bignum into Integer - Ruby trunk - Ruby Issue Tracking System](https://bugs.ruby-lang.org/issues/12005)
  * [Ruby 2.4.0-rc1 Released](https://www.ruby-lang.org/en/news/2016/12/12/ruby-2-4-0-rc1-released/)


### 対応
* therubyracer も 0.12.3 で対応した
  * [Support for unified Integer class in Ruby 2.4+ by koic · Pull Request #413 · cowboyd/therubyracer](https://github.com/cowboyd/therubyracer/pull/413)
* アプリケーションが依存している therubyracer は 0.12.2
* therubyracer を最新版 (0.12.3) にアップデートする

## rails 
### ERROR メッセージ
```
SystemStackError: stack level too deep
  /root/repo/vendor/bundle/ruby/2.6.0/gems/activesupport-4.2.6/lib/active_support/core_ext/numeric/conversions.rb:131:in `block (2 levels) in <class:Numeric>'
```

### 原因
* Ruby 2.4.0-rc1 で Fixnum と Bignum が Integer に統一されたため
  * [Feature #12005: Unify Fixnum and Bignum into Integer - Ruby trunk - Ruby Issue Tracking System](https://bugs.ruby-lang.org/issues/12005)
  * [Ruby 2.4.0-rc1 Released](https://www.ruby-lang.org/en/news/2016/12/12/ruby-2-4-0-rc1-released/)

### 対応
* Rails 5 向けに追加された対応を 4 向けにバックポート。Rails 4.2.8-rc1 で追加された。
  * [Support for unified Integer class in Ruby 2.4+ by tenderlove · Pull Request #25161 · rails/rails](https://github.com/rails/rails/pull/25161)
* アプリケーションが依存している rails は 4.2.6
* rails を最新版 (4.2.11.1) にアップデートする

# 簡単な動作を確認をする
ここまでで rails server が立ち上がりました :raised_hands: 動作を確認しながら WARN/ERROR メッセージに対応していきます。

## sass-rails
### WARN メッセージ
```
DEPRECATION WARNING: Sprockets method `register_engine` is deprecated.
Please register a mime type using `register_mime_type` then
use `register_compressor` or `register_transformer`.
https://github.com/rails/sprockets/blob/master/guides/extending_sprockets.md#supporting-all-versions-of-sprockets-in-processors
 (called from block (2 levels) in <class:Railtie> at /root/repo/vendor/bundle/ruby/2.6.0/gems/sass-rails-5.0.4/lib/sass/rails/railtie.rb:57)
DEPRECATION WARNING: Sprockets method `register_engine` is deprecated.
Please register a mime type using `register_mime_type` then
use `register_compressor` or `register_transformer`.
https://github.com/rails/sprockets/blob/master/guides/extending_sprockets.md#supporting-all-versions-of-sprockets-in-processors
 (called from block (2 levels) in <class:Railtie> at /root/repo/vendor/bundle/ruby/2.6.0/gems/sass-rails-5.0.4/lib/sass/rails/railtie.rb:58)
no_proxy is unsupported
```

### 原因
* sprockets 3.7.0 で `register_engine` が非推奨になったため
  * [Allow for `register_engine` without deprecation by schneems · Pull Request #345 · rails/sprockets](https://github.com/rails/sprockets/pull/345)
  * [Deprecation Branch for 3.X by schneems · Pull Request #264 · rails/sprockets](https://github.com/rails/sprockets/pull/264)
* rails のバージョンを上げたタイミングで依存する sprockets も 3.5.2 から 3.7.2 に上がった

### 対応
* sass-rails も 5.0.6 で対応
  * [[close #381] Avoid Sprockets deprecations by schneems · Pull Request #382 · rails/sass-rails](https://github.com/rails/sass-rails/pull/382)
* アプリケーションが依存している sass-rails は 5.0.4
* sass-rails を最新版 (5.0.7) にアップデートする

`bundle update` は完了するが、以下のメッセージが表示される。

> Ruby Sass is deprecated and will be unmaintained as of 26 March 2019.

sass-rails のメンテナンスが2019年3月26日をもって終了した。今後は、[sass/sassc-ruby: Use libsass with Ruby!](https://github.com/sass/sassc-ruby) を利用することが推奨されている。今回の修正範囲外とするが早めに対応したい。

See also: [Ruby Sass Has Reached End-Of-Life « Sass Blog](http://sass.logdown.com/posts/7828841)

## activerecord-session_store
### ERROR メッセージ
```
Thread.exclusive is deprecated, use Thread::Mutex
/Users/tetsuya/dev/app_name/vendor/bundle/ruby/2.6.0/gems/activerecord-session_store-0.1.1/lib/active_record/session_store/session.rb:32:in `find_by_session_id'
/Users/tetsuya/dev/app_name/vendor/bundle/ruby/2.6.0/gems/activerecord-session_store-0.1.1/lib/action_dispatch/session/active_record_store.rb:66:in `block in get_session'
```

### 原因
* Ruby 2.3.0-preview2 から `Thread.exclusive` が非推奨になったため
  * [prelude.rb: deprecate Thread.exclusive · ruby/ruby@2069c9e](https://github.com/ruby/ruby/commit/2069c9e031fc968d6d3d0fe30a9316851e4d91d8)
  * [ruby/ChangeLog at v2_3_0_preview2 · ruby/ruby](https://github.com/ruby/ruby/blob/v2_3_0_preview2/ChangeLog#L1598-L1600)

### 対応
* activerecord-session_store では 1.1.3 で修正されている
  * [Rails 5.0 Redux by mvastola · Pull Request #66 · rails/activerecord-session_store](https://github.com/rails/activerecord-session_store/pull/66)
* アプリケーションが依存している activerecord-session_store は 0.1.1
* activerecord-session_store を最新版 (1.1.3) にアップデートする

## devise
### ERROR メッセージ
```
SyntaxError - syntax error, unexpected '{', expecting keyword_end
...ter only: [:create, :destroy] { request.env["devise.skip_tim...
...                              ^
/Users/tetsuya/dev/app_name/vendor/bundle/ruby/2.6.0/gems/devise-3.5.10/app/controllers/devise/sessions_controller.rb:5: syntax error, unexpected '}', expecting keyword_end
..."devise.skip_timeout"] = true }
...                              ^:
  devise (3.5.10) app/controllers/devise/sessions_controller.rb:5:in `'

undefined method `unshift' for nil:NilClass excluded from capture: No host specified, no public_key specified, no project_id specified

NoMethodError (undefined method `unshift' for nil:NilClass):
```

### 原因
* 本来は syntax error であるが ruby のバージョンによっては ERROR となっていなかった
  * [Bug #505: 1.upto 2 {|i| p i } - Ruby trunk - Ruby Issue Tracking System](https://bugs.ruby-lang.org/issues/505)
  * [Bug #13547: [].delete 1 { 'NG' } - Ruby trunk - Ruby Issue Tracking System](https://bugs.ruby-lang.org/issues/13547)
  * [Bug #14023: SyntaxError on array argument and block - Ruby trunk - Ruby Issue Tracking System](https://bugs.ruby-lang.org/issues/14023)

### 対応
* devise は 4.4.0 で修正された
  * [Fix syntax for MRI 2.5.0-preview1. by pat · Pull Request #4668 · plataformatec/devise](https://github.com/plataformatec/devise/pull/4668)
* アプリケーションが依存している devise は 3.5.10
* devise を最新版 (4.6.2) にアップデートする
```
$ bundle update devise
```

### ERROR メッセージ
```
NoMethodError - undefined method `for' for #<Devise::ParameterSanitizer:0x00007fc999e94890>:
```
```
undefined method `user_omniauth_authorize_path' for #<#<Class:0x00007ffa5913a5c8>:0x00007ffa58bae348>
```

### 原因
* 上記対応で devise がバージョンアップされた
* devise 4.0.0.rc2 で非推奨となった method が 4.2.0 で削除されたため
  * [Remove deprecated APIs by lucasmazza · Pull Request #4083 · plataformatec/devise](https://github.com/plataformatec/devise/pull/4083)

### 対応
```diff
- devise_parameter_sanitizer.for(:sign_up)
- devise_parameter_sanitizer.for(:sign_in)
+ devise_parameter_sanitizer.permit(:sign_up)
+ devise_parameter_sanitizer.permit(:sign_in)
```

```diff
- user_omniauth_authorize_path(:facebook)
+ user_facebook_omniauth_authorize_path
```

## devise_invitable
### ERROR メッセージ
```
Traceback (most recent call last):
bin/rails: undefined method `attributes_for' for class `Devise::ParameterSanitizer' (NameError)
```

### 原因
* devise 4.0.0.rc1 で `Devise::ParameterSanitizer#attributes_for` が削除されたため
  * [Remove `ParametersSanitizer` inheritance. by lucasmazza · Pull Request #3856 · plataformatec/devise](https://github.com/plataformatec/devise/pull/3856)

### 対応
* devise_invitable 1.6.0 から devise 4 に対応した
  * [Updates for Devise master with Rails 5 by twalpole · Pull Request #619 · scambra/devise_invitable](https://github.com/scambra/devise_invitable/pull/619)
* アプリケーションが依存している devise_invitable は 1.5.5
* devise_invitable を最新版 (1.7.5) にアップデートする

# Rspec や Rubocop などを動かす
ここまででローカル環境での動作確認ができました :raised_hands: ここから Rspec や Rubocop などを動かしていきます。

## rubocop
### ERROR メッセージ
```
$ bundle exec rubocop
unknown keywords: whitelist_classes, whitelist_symbols
/Users/tetsuya/.rbenv/versions/2.6.2/lib/ruby/2.6.0/psych.rb:328:in `safe_load'
/Users/tetsuya/dev/app_name/vendor/bundle/ruby/2.6.0/gems/rubocop-0.60.0/lib/rubocop/config_loader.rb:185:in `yaml_safe_load'
/Users/tetsuya/dev/app_name/vendor/bundle/ruby/2.6.0/gems/rubocop-0.60.0/lib/rubocop/config_loader.rb:159:in `load_yaml_configuration'
/Users/tetsuya/dev/app_name/vendor/bundle/ruby/2.6.0/gems/rubocop-0.60.0/lib/rubocop/config_loader.rb:40:in `load_file'
/Users/tetsuya/dev/app_name/vendor/bundle/ruby/2.6.0/gems/rubocop-0.60.0/lib/rubocop/config_loader.rb:82:in `configuration_from_file'
/Users/tetsuya/dev/app_name/vendor/bundle/ruby/2.6.0/gems/rubocop-0.60.0/lib/rubocop/config_store.rb:44:in `for'
/Users/tetsuya/dev/app_name/vendor/bundle/ruby/2.6.0/gems/rubocop-0.60.0/lib/rubocop/cli.rb:187:in `apply_default_formatter'
/Users/tetsuya/dev/app_name/vendor/bundle/ruby/2.6.0/gems/rubocop-0.60.0/lib/rubocop/cli.rb:40:in `run'
/Users/tetsuya/dev/app_name/vendor/bundle/ruby/2.6.0/gems/rubocop-0.60.0/exe/rubocop:13:in `block in <top (required)>'
/Users/tetsuya/.rbenv/versions/2.6.2/lib/ruby/2.6.0/benchmark.rb:308:in `realtime'
/Users/tetsuya/dev/app_name/vendor/bundle/ruby/2.6.0/gems/rubocop-0.60.0/exe/rubocop:12:in `<top (required)>'
/Users/tetsuya/dev/app_name/vendor/bundle/ruby/2.6.0/bin/rubocop:23:in `load'
/Users/tetsuya/dev/app_name/vendor/bundle/ruby/2.6.0/bin/rubocop:23:in `<top (required)>'
```

### 原因
* 元を辿ると DHH の black/whitelist って呼び方はよくないんじゃないかという発言に起因する
  * [DHH on Twitter:](https://twitter.com/dhh/status/1032050325513940992)

### 対応
* psych もこの流れを受け名前を変更、 3.1.0 で対応が取り込まれる
  * [Use friendlier terminology in YAML.safe_load by JuanitoFatas · Pull Request #378 · ruby/psych](https://github.com/ruby/psych/pull/378)
* ruby では 2.6.0 で psych の version アップデートが行われる
  * [Bump version to psych 3.1.0. · ruby/ruby@623c6d9](https://github.com/ruby/ruby/commit/623c6d972a5365f469d4ceb0fc3326faf35ba7b6#diff-8770f33594539b98c12dfe78afc951a9)
* rubocop では 0.61.0 で対応されている
  * [[Fix #6412] Fix an error when using Ruby 2.6.0-preview2 by koic · Pull Request #6413 · rubocop-hq/rubocop](https://github.com/rubocop-hq/rubocop/pull/6413)
* アプリケーションが依存している rubocop は 0.60.0
* rubocop を最新版 (0.67.2) にアップデートする

## rubocop-github
### ERROR メッセージ
```
Error: The `Performance/LstripRstrip` cop has been moved to `Style/Strip`
(obsolete configuration found in vendor/bundle/ruby/2.6.0/gems/rubocop-github-0.12.0/config/default.yml, please update it)
```

### 原因
* 上記対応で rubocop がバージョンアップされた
* Rubocop 0.63.0 で Style/FlipFlop が Lint/FlipFlop に移動された
  * [Move `FlipFlop` cop from `Style` to `Lint` department by koic · Pull Request #6636 · rubocop-hq/rubocop](https://github.com/rubocop-hq/rubocop/pull/6636)

### 対応
* 既に PR は出ている
  * [Move `FlipFlop` cop from `Style` to `Lint` department by y-yagi · Pull Request #35 · github/rubocop-github](https://github.com/github/rubocop-github/pull/35)
* merge されるまで、 rubocop 0.62.0 にバージョンを固定する

# 検証環境に deploy する
CI も通ったので検証環境に deploy 試みます。

## sshkit
### ERROR メッセージ
```
/Users/tetsuya/dev/app_name/vendor/bundle/ruby/2.6.0/gems/sshkit-1.7.1/lib/sshkit/runners/parallel.rb:16:in `rescue in block (2 levels) in execute': Exception while executing as devuser@52.197.98.157: undefined method `<' for nil:NilClass (SSHKit::Runner::ExecuteError)
```

### 原因
--

### 対応
* sshkit 1.8.0 で入っていった対応により、副次的に修正がされている
  * [No colors for non tty outputs by robd · Pull Request #246 · capistrano/sshkit](https://github.com/capistrano/sshkit/pull/246)

* アプリケーションが依存している capistrano は 3.4.0
* capistrano を最新版 (3.11.0) にアップデートする

## capistrano
### WARN メッセージ
```
[Deprecation Notice] Future versions of Capistrano will not load the Git SCM
plugin by default. To silence this deprecation warning, add the following to
your Capfile after `require "capistrano/deploy"`:

    require "capistrano/scm/git"
    install_plugin Capistrano::SCM::Git
```

### 原因
* 上記対応で capistrano がバージョンアップされた
* capstrano 3.7 で `scm` が非推奨となった
  * [capistrano/UPGRADING-3.7.md at master · capistrano/capistrano](https://github.com/capistrano/capistrano/blob/b545969d083952bb5b3a06c3857696ad8a4c3a0e/UPGRADING-3.7.md)
* それに伴い、WARN メッセージが表示されるようになった
  * [Deprecate `set :scm, ...` in favor of new SCM plugin system by mattbrictson · Pull Request #1572 · capistrano/capistrano](https://github.com/capistrano/capistrano/pull/1572)

### 対応
WARN メッセージにある通り、 `Capfile` に追記した

```diff
+ require "capistrano/scm/git"
+ install_plugin Capistrano::SCM::Git
```

# 未解決
### WARN メッセージ
```
/root/repo/vendor/bundle/ruby/2.6.0/gems/activesupport-4.2.11.1/lib/active_support/core_ext/object/duplicable.rb:111: warning: BigDecimal.new is deprecated; use BigDecimal() method instead.
```

* 解決されてそう :thinking_face: 
  * [BigDecimal#duplicable? returns false if Ruby is 2.5.0 or higher by yahonda · Pull Request #31499 · rails/rails](https://github.com/rails/rails/pull/31499)

# 最後に
ruby 2.2.3 から 2.6.2 とマイナーバージョンが4つ上がっただけの様に感じましたが、2.2.3 (2015年8月18日) から 2.6.2 (2019年3月13日リリース) の間に実に3年7ヶ月の時間があり、様々なアップデートが行われていたことがわかりました。

Rails の脆弱性対応も気になっていたので、4.2 系の最新版である 4.2.11.1 にアップデートできたのは安心感があります。

Ruby や Rails を使うメリットの一つに、活発なコミュニティと成熟したエコシステムがあると思います。今後も進化し続けるこの言語/フレームワークを使い続けられる様、しっかりとアップデートに追従していきながら、残念ながら今回はバージョンアップを見送ってしまった gem 達も、アップデートしていきたいと思います。