はてなのアプリケーションエンジニアのid:shiba_yu36です。社内技術勉強会で「新機能作成時に開発ブランチに細かくmergeしていく戦略」という発表をしたので、資料を公開します。
以下、簡単に文字でまとめておきます。
戦略
- ユーザーに新機能が見えないようにする工夫をし、新機能のbranchもどんどん開発ブランチにmergeしていく
- mergeされたものは随時本番にリリースされるが、ユーザーに見えない工夫をしているので問題なし
- PRは可能な限り細かくする
- 機能が完成したら最後にユーザーに新機能を見えるようなPRを作り、mergeしてリリースする
なぜこの戦略を使うのか
いろんな失敗を経験して、この戦略を最近使っている。- 失敗パターンその1: 巨大PRパターン
- 失敗パターンその2: 新機能リリースブランチパターン
失敗パターンその1: 巨大PRパターン
新機能を1ブランチで作って一気にPRするパターン。以下のような困り事がある。
- 超コンフリクト問題
- レビューする観点が多すぎて見落としてしまう
- バグの混入を防ぎにくくなる
- 指摘が多くなりがちで、レビューされる側が疲弊する
- レビュー依頼 -> レビュー完了 -> レビュー依頼 -> ...のサイクルが多くなり、レビューする側もレビューされる側も疲弊する
- mergeしたところ他にmergeした箇所と競合して動かなくなったということが起こる
- etc...
大体500行の変更を超えてきたあたりで辛くなる印象。
失敗パターンその2: 新機能リリースブランチパターン
新機能のリリース用ブランチを一つ用意して、そこにPRを送るパターン。PRは細かくレビューされ、リリース用ブランチへmergeするため、巨大PRパターンよりましに見える。
しかし、以下の困り事が起こる。
- 最後のmergeしたところ動かなくなる問題が解消しない
- 最後にリリースPRを全チェックしないとなんとなく安心できない
- 他の機能で必要になったメソッドをcherry-pickしまくる
「他の機能で必要になったメソッドをcherry-pickしまくる」が一番つらい。例えば
- feature1でfind_hoge_blogというメソッドを作る
- feature2でも同じメソッドを使うことになった
- find_hoge_blogを作ったcommitをcherry-pickする?
- しかしそのcommitをcherry-pickするとそのメソッド以外のコードも紛れてくる
- コピペするしかない???
のような状況が起こる場合がある。
これらの失敗から
これらの失敗から、最初の戦略を用いることになった。
ユーザーに新機能を見えないようにするパターン
最初に掲載した戦略を見ると、「ユーザーに新機能が見えないようにする工夫」という部分ができれば、細かくmergeすることが出来る。このような工夫のパターンは以下のようなものがある。- ロジックのメソッドから作る
- ユーザーに見えない部分から作る
- エンドポイントを本番では見えないようにする
- 一部のHTMLだけ本番では隠しておく
- リニューアルパターン
ロジックのメソッドから作る
- Logic::Blog->find_public_blogsとか、Logic::Blog->createとかだけ作ってPRする
- ロジックをユーザーに見えるControllerとかで使わなければ当然影響なし
ただし、次の点に注意。
- ユースケースを分析しておかないと、実際に使う時に使えないことに気づくことがある
- 使い方擬似コードを書きつつ、それも合わせてレビューしてもらうと良い
ユーザーに見えない部分から作る
例えば「MySQLのLIKE検索から、Elasticsearchに置き換えて自由なソートも利用可能に」という機能を作ることを考える。これをもう少し分割すると以下の4つになる。
- Elasticsearchの環境作り(手元、開発環境、本番)
- 検索したいデータのElasticsearchへの同期
- 検索クエリでの検索裏側をMySQLからElasticsearchに置き換え
- Elasticsearchを使ってソートを実装
このようにすると、3番まではユーザーに影響がないので、いつリリースしても問題ないので、細かくmergeできる。
エンドポイントを本番では見えないようにする
用途
新機能を特定のURLで提供する時に使える。
方法
- 開発環境ではURLでその機能にアクセスできるように
- 本番環境ではNot Foundにする
- 単なるフラグだけで実装可能
コードのイメージは以下の通り。
# /feature1でアクセスできるエンドポイント(Controller) sub feature1 { my ($req, $res) = @_; # 開発環境でのみONになるフラグ # フラグは環境変数を使ってもいいし、フレームワークの設定などを利用しても良い。 # TODO(feature1_release): リリース前に消す if (!$ENV{CAN_SHOW_NEW_FEATURE}) { return $res->not_found; } ... }
これでこのURLでアクセスする機能は細かくPRを作って開発できる。
最後に見えるようにするには
完成したら、開発環境でのみONになるフラグを使ったif文を消すだけでユーザーに公開できる。
一部のHTMLだけ本番では隠しておく
用途
あるページの一部に新機能追加する時に使える。
方法
テンプレートエンジンを使っていると簡単。フラグをテンプレートに渡して、特定HTMLを消したりするだけで実現可能。
... [%- IF can_show_new_feature %] <section id="feature1"> ... </section> [%- END %] ...
もしこの要素に関係するJSを実装したい時も、要素のあるなしでJSを実行するかどうかを決めれば、JSも細かく作ることができる。
最後に見えるようにするには
完成したら、テンプレートのIF文を削除するだけでユーザーに公開できる。
リニューアルパターン
用途
新機能でページがガラッと変わり、特定HTMLを隠すとかでは対処不能な時に利用できる。
方法
- そのページ用のコントローラとテンプレートのセットをもう1つ用意する
- 新しいコントローラは本番では見えないように
- 最後に前のやつとごそっと入れ替える
別URLでアクセスできるコントローラを作り
# /feature1_renewalでアクセスできるエンドポイント sub feature1_renewal { my ($req, $res) = @_; # 開発環境でのみONになるフラグ # TODO(feature1_release): リリース前に消す if (!$ENV{CAN_SHOW_NEW_FEATURE}) { return $res->not_found; } ... }
feature1_renewal.htmlのようにHTMLファイルも別で作る。
これで、新しいページはユーザーに見えることなく細かく実装できる。
最後に見えるようにするには
- feature1_renewalメソッドをfeature1メソッドに改名し
- feature1_renewal.htmlをfeature1.htmlに改名し
- 対応するCSSやJSなどを移動する
ということをすれば、/feature1でアクセスできるページを一気にリニューアルできる。
まとめ
- 新機能作成時に開発ブランチに細かくmergeしていく戦略について紹介した
- ユーザーに見えないようにするパターンを5つ紹介した
- 実際にはこの5つをうまく組み合わせることで、最後にユーザー向けにリリースするためのPRの変更量を最小にすることができる