Timee Product Team Blog

タイミー開発者ブログ

GitHubマージキューTIPS:CIの実行を最適化し、障害対応を10分高速化する

こんにちは、タイミーでバックエンドのテックリードをしている新谷 (@euglena1215) です。

このシリーズでは「モノリスRailsにマージキューを導入してデプロイフローを安定させる」の続編として、導入時に工夫した点や直面した課題をTIPS形式で紹介しています。前回の記事では「GitHubマージキューの制約:マージメソッドが1つに強制される」について解説しました。

今回は、マージキューの特性を活かしてCIの実行方法を最適化し、特に緊急時のデプロイを高速化した話を紹介したいと思います。

Pull RequestにおけるCIは本当に必須か?

マージキューの最も基本的な機能は、エンキューされたPull Request(PR)を常に最新の master ブランチに取り込んだ状態でCIを実行することです。これによりmaster ブランチのCI成功が保証されるため、私たちはまず「master ブランチへのマージ後のCI」を不要と判断しました。

さらに、私たちはもう一歩踏み込んで考えました。

「そもそも、PR上でのCI実行も必須ではなくなったのでは?」

仮にPR単体でCIが失敗したとしても、その変更はマージキューのCIで検知され、キューから自動的に取り除かれます。master ブランチが壊れることはないため、PR作成時のCIはあくまで開発者のための任意実行とし、マージをブロックする「必須チェック」から外せるのではないか、という発想です。

CIを「必須」と「任意」に分ける方法

この「PRでのCIを必須としない」運用を実現するには、一つ技術的な壁がありました。

GitHubの仕様上の壁

GitHubの仕様では、PRの必須チェック(Required Check)とマージキューの必須チェックを分けることができません。マージキューでマージ前にチェックしたいCIはPRでも常に実行する必要があります。
こちらはマージキューへのフィードバックとして挙がっていますが、2025年7月時点では対応されていませんでした。

ref. https://github.com/orgs/community/discussions/46757#discussioncomment-4912738

PRでの必須チェックを外すためにチェックを外すと、マージキューでも必須ではなくなってしまい、CIが実行されないままマージされる問題が生じます。

ワークフロー分割による解決策

そこで我々は、CIのコアロジックを再利用可能なワークフロー(_ci.yml)として切り出し、それを呼び出す2つのワークフローに分割することで、この問題を解決しました。

1. ci.yml(マージキューでの必須CI)

これをブランチ保護ルールの必須チェックに設定します。このワークフローはpushイベントとmerge_groupイベントの両方でトリガーされますが、github.event_nameを判定し、merge_groupイベントでない場合はskip: trueを渡すことで、CIの実行をスキップします。

これにより、「必須チェックとしては存在するが、PR上では即座に完了(スキップ)し、マージキューでのみ実行される」状態を作り出せます。

# .github/workflows/ci.yml
name: ci

on:
  push:
    branches:
      - '**'
      - '!master'
  merge_group:

jobs:
  ci:
    # ...
    uses: ./.github/workflows/_ci.yml
    with:
      # merge_group イベントでない場合は true を渡し、ワークフローをスキップさせる
      skip: ${{ github.event_name != 'merge_group' }}
    secrets: inherit

2. ci_branch.yml(PRでの任意CI)

こちらは必須チェックには設定しません。pushイベントでのみトリガーされ、常にskip: falseを渡してCIを実行します。開発者はこのCIの結果を参考にしますが、完了を待たずにエンキューできます。

# .github/workflows/ci_branch.yml
name: ci branch

on:
  push:
    branches:
      - '**'
      - '!master'

jobs:
  ci:
    uses: ./.github/workflows/_ci.yml
    with:
      # こちらは常に false を渡し、CIを実行させる
      skip: false
    secrets: inherit

この構成により、「PR上ではCI実行は必須ではないが、マージキューではCI実行が必須」という状態を実現しました。

実行結果を確認する

挙動を確認するために、実行結果を確認してみましょう。

PR上では、ci.yml と ci_branch.yml が実行されます。必須であるci.ymlはほとんどのステップがskipされているので全体が数秒で確認できます。対して、ci_branch.ymlでは数分かかっていますが、必須ではないので実行途中でもマージキューにエンキューできます。

PR上でのci.ymlの実行結果

PR上でのci_branch.ymlの実行結果

対して、マージキュー上ではci.ymlのみが実行されます。マージキューで実行した場合、ci.ymlはskipされずに実行されるので、CIが失敗しているのにmasterブランチにマージされてしまうといったリスクはありません。

マージキュー上でのci.ymlの実行結果

障害対応のデプロイを10分短縮

この最適化は、特に緊急の修正(Revertなど)をデプロイする際に大きな効果を発揮します。

  • 変更前
    • featureブランチのCI(10分) + masterマージ後のCI(10分) + デプロイ(5分) = 合計25分
  • 変更後
    • マージキューのCI(10分) + デプロイ(5分) = 合計15分

結果として、障害発生から復旧までの時間を10分短縮できました。

まとめ

マージキューはmaster ブランチを安定させるための機能ですが、その特性をうまく利用し、CIの実行方法を工夫することで、デプロイの安定化とデプロイの高速化を両立させることができました。まさに一石二鳥の改善だったと感じています。

次回は、マージキュー導入時に発覚した、GitHubのブランチ保護機能(Ruleset)との思わぬ非互換性について紹介します。