はじめに
こんにちは。インフラ・ストリーミングチームの小河です。
本記事では GitHub リポジトリにおける master への意図しないマージを防ぐために行った取り組みについて紹介します。
私たちのチームでは、いわゆる Git-flow に少し似たブランチ運用ルールを設けています。
このブランチ運用ルールと GitHub の Pull Request との仕様の組み合わせで master ブランチへの意図しないマージが起きやすい状態になっていました。
今回実際に事故が発生してしまったため、GitHub Actions を活用して再発防止の取り組みを行いました。
なぜ意図しないマージは起こったか
ブランチ運用ルールについて
前提として、私たちのチームのブランチ運用ルールについて説明する必要があります。
master からバージョンブランチ(例: v1.0.1)を切り、そこからさらに各トピックブランチ(例: topic-1、topic-2)を切って作業を行い、最終的にバージョンブランチへマージ、そしてバージョンブランチを master にマージするというフローです。
master へマージされたバージョンブランチはその後削除されます。

私たちのリポジトリでは、master ブランチへ変更がマージされると CI が走り、バイナリがビルドされます。
ビルドされたバイナリが本番環境へそのまま反映される訳でもないので即刻事故につながる訳ではありませんが、できるだけ汚したくないブランチとなります。
また、 master ブランチはリポジトリの default branch としても設定しています。
意図しないマージが発生した経緯
問題は GitHub の Pull Request の仕様によって引き起こされました。
あるトピックブランチのPRを作成した際、マージ先(base branch)が正しくバージョンブランチに設定されていたとします。この PR を PR1 とします。

さらにそのバージョンブランチがマージ元で master がマージ先になっているPRもあったとします。この PR を PR2 とします。

PR2 がマージされてしまうと、PR1 のマージ先が自動的に default branch である master に変更されてしまいます(バージョンブランチは master へマージされると削除されるようになっているため)。

このため、PR作成者が「マージ先はバージョンブランチになっているはずだ」と思い込み、向き先が変わったことに気づかず、うっかり master へマージしてしまうリスクがありました。
PRの作成時には「マージ先がバージョンブランチに向いているか」というチェック項目を設けていましたが、今回のようにPR 作成後 に向き先が変わってしまうケースでは、このチェックをすり抜けてしまいます。
どのような仕組みを導入したか
この問題を解決するため、GitHub Actionsを利用して「特定の条件を満たさないとマージできない」ワークフローを導入しました。
具体的には、PRに設定されているマージ先(base branch)とマージ元(head branch)の名前をチェックするジョブ check-branch-name を作成しました。
実際のワークフローの定義は以下のようになります。
name: 'Prevent Invalid Merge' on: pull_request: types: [opened, reopened, edited] defaults: run: shell: 'bash' jobs: check-branch-name: runs-on: ubuntu-latest steps: - name: Check Branch Name env: BASE_BRANCH_NAME: "${{ github.base_ref }}" HEAD_BRANCH_NAME: "${{ github.head_ref }}" run: | if [[ "${BASE_BRANCH_NAME}" != "master" ]]; then echo "Merging version branch ${HEAD_BRANCH_NAME} into ${BASE_BRANCH_NAME} is allowed." exit 0 fi if [[ "$HEAD_BRANCH_NAME" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then echo "Merging version branch ${HEAD_BRANCH_NAME} into ${BASE_BRANCH_NAME} is allowed." exit 0 fi echo "Error: Merging non-version branch ${HEAD_BRANCH_NAME} into ${BASE_BRANCH_NAME} is restricted." echo "Only version branches (e.g., v1.2.3) are allowed." exit 1
このワークフローをリポジトリに追加した上でGitHubの Ruleset で Require status checks to pass を有効にし、この check-branch-name を必須ステータスチェックとして登録します。

これにより、対象外のブランチへのマージはブロックされるようになります。

ただし、緊急時などに master へ直接マージしたいケースもあります。そういった場合は、Ruleset の Bypass list に特定の role (例: Repository Admin)を登録します。

これにより、権限を持つユーザーはマージを行えるような運用にしています。

他に検討した手段
そもそもバージョンブランチを削除しないようにする
master にマージされたバージョンブランチを削除するとマージ先が master に向いてしまうのだから、そもそもバージョンブランチを削除しないようなブランチ運用にすれば良いのでは?と考えました。
バージョンブランチを削除しなければ、base branch が古いバージョンブランチに向いたままになり、少なくとも master へマージする事故は防げます。
しかし、以下の理由から採用しませんでした。
- バージョンブランチが増え続けて煩雑になる(バージョンブランチを都度削除している元々の理由)
- 古いバージョンブランチにマージしたことに気づかないままになってしまい、本番環境に適用できていると思ったらできていなかったという事故のリスク
GitHub の Ruleset だけでどうにか実現する
GitHub Actions でワークフローを組む前に、やりたいことを実現するために GitHub 側で用意されている仕組みで実現できないかを確認する必要があります。
ブランチごとに設定できる Ruleset では Require a pull request before merging という設定項目があり、マージするために必要なレビューについての条件を設定することができます。
例えば Dismiss stale pull request approvals when new commits are pushed は「新しいコミットがプッシュされたら既存の Approve を無効化する」という機能で、
もしかするとベースブランチを変更した際にも (マージされるコミットが変化するため) レビュー承認が無効化されるのではないかと考えました。
加えて、Require approvals (X 人によるレビュー承認がなければマージを許可しない)を併用すれば、やりたいことが達成できるはずです。
実験してみたところ、ベースブランチの変更では Approve が無効化されることはなく、この方法は使えないことがわかりました。
他の機能についても検討しましたが今回やりたいことを満たすような機能はなく、GitHub Actions でワークフローを組むこととなりました。
おわりに
この仕組みを導入してからすでに数ヶ月が経過しましたが、運用上の問題は特に発生していません。
間違って master ブランチにマージしてしまうかもしれない、という不安がなくなり、安心してマージボタンを押せるようになったため、心理的な負荷が軽減されたと感じています。
私たちのように「汚したくないブランチ」と「default branch」が一致しているようなブランチ運用している場合は、ぜひ導入を検討してみてください。
We are Hiring!
ミラティブのインフラでは日々成長しているミラティブを観測して、サービスの品質をより良くしていくためにインフラを設計してミドルウェアを選定したり、または運用ツールや監視を内製したりしています。 サービスの手触りを感じつつ日々成長するサービスを支えるための技術や知識を学んでみたいという方をおまちしております!