GitHubのMerge Queueとは何か?それと、認識しておきたいこと

同僚に「GitHubのMerge Queueってあんまり知らないんだけど、どう思う?」って聞かれて「あー。僕もあれよく分かってないんだよね」って返事をして、ちょうどいい機会なので見てみた

見てみた感想としては、いくつか気をつけておきたい点があるけど、チームの開発の進め方にうまくはまれば便利な機能だな、という感じ(なんでもそうか・・・)

Merge Queueって?

2023年の7月にGAになったGitHubの機能

プルリクエストをマージするときに「マージ先のブランチ(ベースブランチ)の最新の変更を取り込んでからChecks(つまりCI)を実行して、それが成功したらマージしといて!」ってお願いできる便利機能。名前のとおりQueueになっているので複数のプルリクエストからenqueueできて前から順番に処理してくれる

そうは言われても最初に説明を見た僕は「???」状態だった。「なんでこんな機能が欲しいんだろう?」って。数ヶ月後の僕はもう忘れてそうだから、未来の自分に向けて記事を書いておくことにした

参照

↓利用可能なリポジトリに制限があるので注意

pull request マージ キューは、Organization が所有するパブリック リポジトリ、または GitHub Enterprise Cloud を使っている Organization が所有するプライベート リポジトリで使うことができます。

-- マージキューの管理 - GitHub Docs より

この記事で書くこと・書かないこと

この記事では「Merge Queueとは何か?」について書く。「Merge Queueの設定方法」については書かない

どうしてMerge Queueが欲しいんだろう?

Merge Queueを理解するためには、それが必要となった経緯を理解すると分かりやすそうなので、こういう流れかなと自分が思うものを書いてみる。具体的に考えられるように対象としてmainブランチを考えるけど、別にmainブランチじゃなくてもいい

直接プッシュをしないようにしたい

まずはじめに、mainブランチには直接変更をプッシュしたくない。プルリクエストを必須にしてレビューしたものだけをマージしたい

だから、mainブランチにブランチプロテクションを設定して「直接プッシュせずにプルリクエストだけで更新する(Require a pull request before merging)」という条件をつける

これで、mainブランチの更新にはプルリクエストが必須になる。あんしん

Checksに成功したときだけマージしたい

次に、プルリクエストをマージするときには、コンパイルやリンターや自動テストなどが成功したものだけをマージするようにしたい

だから、ブランチプロテクションに「特定のChecksが成功していないとマージできない(Require status checks to pass before merging )」という条件をつける

これで、例えば自動テストのChecksが成功していないとマージできないようになる。あんしん

最新のベースブランチに対してChecksを実行したい

プルリクエストではChecksが成功していたのに、mainにマージしたら失敗する場合がある。ベースブランチであるmain側が更新されているのに、それを取り込まずにChecksを実行している場合だ

だから、ブランチプロテクションに「ベースブランチが最新じゃないとマージできない(Require branches to be up to date before merging)」という条件をつける

これで、最新のベースブランチへの変更に対してChecksが成功していないとマージできなくなる。あんしん

Checksの完了を待ちたくない

ベースブランチの更新を必須にしたことで安心にはなったのだけど、ちょっとめんどくさいことが発生する

誰か別の人がプルリクエストをマージしてmainブランチが更新されている場合は、それを取り込んで、もういちどChecksを流さないといけないのだ。それはそう

でも、そうなると開発者はChecksの実行を待たないといけない。5分程度の時間だったとしても開発者は他の作業には集中できない。しょうがないからTwitterを見始めたりする(?)

だから、Auto Mergeを利用する

これで、「Enable auto-merge」ボタンを押せば「Checksが成功したらマージしておいてね」とGitHubにお願いしたことになり、開発者はChecksが成功するのを待たずに別の作業を始められる。やったね

複数のプルリクエストでもAuto Mergeしたい

Auto Mergeのおかげで待たなくてよくなったのだけど、複数のプルリクエストが出ている場合には結局開発者によるアクションが必要になる。全部のプルリクエストで「Enable auto-merge」を押したとして、最初の1つがマージされたらそれ以外のプルリクエストでは結局「Update Branch」ボタンを押さないといけない

だから、Merge Queueを利用する(やっとたどりついた!!)

Merge Queueを利用すると「最新のベースブランチに対してプルリクエストの変更を適用してからChecksを実行して、それが成功したらマージしといて!」とお願いしたことになる。しかもそれをQueueとして順番に実行できるので、前から順番に A B C のようにQueueに入ると、BのChecksは main<-A<-B に対して実行される

これで、複数のプルリクエストがある状態でもボタンを押して、そのあとは忘れて次の開発に入れるぞ!やったー!

どんなときに嬉しいか?

ということでどんなときにMerge Queueが嬉しいかなぁって考えてみると、1つのブランチに対してたくさんのプルリクエストが作られる場合かな

モノレポの場合だと特にそうかもしれない。Update Branchの取り合いになりそうよね。そんなときにMerge Queueを使えば、順番にマージされていくので便利

1つのブランチに対してプルリクエストが同時に数個しか作られないなら、Auto Mergeで十分かもしれない

Merge Queueで認識しておきたいポイント

Merge Queueを使うときには、この2点を認識しておきたいなと思った

  • Merge Queue用のワークフロー設定が必要
  • Checksがプルリクエストと共通

Merge Queue用のワークフロー設定が必要

Merge Queueにキューが積まれると、そのプルリクエストの変更をベースブランチにマージした一時的なブランチが作られて、そこに対してワークフローが実行される。そして、Checksが成功するとベースブランチの最新はそのコミットを指すようになる

このときに発生するイベントは、merge_group イベントの checks_requested アクティビティなので、次のような条件になる

