Upgrading Rails

Overview

I regularly upgrade my Rails applications and have documented all of the changes that needed.

Upgrading Rails 6 to Rails 7

Major changes in Rails 6 to 7

Rails 7 was released in mid-December 2021: https://rubyonrails.org/2021/12/15/Rails-7-fulfilling-a-vision

There are a few significant changes in Rails 7, mainly moving Rails defaults away from the Javascript heavy world and towards using Hotwire / Turbo / Stimilus.

Upgrade Rails

For this upgrade, I followed the steps outlined in the official Rails docs for upgrading from Rails 6.1 to Rails 7. I opened my gem file and bumped the Rails version to the latest available version.

Before

// Gemfile

gem 'rails', '~> 6.1.4'

After

// Gemfile

gem 'rails', '~> 7.0.2'

I then ran bundle update rails

Fix gem dependencies

This resulted in some dependency issues such as the awesome_nested_set gem needing to be updated.

Bundler could not find compatible versions for gem "activerecord":
  In Gemfile:
    awesome_nested_set (~> 3.2, >= 3.2.1) was resolved to 3.4.0, which depends on
      activerecord (>= 4.0.0, < 7.0)

    rails (~> 7.0.2) was resolved to 7.0.2, which depends on
      activerecord (= 7.0.2)

I updated to the latest version from gem 'awesome_nested_set', '~> 3.2' to gem 'awesome_nested_set', '~> 3.5' then ran bundle update rails which passed.

Generate Rails 7 files

Now that the Rails 7 gem was installed, I ran rails app:update which resulted in several files being modified or generated. I was asked to overwrite or replace existing files. I opted to overwrite the files by entering "Y" and used git to see a diff between what Rails 7 was proposing and my existing files.

Files modified

Gemfile
Gemfile.lock
bin/rails
bin/rake
bin/setup
config/boot.rb
config/environments/development.rb
config/environments/production.rb
config/environments/test.rb
config/initializers/assets.rb
config/initializers/content_security_policy.rb
config/initializers/filter_parameter_logging.rb
config/initializers/inflections.rb
db/schema.rb

New files added

config/initializers/new_framework_defaults_7_0.rb
db/migrate/20220306082930_add_service_name_to_active_storage_blobs.active_storage.rb
db/migrate/20220306082931_create_active_storage_variant_records.active_storage.rb
db/migrate/20220306082932_remove_not_null_on_active_storage_blobs_checksum.active_storage.rb

Run migrations

Since migration files were generated, I ran rails db:migrate.

Test the application locally

I started my local server using rails server and manually tested out the different functionality.

Deploy to production

Everything looked good locally, so I took the next step of deploying to production using bundle exec cap production deploy. I once again ran into an nginx error page. I ssh'ed into the server ssh deploy@ip_address tried restarting the application running touch myapp/current/tmp/restart.txt., tried restarting nginx sudo service nginx restart but still saw the same nginx error page.

I then checked the nginx logs using sudo tail -f /var/log/nginx/error.log and found

