Timee Product Team Blog

タイミー開発者ブログ

Rails edgeでCIを回し始めました 〜設定方法編〜

こんにちは、マッチング領域でバックエンドエンジニアをしているぽこひで ( @pokohide ) です。

冷やし中華はじめました的なタイトルですね。分かります。

今回はタイミーが本番運用しているRailsアプリケーションに対してRails edgeでCIを回すようになった話を紹介します。翌週には「〜見つけたエラー編(仮)〜」と題して、実際に弊社で見つけたエラーの例を紹介していきます。記事公開時点(2023年7月)のバージョンは下記の通りです。

$ ruby -v
ruby 3.2.2 (2023-03-30 revision e51014f9c0) +YJIT [aarch64-linux]

$ rails -v
Rails 7.0.6

弊社ではRubyRailsも積極的に最新バージョンにあげる活動をしています。今回の記事はRailsに関してですが、Rubyのアップグレードも同様に行っています。過去にはRuby3.2にし、YJITを有効化にした記事を公開しているので興味があれば、ご一読ください。

tech.timee.co.jp

Rails edgeとは?

ChatGPTに聞いて楽をしてみました。

この記事ではChatGPTと同様に rails/rails のmainブランチを指すこととします。

Rails edgeは、安定版ではなく開発版なので以下のような特徴があります。

  • 将来のリリースで利用可能になるかもしれない新機能が含まれている
  • 正式リリースされていない既知のバグ修正が含まれている

安定版ではなく、まだ評価されていない機能やバグが含まれている可能性があるため、本番環境で使用する際には注意が必要です。GitHubでは、毎週本番環境で使うRailsをedgeにアップデートしているそうです*1

なぜRails edgeでCIを回すのか

タイミーでは、RubyRailsのコミュニティの進化に継続的に追随することで、高速化や機能追加などの恩恵を受け、ユーザーに最大限の価値を提供していきたいと考えています。

Rails edgeでは、将来のリリースで利用可能になる新機能を事前に検査できるため、潜在的な影響を確認することができます。また、バグや互換性の問題を早期に発見し、解決策を見つけることも可能です。その他にも、最新の機能や修正に関して何かあれば、安定版リリース前に異議申し立てをしやすいといったメリットもあります。

これらの利点を考慮し、Rails edgeでCIを回すことにしました。

Rails edgeでCIを回す

rails以外のgemはそのまま利用したいため、既存のGemfileを読み込み、railsのみを rails/rails に上書きすることでRails edge用のGemfileを用意します。

# frozen_string_literal: true

# 既存のGemfileを読み込む
eval_gemfile File.expand_path('../Gemfile', __dir__)

# 上書きしたい依存関係を削除する
dependencies.delete_if { |d| d.name == 'rails' }

# rails/railsのメインブランチに上書きする
gem 'rails', branch: 'main', github: 'rails/rails'

使用するgemのバージョンが変更されたので専用のlockファイルを用意する必要があります。BUNDLE_GEMFILE を指定する事で指定したGemfileを使用できます。

$ BUNDLE_GEMFILE=gemfiles/rails_edge.gemfile bundle install

これによりRails edgeと他のGemとの依存関係を定義したファイルの用意が完了します。次にこれらのファイルを利用してCIでテストを実行する準備をしていきます。実際の設定ファイルを簡略化して紹介しています。なお、弊社ではCIツールとしてCircleCIを利用しています。

version: 2.1
orbs:
  ruby: circleci/ruby@2.0.1
jobs:
  test:
    parameters:
      gemfile:
        type: string
        default: Gemfile
    environment:
      BUNDLE_GEMFILE: << parameters.gemfile >>
      BUNDLE_PATH_RELATIVE_TO_CWD: true
    docker:
      - image: cimg/ruby:3.2.2
    steps:
      - checkout
      - ruby/install-deps:
          key: gems-<< parameters.gemfile >>
          gemfile: << parameters.gemfile >>
      - run:
          name: Test
          command: bundle exec rspec
