定期リリースの直前に、mainブランチへ意図しないコミットが大量に混入していることに気づいた。mainの修復は時間内に終わらないと判断し、正常だった検証ブランチ(stg)を基点に、リリース対象のコミットだけをcherry-pickしてリリースブランチを組み直した。
リリースは予定どおり行えたが、この対応は「検証していない組み合わせを本番に出す」ことになる危うい手段でもある。経緯・原因・手順・危うさ・再発防止の順にふりかえる。
背景
業務システムの開発をベンダーから引き継いで、間もない時期だった。リポジトリのブランチ構成や過去の履歴を、自分がまだ把握しきれていない状態で定期リリースの準備をしていた。
ブランチは、本番に対応するmainと、検証環境に対応するstgがある構成だった。
時系列
- リリース前の最終確認として、ローカルで動作を確認した
- 検証環境では出ていないエラーが、ローカルでだけ発生した
- 最初は環境差異を疑ったが、調べるとmainブランチに意図しないコミットが100本単位で入っていた
- revertで戻そうとしたが、コミット同士が絡み合っており、戻すたびに別の問題が出た
- この時点でリリース時刻まで1時間を切っていた
- mainの修復をやめ、stgを基点にリリースブランチを組み直す方針に切り替えた
- リリース対象のコミットだけをcherry-pickし、リリースした
- リリース後、stgから新しいブランチを切り、それを新しいmainとした
原因
直接の原因は、過去のコミットがまとめてmainに流れ込んだことだ。ブランチをマージする順序が想定と違っていたために起きた、と推測している。ただし推測であり、どの操作がきっかけだったかまでは特定できていない。
特定の操作よりも、それが起きうる状態だったことのほうが本質的な原因だと考えている。
- mainに入るコミットを、マージ前に確認する手順がなかった。「マージできればよい」という運用になっていた
- 引き継ぎ直後で、履歴の全体像を把握できていなかった。そのため混入に気づくのがリリース直前になった
- 検証環境(stg)とmainの内容が食い違っていても、それを検知する仕組みがなかった
取った手順
1. 修復か、組み直しかの判断
revertがうまくいかなかった時点で、選択肢は2つあった。
| 選択肢 | 見込み | 問題 |
|---|---|---|
| mainを修復する | どこまでが正しい状態か判別できず、所要時間が読めない | リリース時刻に間に合う保証がない |
| stgを基点に組み直す | stgは正常と分かっており、作業量が見積もれる | 組み直したブランチは検証環境で確認した状態と一致しない |
判断の決め手は、「正常だと分かっている状態がどこか」がはっきりしていたことだ。stgは検証環境で動作を確認できていた。壊れた状態から正しい状態を復元するより、正しいと分かっている状態に必要な変更を足すほうが、作業の見通しが立つ。
2. リリースブランチの組み直し
stgを基点にリリース用のブランチを切り、今回のリリースに含める変更だけをcherry-pickした。
# 検証ブランチを起点に、リリース分だけ切り出す
git checkout -b release/hotfix origin/staging
git cherry-pick <リリース対象のコミットのみ>
対象のコミットは手作業で選んだ。このブランチからリリースを行い、予定時刻に間に合った。リリース後、利用者に影響する問題は出なかった。
3. リリース後のmainの作り直し
壊れたmainは、リリース後も修復しなかった。stgから新しいブランチを切り、それを新しいmainとして運用することにした。リリース直前の判断と同じで、絡み合った履歴を解くより、正常な基点から作り直すほうが確実だと考えたためだ。
なお、一般にブランチを差し替えるときは、既定ブランチの設定、ブランチ保護、CIが参照するブランチ名、各メンバーのローカルブランチなど、付け替えが必要なものが複数ある。差し替え後に全員がfetchし直す手順まで含めて周知する必要がある。
この対応の危うさ
結果としてリリースはできたが、この手段を「うまいやり方」として残すべきではないと考えている。問題は次のとおり。
検証していない組み合わせを本番に出している。 検証環境で確認したのはstgの状態であり、「stgにcherry-pickを重ねた状態」ではない。個々のコミットが正しくても、組み合わせた結果が正しいとは限らない。cherry-pickは元のコミットと別のコミットを作る操作なので、前提となる変更が欠けていても、コンフリクトが出なければそのまま通ってしまう。
コミットの選別が手作業である。 時間に追われた状態で、対象を目で選んでいる。含めるべきコミットの漏れや、含めるべきでないコミットの混入を機械的に防ぐ手段がなかった。
時間制約のもとで判断している。 1時間を切った状態では、組み直したブランチを検証環境でひととおり確認し直す余裕がない。本来は、リリースを延期する選択肢と並べて比較すべき判断だった。
履歴が途切れる。 mainを作り直したため、それ以前のmainの履歴と連続しない。変更の追跡が必要なシステムでは、作り直した事実と理由を記録に残しておく必要がある。
今回問題が出なかったのは、手順が安全だったからではなく、選んだコミットの組み合わせがたまたま成立していたからだと捉えている。
再発防止
実際に変えたこと
マージ前の確認を習慣にした。
- そのタスクに関係するコミットが、過不足なく入っているかを確認する
- コミット数が想定と合っているかを見る。想定より多ければ、意図しない変更が混じっている可能性がある
- マージ前に「このブランチに何が入っているか」を自分の目で確認する
確認には、たとえば次のコマンドが使える。
# マージ先に対して、このブランチで増えるコミットを一覧する
git log --oneline origin/main..HEAD
# コミット数だけ確認する
git rev-list --count origin/main..HEAD
一般論としての補足
ここからは自分が当時実施したことではなく、同種の事故に対して一般に有効とされる対策である。
- ブランチ保護:mainへの直接pushを禁止し、マージリクエスト経由のみにする。承認を必須にすれば、混入がマージ前のレビューで見える
- リリースブランチ運用:リリースごとにブランチを切り、そのブランチを検証環境で確認してからリリースする。検証した状態と本番に出す状態を一致させるのが目的で、今回の危うさに直接効く
- 差分の検知:mainとstgのコミット差分を定期的に確認する。想定外の差分が出た時点で気づければ、リリース直前まで持ち越さずに済む
- 延期の判断基準:リリース直前に異常が見つかったとき、誰に相談し、どういう条件なら延期するかをあらかじめ決めておく
GitLabでのパイプライン構成についてはGitLab CI-CDを実務で運用するときの設計パターンに、その後整えたリリースフローについては引き継いだリリースフローが、監査で初めて意味を持ったに書いている。
まとめ
- mainに意図しないコミットが混入し、時間内に修復できないと判断して、stgを基点にcherry-pickでリリースブランチを組み直した
- 判断の根拠は、正常だと分かっている状態(stg)が明確だったこと
- ただしこの方法は、検証していない組み合わせを本番に出すことになる。うまくいったのは結果論で、手順としては安全ではない
- 根本の対策は、マージ前に入るコミットを確認することと、検証した状態と本番に出す状態を一致させる運用にすること
関連記事: