Upgrading Rails application from 7.0 to 7.1

On October 5th, Rails 7.1 has been released. In this article, I will show you how I upgraded one of our projects, OneTribe (https://onetribe.team/), to the new major release within one day of my holidays.

Prerequisites

OneTribe runs Ruby 3.2.2 and Rails 7.0.7, we host code on a GitHub and deploy with GitHub Actions and Kamal. I showed how to do this in my previous article: https://jetrockets.com/blog/how-to-use-basecamp-s-kamal-with-aws-and-github.

OneTribe master git branch
OneTribe master git branch

Dependencies Update

I started with upgrading rails in application Gemfile.


# Gemfile
gem 'pg_party'

# ...

gem 'rails', '~> 7.1.0'
gem 'rails-i18n'

After this you can try to run bundle update rails and depending from your other dependencies it will either success or failure. It my case it failed, because pg_party does not support Rails 7.1 yet. The good news is that Ryan (author and maintainer of PgParty) with help of me and one more developer managed to add support for AR 7.1 within one evening, but didn’t release the new version of the gem yet, so I am gonna to take the code from the main branch.


# Gemfile
gem 'pg_party', github: 'rkrage/pg_party'

After solving issue with PgParty bundle succeeded. I was almost sure that application is ready for the next phase of update, but remembered that Rails 7.1 introduced composite primary keys for ActiveRecord support out of the box (you can find a more details list of new features and improvements in the official Ruby on Rails blog post).

In OneTribe we’ve used the gem called composite_primary_keys (https://github.com/composite-primary-keys/composite_primary_keys). It was rather easy, I replaced self.primary_keys = <array> call with self.primary_key = <array> and removed composite_primary_keys from application bundle.


class TimeTracking::Entry < TimeTrackingRecord
	# with composite_primary_keys
	# self.primary_keys = :id, :date

	# with ActiveRecord 7.1
  self.primary_key = [:id, :date]

  range_partition_by :date

	# ...
end

After this, I updated Sidekiq to 6.5.11, which added support to Rails 7.1, and tried to start the application. However, I got an error.

ActionText Rails 7.1 error
ActionText Rails 7.1 error

ActionText in Rails 7.1 introduced a new HTML 5 sanitizer, which is now default and falls back to HTML 4. As a result of this change, ActionText::ContentHelper.allowed_tags and .allowed_attributes are applied at runtime and return nil during application load.

In our case, I don’t need additional tags to be added to ActionText allowed tags configuration, and I removed the initializer.

I ran the application test suit, and all specs passed successfully, meaning I can start rails app:update.

Application Configuration Update

Rails has a special task rails app:update that can help you to update application configuration in an interactive mode. I use VS Code for development and wanted to use its merge tool, so I specified THOR_MERGE constant before running the command THOR_MERGE="code --wait" ./bin/rails app:update and used merge tool to track changes over files.

One notable change in 7.1 release is that config.cache_classes option has been replaced with config.enable_reloading that has inverted meaning. Both options will still work for backward compatibility, but I suggest to replace config.cache_classes = false with config.enable_reloading = true in environments configuration files.

Another new configuration option of Rails 7.1 is config.action_controller.raise_on_missing_callback_actions . I always try to reduce the number of callbacks used in controllers, since they can be extremely hard to maintain. However there may be situations when controller callbacks fit well (e.g. authorization check). Conditional callbacks are even a bigger hell. Before Rails 7.1 if you defined a condition with only or except option for an action that does not exist, Rails would say you nothing. Now, you can set config.action_controller.raise_on_missing_callback_actions=true for test and development environments and Rails will raise an exception. You can read more info in railties changelog: https://github.com/rails/rails/blob/7-1-stable/railties/CHANGELOG.md.

In my Kamal guide I showed how to create initializer, which will deal with a load balancer that terminates SSL (e.g. AWS ALB). Rails 7.1 config.assume_ssl=true option. This means that in environments that work behind load balancer (usually production, staging, etc) you have to enable it and delete initializer that you used in Rails 7.0.


# config/environments/production.rb

# Assume all access to the app is happening through a SSL-terminating reverse proxy.
# Can be used together with config.force_ssl for Strict-Transport-Security and secure cookies.
config.assume_ssl = true

# Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
config.force_ssl = false

After you merge all your configuration files and the interactive tool is finished, some tweaks can still be done.

First of all, you may notice that you will have a new file in your code base called new_framework_defaults_7_1.rb. It contains all Rails 7.1 default params commented, so you can enable them one by one. Another option is to change config.load_defaults in config/application.rb file to have 7.1 value and delete new_framework_defaults_7_1.rb , this will enable all options at once.

The last, but not the least is config.autoload_lib option. Before 7.1 you probably had something similar to the code below in your application that uses Zeitwerk.


# config/application.rb

module OneTribe
  class Application < Rails::Application
    config.eager_load_paths << config.root.join("lib")

    Rails.autoloaders.main.ignore(
      config.root.join("lib").join("assets"),
      config.root.join("lib").join("tasks"),
      config.root.join("lib").join("middleware"),
    )

    # ...
  end
end

The code above added lib folder to both eager load and autoload paths and excluded from autoload paths from lib that didn’t contain Ruby code or should not be reloaded or eager-loaded.

With Rails 7.1 and config.autoload_lib everything becomes easier. You just tell Rails to autoload everything in lib and provide a list of folders that should not be reloaded and eager-loaded.


# config/application.rb

module OneTribe
  class Application < Rails::Application

    # Please, add to the `ignore` list any other `lib` subdirectories that do
    # not contain `.rb` files, or that should not be reloaded or eager loaded.
    # Common ones are `templates`, `generators`, or `middleware`, for example.
    config.autoload_lib(ignore: %w[assets tasks middleware])

		# ...
  end
end

Thats all. I reran my specs to ensure I didn’t break anything. Of course, I ran Rubocop to ensure all the changes fit the code style, created PR, and successfully upgraded OneTribe to the new Rails version.

Conclusions

Rails 7.1 gives you many new features and abilities https://rubyonrails.org/2023/10/5/Rails-7-1-0-has-been-released, but as usual, the upgrading process is smooth and straightforward.

Thanks to all the contributors, the core team, and those who tested release candidate and beta builds! longreads