App 17259 output: Error: The application encountered the following error: You have already activated io-wait 0.2.0, but your Gemfile requires io-wait 0.2.1. Since io-wait is a default gem, you can either remove your dependency on it or try updating to a newer version of bundler that supports io-wait as a default gem. (Gem::LoadError

I checked the associated Phusion Passenger log output and also saw the same error

   "error" : {
      "aux_details" : "You have already activated io-wait 0.2.0, but your Gemfile requires io-wait 0.2.1. $
      "category" : "INTERNAL_ERROR",
      "id" : "d89e91b2",
      "problem_description_html" : "<p>The Phusion Passenger application server tried to start the web app$
      "solution_description_html" : "<p class=\"sole-solution\">Unfortunately, Passenger does not know how$
      "summary" : "The application encountered the following error: You have already activated io-wait 0.2$
   },

The error message was straightforward, I needed to update a gem file, so, on the server I ran gem install io-wait -v0.2.1. I then restarted the application and nginx and every worked.

Conclusion

Upgrading from Rails 6.1 to 7 was pretty straightforward. At first I thought it'd be difficult given all Rails 7 talk about changing to Hotwire and changes to the asset pipelines for CSS and JS, but, I realized those were mostly changes to Rails defaults for new application rather than applying to existing applications.

Upgrading Rails 5 to Rails 6

Major changes in Rails 5 to 6

When starting, I anticipated that there were going to be a lot of major changes going from Rails 5 to 6 including:
- Webpack replacing the Rails asset pipeline.
- ActionText (formerly Trix) being included as a Rich Text Editor.
- ActiveStorage being introduced for handling uploads in Rails 5.2 (replacing CarrierWave).

The first attempt

On my first attempt I tried to do everything at once hoping it would all magically work. I tried to upgrade both major versions of Rails and Ruby at the same time by swapping out ruby '2.6.3' for ruby '3.0.1' and gem 'rails', '~> 5.1.7' for gem 'rails', '~> 6.1.4'. Needless to say, both Rails and Ruby were major version upgrades and a lot of things ended up breaking. I couldn't tell if a problem was the result of the Rails upgrade or Ruby upgrade -- lesson learned to make changes one at a time in isolation.

Upgrading Rails

My second time around, I did things one at a time:

Upgrade Rails in your Gemfile

I navigated to my Gemfile and upgraded Rails to the latest stable version:

Before

// Gemfile

gem 'rails', '~> 5.1.7'

After

// Gemfile

gem 'rails', '~> 6.1.4'

I ran bundle update rails

Fix Gemfile dependencies

This failed due to some dependencies which weren't supported by Rails 6.

Bundler could not find compatible versions for gem "rails":
  In Gemfile:
    rails (~> 6.1.4)

    trix (~> 0.11.1) was resolved to 0.11.1, which depends on
      rails (> 4.1, < 5.2)

In this case, the trix gem (a rich text editor) was only supported by Rails > 4.1, < 5.2 and not supported by Rails 6. When I encounteed this error, I could either (1) upgrade the gem version to a version that was compatible with Rails 6, or (2) remove the gem.

For trix, this gem was likely going to be replaced by ActionText so I temporarily removed the gem.

I ran bundle update rails again and dealt with the next failures.

Bundler could not find compatible versions for gem "railties":
  In Gemfile:
    rails (~> 6.1.4) was resolved to 6.1.4, which depends on
      railties (= 6.1.4)

    sass-rails (~> 5.0) was resolved to 5.0.7, which depends on
      railties (>= 4.0.0, < 6)

For sass-rails, it looked like there was a newer version available so I upgraded it to the latest version.

Before

// Gemfile

gem 'sass-rails', '~> 5.0'

After

// Gemfile

gem 'sass-rails', '~> 6.0'

I kept repeating the above steps of running bundle update rails and of upgrading or removing gems until the rails gem successfully updated. When the rails gem updated, I noticed that most of the rails dependencies in my Gemfile.lock were also updated including actioncable, actionmailbox, actionmailer, etc. I paused here, commited my code, and pushed it up to GitLab.

Generate Rails 6 files

Now that the Rails 6 gem was installed, I ran rails app:update to generate the necessary files and configurations. When generating files, I was asked if I wanted to overwrite existing files. I pressed "Y" and opted to use git diff in my IDE (VSCode) to understand the differences between my existing files and the changes Rails 6 was proposing.

Modified files:

modified: bin/rails
modified: bin/rake
modified: bin/setup
modified: bin/spring
modified: bin/yarn
modified: config.ru
modified: config/application.rb
modified: config/boot.rb
modified: config/cable.yml
modified: config/environment.rb
modified: config/environments/development.rb
modified: config/environments/production.rb
modified: config/environments/test.rb
modified: config/initializers/backtrace_silencers.rb
modified: config/initializers/filter_parameter_logging.rb
modified: config/locales/en.yml
modified: config/puma.rb
modified: config/routes.rb
modified: config/spring.rb

New files added:

config/initializers/content_security_policy.rb
config/initializers/new_framework_defaults_6_1.rb
config/initializers/permissions_policy.rb
config/storage.yml
db/migrate/20210906071742_add_service_name_to_active_storage_blobs.active_storage.rb
db/migrate/20210906071743_create_active_storage_variant_records.active_storage.rb

Running migrations

It looks like some migration files were generated so I ran rails db:migrate.

When I ran the migrations I encountered an error:

== 20210906071742 AddServiceNameToActiveStorageBlobs: migrating ===============
-- column_exists?(:active_storage_blobs, :service_name)
rails aborted!
StandardError: An error has occurred, this and all later migrations canceled:

PG::UndefinedTable: ERROR:  relation "active_storage_blobs" does not exist
LINE 8:  WHERE a.attrelid = '"active_storage_blobs"'::regclass
...

It looks like I didn't have the table active_storage_blobs. After a bit of Googling it turns out that I should have ran rails active_storage:install. After running it, it created:

20210906074230_create_active_storage_tables.active_storage.rb

I inspected the contents of the new migraiton file (20210906074230_create_active_storage_tables.active_storage.rb) and it looked like it included everything from the previous two migration files (db/migrate/20210906071742_add_service_name_to_active_storage_blobs.active_storage.rb and db/migrate/20210906071743_create_active_storage_variant_records.active_storage.rb). The previous two migration files were redundant so I removed them and ran the migration rails db:migrate again.

Resolving diffs of modified files

Once migrations were complete, the next step was to resolve the differences of any modified files. There was no one size fits all approach to this as each Rails application has it's own custom configurations. The approach I took was to look at my previous files and copy over any custom configurations into the newly modified file. Using this approach, the Rails 6 default files acted as the base of the file while I layered on top my existing application's custom configurations.

After this, I ran rails s to start my local development server at localhost:3000

Testing the Application Locally

Once my Rails application was running, I visited localhost:3000 and began manually testing all the different user flows including signup / search / create-read-update-delete records / payment / etc.

Fixing Trix

One bug I found was that my form fields no longer supported rich text due to the removal of the trix gem.

There were a couple of ways to fix this. (1) Re-add Trix as a JS package in package.json instead of a Ruby gem in Gemifle (2) Upgrade to Action Text by following the Ruby on Rails guide.

I originally attempted to move forward with option (2) and Upgrade to Action Text, however, after running bin/rails action_text:install the Action Text generator introduced a new polymorphic table to store all rich text. I was quite happy with how my existing tables were modelled to already handled rich text, so, I decided to undo these changes and explore the other option.

For option (1) Re-add Trix as a JS package instead of a Ruby gem, I first ran yarn add trix which added trix into package.json. Afterwards, I imported trix the JS and CSS.

// myapp/app/assets/javascripts/application.js

//= require trix
// myapp/app/assets/stylesheets/application.scss

//= require trix

I navigated to a page with a trix form and saw that the trix editor was now rendering properly. What's happening behind the scenes to make this happen is that the trix javascript is finding the trix-editor HTML element and appending the trix editor to it. When content is modified in the trix editor, trix javascript writes the contents into an html hidden_field which is used for a form submission.

      .form-group
        = f.hidden_field :about, id: :about, class: "form-control"
        %trix-editor{:input => "about"}

I paused here, commited my code, and pushed it up to GitLab.

Deployment

My application now ran locally without any issues and I decided to deploy the application to a live environment. My application was configured to use Capistrano for deployments and I simply ran bundle exec cap production deploy to start the deployment. This deployment took a bit longer than normal due to the Rails 6 upgrade and installation of new dependencies.

Once the deployment was complete, I visited the website and found:

We're sorry, but something went wrong.
The issue has been logged for investigation. Please try again later.

Web application could not be started by the Phusion Passenger application server.
Please read the Passenger log file (search for the Error ID) to find the details of the error.

The first thing I tried was re-deploying. I ran bundle exec cap production deploy but still received the same error.

Next, I tried restarting the application by SSHing into the server ssh deploy@ip_address and running touch myapp/current/tmp/restart.txt.

Still no luck, next I tried restarting nginx by first checking that it was infact running sudo service nginx status and then restarting it sudo service nginx restart.

Still no luck, next I decided to dive into the nginx logs by running sudo tail -f /var/log/nginx/error.log. I quickly found this error:

Could not spawn process for application /home/deploy/myapp/current: The application encountered the following error: undefined method `add_template_helper' for Mailer:Class (NoMethodError)

A quick Google search brought me to a StackOverflow article that mentioned add_template_helper method was deprecated in Rails 6.1 and was simply replaced with helper method. I replaced the method, commited and pushed my code, redeployed and viola, it worked!

Conclusion

Upgrading from Rails 5.1.7 application to Rails 6.1.4 wasn't as painful as I thought it would be. Before starting, I thought that I'd have to re-implement my entire asset pipeline to use Webpack and that I'd have to convert to using ActionText and ActiveStorage. None of these things ended up happening and my time was instead spent on resolving minor gem and package compatability issues.