Gemfileのベストプラクティス

長年の運用で「Gemfileはこう運用すると上手くいった」という知見が蓄積されてきたので、ここに書き出してみておく。

Bundler/OrderedGemsを有効化する

RuboCopの Bundler/OrderedGems Copを有効化する。

悪い例:

gem 'puma'
gem 'jbuilder'

良い例:

gem 'jbuilder'
gem 'puma'

セクションを分けない

基本的に、独自の判断で空行を入れてセクションを分けたりしない。

ここで言うセクションとは「空行で区切られた1つのまとまり」のことである。Bundler/OrderedGems は、このまとまりの中で辞書順であることを要求する。

悪い例:

gem 'aws-sdk-rails'
gem 'aws-sdk-s3'

gem 'graphql'
gem 'graphql-batch'

良い例:

gem 'aws-sdk-rails'
gem 'aws-sdk-s3'
gem 'graphql'
gem 'graphql-batch'

但し、1つの例外を認める。Bundler.require を活用しているプロジェクトで、どうしても先に読み込ませたいgemがある場合である。代表的な例だと、if defined?(Rails) のためにrails gemを先に読み込ませておきたいといった例が挙げられる。

悪い例:

gem 'jbuilder'
gem 'puma'
gem 'rails'

良い例:

gem 'rails'

gem 'jbuilder'
gem 'puma'

無駄にgemを読み込まない

Bundler.require が使われている場合、適切に require: false を付ける。

Bundler.require が使われる代表例として、主にRailsアプリが挙げられる。 このような環境では、require: false を明示的に付けない限り、Gemfileに定義したgemは自動的に読み込まれることになる。 静的解析器などの同じプロセスで利用されないgemや、CIなどの特定の状況でしか利用されないgemについては、適切に require: false を付ける。

悪い例:

group :development do
  gem 'brakeman'
  gem 'bullet'
  gem 'rubocop'
  gem 'rubocop-rails'
end

group :test do
  gem 'rspec'
  gem 'rspec_junit_formatter'
end

良い例:

group :development do
  gem 'brakeman', require: false
  gem 'bullet'
  gem 'rubocop', require: false
  gem 'rubocop-rails', require: false
end

group :test do
  gem 'rspec'
  gem 'rspec_junit_formatter', require: false
end

gem groupを分ける

独立して使われるgemは別のgem groupに分ける。

例えばRailsアプリの開発において、rubocopやbrakemanなどの静的解析ツールは、Railsの実行時に使うgem群と同時に読み込む必要がない。 こういった場合、gem groupを適切に分けておくことで、結果的にCIでインストールするべきgemの数を減らたり、require: false オプションの記述を省けたりといった恩恵がある。

悪い例:

group :development do
  gem 'brakeman', require: false
  gem 'bullet'
  gem 'rubocop', require: false
  gem 'rubocop-rails', require: false
end

良い例:

group :brakeman do
  gem 'brakeman'
end

group :development do
  gem 'bullet'
end

group :rubocop do
  gem 'rubocop'
  gem 'rubocop-rails'
end

コメントにWhatを書かない

そのGemのdescriptionを見れば分かるような情報をコメントに書かない。

コメントはあればあるほど情報量が増えて嬉しいという訳ではない。重要性の低い情報が増えれば増えるほど、重要な情報がより見られなくなってしまう。また、コメントは無料ではなく、意外とそれを保守するためにコストが支払われる。

どのコメントが「必要」でどのコメントが「不要」かを決めるのは容易ではない。指針として、5W1Hで言うところのWhyを表す情報は残すに足る価値があり、Whatには無い、という指針が良いと考えている。

悪い例:

gem 'jbuilder' # JSON生成用のDSLを提供する。
gem 'listen' # i18n-jsの自動更新機能に必要。
gem 'vcr', '5.0.0' # 次のバージョンからライセンスが変更されてしまう。

良い例:

gem 'jbuilder'
gem 'listen' # i18n-jsの自動更新機能に必要。
gem 'vcr', '5.0.0' # 次のバージョンからライセンスが変更されてしまう。

無駄にバージョンを指定しない

バージョン指定は「この範囲までのアップグレードを機械的に提案してほしい」という意図を持って指定する。

より大きな人件費を掛けて人力で変更を提案してもらいたい場合はバージョンを指定する。「その時点で採用したバージョンにとりあえず ~> を付けて指定しておこう」といった類の行為は避ける。なお、前述の話と合わせると、バージョンを指定する場合にはその理由がコメントに書かれることになるはず。

悪い例:

gem 'aws-sdk-rails', '~> 5.0.0'

良い例:

gem 'aws-sdk-rails'

ところで、主にGemfileの記述や保守のコストの観点から、基本的にgemのダウングレードが提案される可能性を考慮しない形でバージョンを指定していくほうが効率的なことが多い。「◯◯以上」というバージョン指定が単独で発生するのは稀なことになる。

dependabotにlockfile-onlyを指定する

dependabotに versioning-strategy: lockfile-only を設定する。

dependabotは、デフォルトではGemfileでのバージョン指定を考慮してくれない。この状態では、どのようにバージョンを指定していてもその指定ごと変更を提案されてしまい、先述したようなバージョン指定の運用ができない。

またこのデフォルト設定で運用していると、dependabotの設定ファイルに特定のGemのアップグレードを提案しない設定やその理由が書かれることになるため、情報の分散を招き保守性を下げるという点でも好ましくない。

悪い例:

updates:
  - package-ecosystem: bundler
    directory: ...
    schedule: ...

良い例:

updates:
  - package-ecosystem: bundler
    directory: ...
    schedule: ...
    versioning-strategy: lockfile-only

dependabotの付けるラベルを変更する

デフォルトの dependencies + ruby のラベルから、dependabot-bundler 等に変更しておく。

デフォルトで用意される ruby ラベルは特に誤解を招きやすく、まともな運用に適さない。ruby ラベルの説明文には「Pull requests that update Ruby code」と記載されるが、こういうものがあると「Rubyのコードを変更するから、自分のつくったPull Requestに ruby ラベルを付けておこう」と考えてこのラベルを付けたり付けなかったりする者が後を絶たない。

dependencies ラベルは author:dependabot で代替できるので、このラベルの存在によって付加される情報量は少ない。

悪い例:

updates:
  - package-ecosystem: bundler
    directory: ...
    schedule: ...

良い例:

updates:
  - package-ecosystem: bundler
    directory: ...
    schedule: ...
    labels:
      - dependabot-bundler