Rails 8.1 represents the work of over 500 contributors across 2500 commits in the last ten months since our last major release, and we’re thrilled to time the first beta release with the first day of Rails World. Here are a few of the highlights:
Long-running jobs can now be broken into discrete steps that allow execution to continue from the last completed step rather than the beginning after a restart. This is especially helpful when doing deploys with Kamal, which will only give job-running containers thirty seconds to shut down by default.
Example:
class ProcessImportJob
include ActiveJob::Continuable
def perform(import_id)
@import = Import.find(import_id)
# block format
step :initialize do
@import.initialize
end
# step with cursor, the cursor is saved when the job is interrupted
step :process do |step|
@import.records.find_each(start: step.cursor) do |record|
record.process
step.advance! from: record.id
end
end
# method format
step :finalize
private
def finalize
@import.finalize
end
end
end
Active Job Continuations was lead by Donal McBreen from 37signals.
The default logger in Rails is great for human consumption, but less ideal for post-processing. The new Event Reporter provides a unified interface for producing structured events in Rails applications:
Rails.event.notify("user.signup", user_id: 123, email: "user@example.com")
It supports adding tags to events:
Rails.event.tagged("graphql") do
# Event includes tags: { graphql: true }
Rails.event.notify("user.signup", user_id: 123, email: "user@example.com")
end
As well as context:
# All events will contain context: {request_id: "abc123", shop_id: 456}
Rails.event.set_context(request_id: "abc123", shop_id: 456)
Events are emitted to subscribers. Applications register subscribers to
control how events are serialized and emitted. Subscribers must implement
an #emit
method, which receives the event hash:
class LogSubscriber
def emit(event)
payload = event[:payload].map { |key, value| "#{key}=#{value}" }.join(" ")
source_location = event[:source_location]
log = "[#{event[:name]}] #{payload} at #{source_location[:filepath]}:#{source_location[:lineno]}"
Rails.logger.info(log)
end
end
Structured Event Reporting was lead by Adrianna Chang from Shopify.
Developer machines have gotten incredibly quick with loads of cores, which make them great local runners of even relatively large test suites. The HEY test suite of over 30,000 assertions used to take over 10 minutes to run in the cloud when counting coordination, image building, and parallelized running. Now it runs locally on a Framework Desktop AMD Linux machine in just 1m 23s and an M4 Max in 2m 22s.
This makes getting rid of a cloud-setup for all of CI not just feasible but desireable for many small-to-mid-sized applications, and Rails has therefore added a default CI declaration DSL, which is defined in config/ci.rb
and run by bin/ci
. It looks like this:
CI.run do
step "Setup", "bin/setup --skip-server"
step "Style: Ruby", "bin/rubocop"
step "Security: Gem audit", "bin/bundler-audit"
step "Security: Importmap vulnerability audit", "bin/importmap audit"
step "Security: Brakeman code analysis", "bin/brakeman --quiet --no-pager --exit-on-warn --exit-on-error"
step "Tests: Rails", "bin/rails test"
step "Tests: Seeds", "env RAILS_ENV=test bin/rails db:seed:replant"
# Requires the `gh` CLI and `gh extension install basecamp/gh-signoff`.
if success?
step "Signoff: All systems go. Ready for merge and deploy.", "gh signoff"
else
failure "Signoff: CI failed. Do not merge or deploy.", "Fix the issues and try again."
end
end
The optional integration with gh ensures that PRs must be signed off by a passing CI run in order to be eligible to be merged.
The local CI work was lead by Jeremy Daer from 37signals.
Markdown has become the lingua franca of AI, and Rails has embraced this adoption by making it easier to respond to markdown requests and render them directly:
class Page
def to_markdown
body
end
end
class PagesController < ActionController::Base
def show
@page = Page.find(params[:id])
respond_to do |format|
format.html
format.md { render markdown: @page }
end
end
end
Kamal can now easily grab its secrets from the encrypted Rails credentials store for deploys. This makes it a low-fi alternative to external secret stores that only needs the master key available to work:
# .kamal/secrets
KAMAL_REGISTRY_PASSWORD=$(rails credentials:fetch kamal.registry_password)
Work by Matthew Nguyen from Shopify and Jean Boussier.
Active Record associations can now be marked as being deprecated:
class Author < ApplicationRecord
has_many :posts, deprecated: true
end
With that, usage of the posts
association will be reported. This includes explicit API calls like
author.posts
author.posts = ...
and others, as well as indirect usage like
author.preload(:posts)
usage via nested attributes, and more.
Three reporting modes are supported (:warn
, :raise
, and :notify
), and
backtraces can be enabled or disabled, though you always get the location of the
reported usage regardless. Defaults are :warn
mode and disabled backtraces.
Upstreamed by Xavier Noria while consulting for Gusto.
With 2500 commits since the last release, there’s a ton of fixes, minor features, and improvements included in Rails 8.1 as well. Please have a look at the CHANGELOG files for details.