🐛

プルリク承認時にCIを回そうとしたら予期せぬバグに遭遇した話

に公開

はじめに

こんにちは、ソフトウェアエンジニアリングチームの秦野です。
2025年4月に新卒でウェルスナビに入社し、現在は技術負債の解消やバックエンド開発に携わっています。

今回は開発プロセスにおけるGitHub Actionsコスト削減のため、プルリクエスト(以降、PR)承認時にCIを回そうとした結果、遭遇したバグとその解決方法について紹介します。

https://docs.github.com/en/actions

対象読者

  • GitHub ActionsのCI設計や運用に携わっている方
  • 特にCI実行タイミングの制御に関心がある方

得られること

  • GitHub ActionsのCI設計で注意すべきポイント
  • GitHub仕様についての理解

背景

対象リポジトリはGitHubで管理されており、GitHub Actionsを用いてCIを実行していました。また以下のリポジトリ保護ルールが設定されていました。

リポジトリ保護ルール

  1. PRのマージには2人以上の承認が必要
  2. PRのマージにはステータスチェック[1]のクリアが必要
  3. PRのマージにはベースブランチ(develop)より最新であることが必要
  4. 承認後のPRを更新しても承認は維持されたまま

リポジトリ保護ルール

対象リポジトリの課題

GitHub Actionsのコストが肥大

GitHub ActionsのコストはCI実行時間に基づいて課金されます。
対象リポジトリの2025年9月のCI実行時間は35,389分と、全社共有リソース(総枠50,000分[2])の70%以上を1つのリポジトリで占有している状況でした。
GitHubActionsの使用量の増加

CIの修正

GitHub Actionsコスト肥大の要因として、PRレビュー段階でコード修正が入るたびにCIが再実行され、CIの実行回数が肥大化していることが挙げられました。

よって以下の点を修正しました。

  • 実行タイミングをPR更新時からPR承認時(2人目以降)に変更
  • 同一PRに対して複数のCIが重複実行した場合に、より古いCIをキャンセルする設定を追加[3]

Before

実行タイミング

  • PR作成時
  • PR更新時

従来のCIフロー

After

実行タイミング

  • PR作成時
  • PR承認時(2人目以降)

修正版CIフロー

実装[4][5]

コード
name: 修正したCI

on:
  pull_request:
    branches:
      - "develop"
    types:
      - opened # PR作成時
  pull_request_review:
    branches:
      - "develop"
    types:
      - submitted # PRレビュー時(承認、変更を要求、コメント時)

# 同一PRに対して同時に複数のCIが実行された場合に、より古いCIをキャンセル
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  # 後続のCIを実行するか判定するジョブ
  check-should-run:
    name: Check Should Run
    runs-on: ubuntu-latest
    outputs:
      should-run: ${{ steps.check-should-run.outputs.should-run }}
    steps:
      - name: Check Approval Count
        id: check-approval
        # PRレビュー時にのみ実行
        if: |
          github.event_name == 'pull_request_review' && github.event.review.state == 'approved'
        uses: actions/github-script@v8
        with:
          script: |
            const { owner, repo } = context.repo;
            const prNumber = context.payload.pull_request.number;
            
            # GitHub APIを呼び出してPRレビュー情報(承認、変更を要求、コメント)を取得する
            const response = await github.rest.pulls.listReviews({
              owner,
              repo,
              pull_number: prNumber
            });
            
            # GitHub APIの呼び出しが失敗した場合
            if (response.status !== 200) {
              core.setFailed(`GitHub API 呼び出しが失敗しました。[${response.status}]`);
              return;
            }
            
            # 取得したレビュー情報のうち承認済みのレビュー数を取得する
            const approvals = response.data.filter(r => r.state === "APPROVED").length;
            
            # PR承認数を表示
            await core.summary
              .addHeading('承認数', 2)
              .addRaw(`このPRの合計承認数は **${approvals}** です。`)
              .write();
            
            # 2人以上に承認されているか判定
            if (approvals >= 2) {
              core.setOutput("has-sufficient-approvals", "true");
            } else {
              core.setOutput("has-sufficient-approvals", "false");
            }
            
      - name: Check Should Run
        id: check-should-run
        # 以下の条件を満たす場合に後続のジョブを実行する
        # 1. PR作成時 
        # 2. PR承認時(2人目以降)
        if: |
          (github.event_name == 'pull_request' && github.event.action == 'opened') ||
          steps.check-approval.outputs.has-sufficient-approvals == 'true'
        run: |
          echo "should-run=true" >> $GITHUB_OUTPUT

  build:
    needs: check-should-run
    if: needs.check-should-run.outputs.should-run == 'true'
    # ... 後続のCIジョブ ...

バグ発生

修正したCIは一見問題なく見えましたが、ほどなくして以下のバグが発生しました。

2人以上承認済みのPRを更新するとステータスチェックがリセットされる

このバグはステータスチェックがPRではなく、個々のコミットに紐づくために起きていました。

バグが起こる流れ

  1. ステータスチェックはPRの最新コミットに紐づく
  2. PRが更新されると最新コミットが変わるため、ステータスチェックがリセットされる
  3. しかし修正したCIは「PR更新時」に実行されない
  4. 再度ステータスチェックを通すには、新たなレビュワーに承認してもらうしかない

対応

再度ステータスチェックを通すために、新たなレビュワーに承認してもらうことは現実的でないため、2人以上承認済みのPRについては、その更新ごとにCIを実行するよう修正しました。

PR承認数が正しくカウントされない

レビュー数が多いPRにて、2人以上承認済みにも関わらず承認数が2人未満とカウントされ、CIが正しく実行されないバグが発生しました。