on:
  merge_group:
    types: [checks_requested]
    branches: [main]

基本的には on: pull_request と同じジョブを実行すればいいかなと思う

注意しておきたいのはプルリクエストのときとは環境が少し違うという点。たとえば↓は on: pull_request に対する情報

github.ref=refs/pull/9/merge
github.sha=f6dd1d19260fdf954b055f4b66359ca2f593bc6c
github.base_ref=main
github.head_ref=hello

https://github.com/bufferings-hello/hello-merge-queue/pull/9#issuecomment-1936890826

これが、 on: merge_group だと↓こうなる

github.ref=refs/heads/gh-readonly-queue/main/pr-9-585e0bea0e4a1d10ce8ba48e5a6fa9615ee6553e
github.sha=f60ed60e01d9162d835e522f6a2d70bc2661bbdc
github.base_ref=
github.head_ref=

https://github.com/bufferings-hello/hello-merge-queue/pull/9#issuecomment-1936890935

ref を利用しているジョブがある場合は注意が必要だね

Checksの条件がプルリクエストと共通

上記のようにMerge Queueに対しては別のワークフローが実行されることになるが、このワークフローが成功したとみなされるのは「プルリクエストに対して設定されているChecks条件」と同じ条件が満たされた場合になっている

on: pull_request と同じジョブを実行している場合は特に問題ないが、↓の記事のように「Merge Queueの場合は別のジョブを実行したい」という場合には注意が必要となる(なかひこくん、とても参考になりましたありがとうございます)

hackerslab.aktsk.jp

この記事では「違う内容のジョブを同じ名前で実行する」という方法で解決している

それ以外にも↓こんな方法でも対応できる。最後にステータスをチェックするジョブを用意しておく方法。モノレポの場合は変更された内容によって実行されるジョブが違ったりするから、こういうのを最後に置いておくと便利

  job_status:
    runs-on: ubuntu-latest
    needs: [job_a, job_b, job_c]
    if: always()
    steps:
      - if: >-
          contains(needs.*.result, 'failure')
          || contains(needs.*.result, 'cancelled')
        run: exit 1
      - run: echo "ステータスチェックOK"

https://github.com/bufferings-hello/hello-merge-queue/blob/f60ed60e01d9162d835e522f6a2d70bc2661bbdc/.github/workflows/build.yml#L99-L108

GitHub側が、Merge Queue用に別のChecks設定を入れられるようにしてくれても良さそうだが、これは想像だけだけど、もし僕がGitHubの開発者だったとしても「まずはプルリクエストと同じ条件を使うようにしておく」という判断をするだろうなと思う。Merge Queue用の設定追加は、めっちゃ使われるようになってから考えるだろうなと

Merge Queueで注意しておきたいポイント

Merge Queueは、ここまで見てきたような特徴を持っているので、注意しておきたいポイントがある。それは「ワークフローの実行時間が増える」ということ。つまりコストも増える

↓こちらの記事に、実際に使ってみての感想として、そう書かれている

30 Days of Merge Queue

どういうことかというと、これまでは on: pull_request -> on: push の2回実行されていたのが、 on: pull_request -> on: merge_group -> on: push と、3回実行されるようになるということ

ただ、ランチには行ける(ビルドを待たずに別の開発を始められる)ので、そっちの効果の方が大きいだろうなとは思う

Merge Queueをどんなふうに使おう?

ということで最後に「Merge Queueをどんなふうに使おう?」って考えてみる

単純に使うなら

単純に使うならMerge Queueでも、プルリクエストのときと同じジョブを実行すればいい

Checksが成功したら勝手にマージされるので待たなくていい

でも、待ち時間は2倍になる

ちょっとでも短くしようと思ったら

さっきのなかひこくんの記事みたいに、プルリクエストのときには時間のかかるジョブを実行せずに、Merge Queueのときだけ実行するようにするというのはありだなと思う

そうすれば、プルリクエストのときはさくっとChecksの実行結果が見えて、だけど、mainにマージするときには例えばe2eテストまで成功している、という状況が作れる

pushも考えてみる?

main ブランチに on: push で重めのジョブを実行している場合、それを on: merge_group だけで実行することも可能かもしれない。というのもさっき書いたように↓

そのプルリクエストの変更をベースブランチにマージした一時的なブランチが作られて、そこに対してワークフローが実行される。そして、Checksが成功するとベースブランチの最新はそのコミットを指すようになる

on: merge_group の対象が、最後にはベースブランチのコミットになるから

これはつまり「Checksが成功したらそれをmainの最新コミットにする。失敗したらそのコミットは捨てる」という動き

ただ、これはGitHubがドキュメントに書いているわけじゃなくて、僕がこんな感じで github.sha が同じことを確認しただけなので、変わる可能性もある

on: merge_group の場合

github.ref=refs/heads/gh-readonly-queue/main/pr-9-585e0bea0e4a1d10ce8ba48e5a6fa9615ee6553e
github.sha=f60ed60e01d9162d835e522f6a2d70bc2661bbdc
github.base_ref=
github.head_ref=

https://github.com/bufferings-hello/hello-merge-queue/pull/9#issuecomment-1936890935

on: push の場合

github.ref=refs/heads/main
github.sha=f60ed60e01d9162d835e522f6a2d70bc2661bbdc
github.base_ref=
github.head_ref=

https://github.com/bufferings-hello/hello-merge-queue/pull/9#issuecomment-1936891096

長くなった

長くなったけど、もしあなたのチームで「そっちのプルリクエストのCIが通ったら、こっちのプルリクエストのUpdate Branchを押しますね」というセリフが聞こえているならMerge Queueは役に立つかもしれない