2025年9月4日,星期四

Rails 8.1 Beta 1:作业连续性、结构化事件、本地 CI

由 dhh 发布

自上次主要版本发布以来,Rails 8.1 在过去十个月中集成了超过 500 位贡献者、2500 次提交的成果,我们很高兴能赶在 Rails World 的第一天发布第一个 Beta 版本。以下是其中的一些亮点:

Active Job 连续性

现在,长时间运行的作业可以分解为离散的步骤,允许执行在重启后从最后一个完成的步骤继续,而不是从头开始。这在使用 Kamal 进行部署时特别有用,因为 Kamal 默认只允许运行作业的容器在三十秒内关闭。

示例

class ProcessImportJob < ApplicationJob
  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 连续性由 37signals 的 Donal McBreen 主导。

结构化事件报告

Rails 的默认日志记录器非常适合人类阅读,但不太适合后期处理。新的 Event Reporter 为在 Rails 应用程序中生成结构化事件提供了一个统一的接口。

Rails.event.notify("user.signup", user_id: 123, email: "user@example.com")

它支持为事件添加标签

Rails.event.tagged("graphql") do
  # Event includes tags: { graphql: true }
  Rails.event.notify("user.signup", user_id: 123, email: "user@example.com")
end

以及上下文

# All events will contain context: {request_id: "abc123", shop_id: 456}
Rails.event.set_context(request_id: "abc123", shop_id: 456)

事件会发送给订阅者。应用程序会注册订阅者来控制事件的序列化和发出方式。订阅者必须实现一个 #emit 方法,该方法接收事件哈希。

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

结构化事件报告由 Shopify 的 Adrianna Chang 主导。

本地 CI

开发者的机器现在速度非常快,拥有大量的核心,这使得它们非常适合本地运行,即使是相对庞大的测试套件。以前,HEY 的测试套件(超过 30,000 个断言)在云端运行,包括协调、镜像构建和并行运行,需要超过 10 分钟。现在,在 Framework Desktop AMD Linux 机器上本地运行只需要 1 分 23 秒,在 M4 Max 上需要 2 分 22 秒。

这使得对于许多中小型应用程序来说,摆脱云端 CI 设置不仅可行,而且是可取的。因此,Rails 添加了一个默认的 CI 声明 DSL,它定义在 config/ci.rb 中,并由 bin/ci 运行。它看起来是这样的:

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

与 gh 的可选集成确保了 PR 必须通过 CI 运行才能合并。

本地 CI 工作由 37signals 的 Jeremy Daer 主导。

Markdown 渲染

Markdown 已成为 AI 的通用语言,Rails 通过简化响应 Markdown 请求和直接渲染它们来拥抱这一趋势。

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 现在可以轻松地从加密的 Rails 凭据存储中获取其部署所需的秘密。这使其成为外部秘密存储的一个低配置替代方案,只需要主密钥即可工作。

# .kamal/secrets
KAMAL_REGISTRY_PASSWORD=$(rails credentials:fetch kamal.registry_password)

此工作由 Shopify 的 Matthew Nguyen 和 Jean Boussier 完成。

已弃用的关联

Active Record 关联现在可以标记为已弃用。

class Author < ApplicationRecord
  has_many :posts, deprecated: true
end

这样,将报告对 posts 关联的使用。这包括显式的 API 调用,例如:

author.posts
author.posts = ...

以及其他,还有间接使用,例如:

author.preload(:posts)

通过嵌套属性的使用,等等。

支持三种报告模式(:warn:raise:notify),并且可以启用或禁用回溯,尽管您始终可以获得报告使用位置的信息。默认模式是 :warn 模式,并且禁用回溯。

此功能由 Xavier Noria 在为 Gusto 咨询时向上游贡献。

以及其他所有内容

自上次发布以来,Rails 8.1 共有 2500 次提交,其中包含了大量的修复、次要功能和改进。请查阅 CHANGELOG 文件以获取详细信息。