原因は、レビュー情報取得処理がGitHub APIのページネーション[6]に対応していなかったことにありました。GitHub APIはデフォルトで30件(1ページ分)までしかレビュー情報(承認・変更を要求・コメント)を返却しません。よって承認のレビューが31件目以降のレビュー情報だった場合に、その承認を取得・カウントできていませんでした。

対応

全てのレビュー情報を取得するためにGitHub APIのページネーションに対応しました。

連続で承認されたPRのステータスチェックが通らない

同一PRでのCI重複実行を防ぐために導入したconcurrencyの設定が、このバグを引き起こしました。

バグが起こる流れ

  1. PRが短時間に連続で承認されるとconcurrencyの設定により、より古いCIがキャンセルされる
  2. キャンセルされたCIのステータスは「失敗」扱いになる
  3. ステータスチェックは最新コミットに紐づく全てのCIの成功を要求する
  4. 後から実行されたCIが「成功」しても、キャンセルされたCIのステータスが「失敗」のため、ステータスチェックは通らない
  5. PRをマージするには、キャンセルされたCIを再実行して成功させなければならない

対応

CIの実行タイミングをPR承認時(2人目以降)からPR承認時(2人目のみ)に変更し、同一コミットに対するCIの重複実行の発生を防ぎました。

最終版CI

バグを修正した最終版CIは以下の通りです。

実行タイミング

  • PR作成時
  • PR承認時(2人目)
  • PR更新時(PRを2人以上承認済みの場合)
    最終版CIフロー

実装

コード
name: 最終版CI

on:
  pull_request:
    branches:
      - "develop"
    types:
      - opened # PR作成時
      - synchronize # PR更新時
  pull_request_review:
    branches:
      - "develop"
    types:
      - submitted # PRレビュー時(承認、変更を要求、コメント時)

# 同一PRに対して同時に複数のCIが実行された場合に、より古いCIをキャンセル
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  # 後続のCIを実行するか判定するジョブ
  check-should-run:
    name: Check Should Run
    runs-on: ubuntu-latest
    outputs:
      should-run: ${{ steps.check-should-run.outputs.should-run }}
    # PR更新時、承認時に承認数をチェックする
    # PR作成時には無条件で後続ジョブを実行する
    if: |
      github.event_name == 'pull_request' ||
      (github.event_name == 'pull_request_review' && github.event.review.state == 'approved')
    steps:
      - name: Check Approval Count
        id: check-approval
        # PR更新時、承認時に承認数をチェックする
        if: |
          (github.event_name == 'pull_request' && github.event.action == 'synchronize') ||
          (github.event_name == 'pull_request_review' && github.event.review.state == 'approved')
        uses: actions/github-script@v8
        with:
          script: |
            const { owner, repo } = context.repo;
            const prNumber = context.payload.pull_request.number;

            let approvals = 0;
            let totalReviews = 0;
            let page = 1;
            let hasNext = true;
    
            # GitHub APIを呼び出してPRレビュー情報(承認・変更を要求・コメント)を取得する
            # GitHub APIのページネーション対応済み
            while (hasNext) {
              const reviews = await github.rest.pulls.listReviews({
                owner,
                repo,
                pull_number: prNumber,
                per_page: 100,
                page
              });
              totalReviews += reviews.data.length;
              approvals += reviews.data.filter(r => r.state === "APPROVED").length;
              hasNext = reviews.data.length === 100;
              page++;
            }
            
            core.info(`イベント: ${context.eventName} | レビュー総数: ${totalReviews} | 承認数: ${approvals}`);
            core.setOutput("approvals", approvals.toString());

      - name: Check Should Run
        id: check-should-run
        # PR作成時、2人目の承認時、2人以上承認済みのPR更新時に後続ジョブを実行する
        if: |
          (github.event_name == 'pull_request' && github.event.action == 'opened') ||
          (github.event_name == 'pull_request_review' && steps.check-approval.outputs.approvals == 2) ||
          (github.event_name == 'pull_request' && github.event.action == 'synchronize' && steps.check-approval.outputs.approvals >= 2)
        run: |
          echo "should-run=true" >> $GITHUB_OUTPUT

  # ビルドジョブ
  build:
    needs: check-should-run
    if: needs.check-should-run.outputs.should-run == 'true'
    # ... 後続のCIジョブ ...

CI修正の成果

開発量の影響が大きいため単純な比較はできませんが、直近30日間のCI実行時間は12,552分と9月の35,389分から64%ほど減少していました。CI実行タイミング最適化の取り組みが一定の効果として現れていると評価しています。
直近30日間の合計CI実行時間

おわりに

本記事では、GitHub Actionsコスト削減のためPR承認時のみCIを回そうとした結果、遭遇したバグとその解決方法について紹介しました。

今回、CI設計時にはCIパイプラインだけでなく、リポジトリ保護ルールなどリポジトリ全体の設定や挙動を考慮することが大切だと学びました。
この経験を糧に、今後もCI/CD最適化に取り組み開発効率向上に貢献したいと思います。

本記事が、CI最適化に取り組む方々の一助となれば幸いです。
最後までお読みいただき、ありがとうございました。

脚注
  1. https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/collaborating-on-repositories-with-code-quality-features/about-status-checks ↩︎

  2. https://docs.github.com/en/billing/concepts/product-billing/github-actions#free-use-of-github-actions ↩︎

  3. https://docs.github.com/en/rest/using-the-rest-api ↩︎

  4. https://github.com/actions/github-script ↩︎

  5. https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/control-workflow-concurrency ↩︎

  6. https://docs.github.com/en/rest/using-the-rest-api/using-pagination-in-the-rest-api ↩︎

WealthNavi Engineering Blog

Discussion