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