workflows:
  version: 2
  workflow:
    jobs:
      - test:
          filters:
            branches:
              only:
                - master
                - /^rails_edge.*$/
          matrix:
            parameters:
              gemfile:
                - "gemfiles/rails_edge.gemfile"

今回はmasterブランチと rails_edge.* ブランチでのみ実行するようにしました。 Orb - circleci/ruby で定義されるコマンドである install-deps ではgemfileパラメータを渡すと指定したGemfileを利用してbundle installを実行してくれるため、その機構を利用しています。

また、BUNDLE_PATH_RELATIVE_TO_CWD を利用することで、Gemfileではなくカレントディレクトリに対して相対的にパスを指定するのでGemの競合を回避でき、キャッシュを有効活用できます。

Rails edgeで落ちるテストに印をつける

後出しですが今回の目的は「Rails edgeでCIを回す」ことであり、全てのテストを修正することではありませんでした。そこで、今回は落ちたテストをpendingすることにしました。

skipではなくpendingを採用したのは、mainブランチの追従によりテストが成功する可能性があるためです。skipはテストの成功・失敗に限らずテストを実行しませんが、pendingはテストの失敗時のみ保留にするため修正に気づくことができます。

便宜上、これから出るであろうRails7.1で動かないこととそれまでに直すぞという意味合いも込めて以下のようなメッセージを表示するようにしています。ここは適当に変えていただくのが良いかと思います。

# frozen_string_literal: true

module PendingIfRailsEdge
  def pending_if_rails_edge
    'PENDING: Rails 7.1で動くように直す' if Rails::VERSION::STRING.start_with?('7.1')
  end
end

RSpec.configure do |config|
  config.extend PendingIfRailsEdge
end

以下のように使えます。こうすることでテストコードをgrepした時に落ちているコードをすぐに見つけることができるの便利でもあります。

context 'foo', pending: pending_if_rails_edge do
  ...
end

it 'bar', pending: pending_if_rails_edge do
  ...
end

これでRails edgeでCIを回すことができるようになりました。

Rails edge用のlockファイルを追従させる

masterブランチと rails_edge.* ブランチでRails edgeでCIを回す運用を開始してから、featureブランチでGemの追加や削除を行ったが rails_edge.gemfile の更新を忘れてmasterブランチにマージしたため、masterブランチでRails edgeでのCIがfailするケースが発生しました。

本番環境に影響がないとはいえmasterブランチのCIがfailしているのはモヤモヤすることや、せっかく導入した仕組みが形骸化してしまうため、masterブランチマージ前に気づける仕組みを導入しました。

具体的には BUNDLE_GEMFILE=gemfiles/rails_edge.gemfile bundle install を実行して差分が発生すれば失敗するステップを追加し、featureブランチでも実行するようにしました。

jobs: 
  check_outdated_gemfile:
    parameters:
      gemfile:
        type: string
    docker:
      - image: cimg/ruby:3.2.2
    steps:
      - checkout
      - ruby/install-deps:
          key: gems-<< parameters.gemfile >>
          gemfile: << parameters.gemfile >>
      - run:
          name: re-bundle install
          command: bundle install
      - run:
          name: check file changes
          command: |
            if [ `git diff --name-only << parameters.gemfile >>.lock` ]; then
              echo 'Please run `BUNDLE_GEMFILE=<< parameters.gemfile >> bundle install`'
              exit 1
            else
              exit 0
            fi

最後に

今回はRails edgeでCIを回し始めた背景や導入する方法について紹介しました。

今回の取り組みを通して、将来のRailsアップグレードにおいて遭遇するであろうバグに早期に気づき、迅速な対応ができるようになりました。まだGitHubのように *1Rails edgeを毎週取り込める状態ではないですが、引き続き頑張っていこうと思います。

次回は、Rails edgeでCI回し始めたことで見つけた問題を紹介していきます。記事公開時には公式Twitter ( @TimeeDev ) でアナウンスしていくのでフォローしていただけると嬉しいです。

また、タイミーでは一緒にサービスを成長させていく方を募集しています。もし少しでも興味を持っていただけたら、カジュアル面談受け付けておりますので是非お話ししましょう!

devenable.timee.co.jp