🤲

Github Actionsでプルリクエストの変更量を取得して、変更量が多かったらプルリクエストを分割するように促す

2024/12/17に公開

なにを解決したいのか

日々の開発でレビューしたり、してもらうときに、レビューするのが辛いと感じることがありました。
個人的な感覚として、プルリクエストの変更量が多いときに辛さを感じてしまいます。

この辛さを、技術で解決できないかと考えました。
この記事では、Github Actionsでプルリクエストの変更量を取得して変更量の妥当性をチェックする方法を紹介します。

プルリクエストの変更量が多いことの辛さ

みなさんは、レビューするプルリクエストの変更量が多くて、そっと閉じてしまった経験はありますか。
私は、変更量が多いと一度そっと閉じることがよくあります。そして、気がつくとレビューが遅くなってしまう、ということもざらです。

私が具体的に感じる辛さは以下の通りです。以下の辛さには、レビュワーとレビュイー両方の目線が入っています。

  • なかなかレビューされない(レビュワー目線)
  • レビューに時間がかかる(レビュイー目線)
  • 変更量が多いとレビューを妥協しがちになり、それにより内部品質の低下が懸念される(レビュイー目線)
  • 多くのコミュニケーションが発生する(双方の目線)

この辛さをなんとか解消できると、レビュイーとレビュワーの双方が幸せになれると思います。

プルリクエストの変更量を減らしてどんな効果を期待するか

変更量を減らすことで、期待したいことは以下の2点です。

  • 1プルリクエストあたりのレビュー時間の短縮
  • 内部品質の向上

コードレビューの時間に時間をかけること自体は悪いことではありませんが、レビューにかかる時間が短縮されることにこしたことはありません。
また、品質の向上については、変更量が少ないとレビューしやすくなるため、レビューの質が向上するとが期待されます。

変更量の基準値をどうするか

変更量の基準値は、Google's Engineering Practices documentationを参考にして決めたいと思います。
Google's Engineering Practices documentationによると変更量の大きさについて以下のように述べられています。

「小さい」とはどういうことか?

一般に、CL の適切なサイズは単一の自己完結的な変更です。これは以下のことを意味します。

どれほど大きければ「大きすぎる」と言えるのかを判断する厳密で手っ取り早いルールはありません。大雑把には 100 行の CL は適度なサイズで、1000 行になると大きすぎると言えますが、これもレビュアーの判断次第です。変更するファイル数も CL の「サイズ」に関係あります。200 行の変更が行われていてもそれが 1 つのファイルで完結していれば許容できるかもしれませんが、 50 ファイルにもわたる変更であれば普通は「大きすぎる」と判断されるでしょう。

Googleでは、100行の変更が適度なサイズといっています。また、200行の変更が1ファイルで完結していれば許容できるとしています。
あくまで参考程度に考えて、変更量の基準値を決めたいと思います。

変更量が小さいとプルリクエストの数が増えてしまうをどう捉えるか

変更量を小さくすると、1プルリクエストあたりのコード量が減るので、結果的にレビューするプルリクエストの数が増えてしまい、総レビュー時間は変わらないもしくは総レビュー時間が増えてしまう問題も起きると考えられます。

個人的には、変更量が小さいことによるプルリクエストの数の増加は、許容できる範囲だと考えています。
変更量が小さいことによるプルリクエストの数の増加は、レビューの質が向上すると考えているためです。

Github Actionsを書いていく

Github Actionsを書く前にどのようなフローを実現したいのか考えます。今回は以下のようなフローを実現したいと思います。

変更量を取得する

まずは、Github CLIを利用して変更量を取得していきます。

Github CLIのgh pr viewコマンドのパラメータとしてプルリクエストの番号が必須なので、Githubのイベントgithub.evnet.numberからプルリクエストの番号を取得して環境変数PR_NUMBERにセットします。

このときonトリガーでpull_requestが設定されていないとgithub.event.numberに値が入ってこないと思うので気を付けてください。

プルリクエストの番号が取得できたらGithub CLIを使って変更差分を取得します。
gh pr viewコマンドを実行するとJSONが返ってくるので、これを環境変数PR_VIEW_JSONにセットします。

name: Diff Checker

on: [pull_request]

env:
  GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
  GH_REPO: ${{ github.repository }}
  PR_NUMBER: 0
  PR_VIEW_JSON: ''

jobs:
  setup:
    runs-on: ubuntu-latest
    steps:
      - name: Set PR number
        run: echo "PR_NUMBER=${{ github.event.number }}" >> $GITHUB_ENV
      - name: Get PR info
        run: echo "PR_VIEW_JSON=$(gh pr view $PR_NUMBER --json changedFiles,additions,deletions)" >> $GITHUB_ENV

https://cli.github.com/manual/gh_pr_view

変更量を計算する

PR_VIEW_JSONの値を元に、変更量を計算していきます。セットされている値はJSONなので、それぞれの値をjqコマンドで環境変数に格納していきます。

それぞれの環境変数に値をセットしたら、変更量を計算します。追加した行と削除した行を足した数を変更量としたいので、PR_ADDITIONSPR_DELETIONSを足してPR_DIFFにセットします。

name: Diff Checker

on: [pull_request]

env:
  GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
  GH_REPO: ${{ github.repository }}
  PR_NUMBER: 0
  PR_VIEW_JSON: ''
+ PR_CHANGED_FILES: 0
+ PR_ADDITIONS: 0
+ PR_DELETIONS: 0
+ PR_DIFF: 0

jobs:
  setup:
    runs-on: ubuntu-latest
    steps:
      - name: Set PR number
        run: echo "PR_NUMBER=${{ github.event.number }}" >> $GITHUB_ENV
      - name: Get PR info
        run: echo "PR_VIEW_JSON=$(gh pr view $PR_NUMBER --json changedFiles,additions,deletions)" >> $GITHUB_ENV
+     - name: Set values
+       run: |
+         echo "PR_CHANGED_FILES=$(echo $PR_VIEW_JSON | jq -r '.changedFiles')" >> $GITHUB_ENV
+         echo "PR_ADDITIONS=$(echo $PR_VIEW_JSON | jq -r '.additions')" >> $GITHUB_ENV
+         echo "PR_DELETIONS=$(echo $PR_VIEW_JSON | jq -r '.deletions')" >> $GITHUB_ENV
+     - name: Calc diff
+       run: echo "PR_DIFF=$(($PR_ADDITIONS + $PR_DELETIONS))" >> $GITHUB_ENV

変更量が適切ではないときはジョブを失敗させる

プルリクエストの変更量が400行を超えた場合はジョブを失敗させたいのでifを使って条件分岐を行います。

変更量が400行を超えた場合はエラーメッセージを出力してexit 1でジョブを失敗させます。変更量が適切であれば条件に入らないのでジョブは成功します。

name: Diff Checker

on: [pull_request]

env:
  GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
  GH_REPO: ${{ github.repository }}
  PR_NUMBER: 0
  PR_VIEW_JSON: ''
  PR_CHANGED_FILES: 0
  PR_ADDITIONS: 0
  PR_DELETIONS: 0
  PR_DIFF: 0
+ PR_DIFF_THRESHOLD: 400

jobs:
  setup:
    runs-on: ubuntu-latest
    steps:
      - name: Set PR number
        run: echo "PR_NUMBER=${{ github.event.number }}" >> $GITHUB_ENV
      - name: Get PR info
        run: echo "PR_VIEW_JSON=$(gh pr view $PR_NUMBER --json changedFiles,additions,deletions)" >> $GITHUB_ENV
      - name: Set values
        run: |
          echo "PR_CHANGED_FILES=$(echo $PR_VIEW_JSON | jq -r '.changedFiles')" >> $GITHUB_ENV
          echo "PR_ADDITIONS=$(echo $PR_VIEW_JSON | jq -r '.additions')" >> $GITHUB_ENV
          echo "PR_DELETIONS=$(echo $PR_VIEW_JSON | jq -r '.deletions')" >> $GITHUB_ENV
      - name: Calc diff
        run: echo "PR_DIFF=$(($PR_ADDITIONS + $PR_DELETIONS))" >> $GITHUB_ENV
+     - name: Check diff
+       if: ${{ env.PR_DIFF }} > ${{ env.PR_DIFF_THRESHOLD }}
+       run: |
+         echo "::error::This PR is too big! Please keep it under $PR_DIFF_THRESHOLD changes."
+         exit 1

特定のラベルがついている場合はジョブをスキップする

どうしても規定の行数に収まらないプルリクエストもあると思います。その場合は、ignore diffラベルをつけてジョブをスキップするようにします。
一行だけ条件を追加するだけで、ラベルがついている場合はジョブをスキップすることができます。

name: Diff Checker

on: [pull_request]

env:
  GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
  GH_REPO: ${{ github.repository }}
  PR_NUMBER: 0
  PR_VIEW_JSON: ''
  PR_CHANGED_FILES: 0
  PR_ADDITIONS: 0
  PR_DELETIONS: 0
  PR_DIFF: 0
  PR_DIFF_THRESHOLD: 400

jobs:
  setup:
+   if: contains(github.event.pull_request.labels.*.name, 'ignore diff') == false
    runs-on: ubuntu-latest
    steps:
      - name: Set PR number
        run: echo "PR_NUMBER=${{ github.event.number }}" >> $GITHUB_ENV
      - name: Get PR info
        run: echo "PR_VIEW_JSON=$(gh pr view $PR_NUMBER --json changedFiles,additions,deletions)" >> $GITHUB_ENV
      - name: Set values
        run: |
          echo "PR_CHANGED_FILES=$(echo $PR_VIEW_JSON | jq -r '.changedFiles')" >> $GITHUB_ENV
          echo "PR_ADDITIONS=$(echo $PR_VIEW_JSON | jq -r '.additions')" >> $GITHUB_ENV
          echo "PR_DELETIONS=$(echo $PR_VIEW_JSON | jq -r '.deletions')" >> $GITHUB_ENV
      - name: Calc diff
        run: echo "PR_DIFF=$(($PR_ADDITIONS + $PR_DELETIONS))" >> $GITHUB_ENV
      - name: Diff is over
        if: ${{ env.PR_DIFF }} > ${{ env.PR_DIFF_THRESHOLD }}
        run: |
          echo "::error::This PR is too big! Please keep it under $PR_DIFF_THRESHOLD changes."
          exit 1

完成したワークフロー

name: Diff Checker

on: [pull_request]

env:
  GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
  GH_REPO: ${{ github.repository }}
  PR_NUMBER: 0
  PR_VIEW_JSON: ''
  PR_CHANGED_FILES: 0
  PR_ADDITIONS: 0
  PR_DELETIONS: 0
  PR_DIFF: 0
  PR_DIFF_THRESHOLD: 400

jobs:
  setup:
    if: contains(github.event.pull_request.labels.*.name, 'ignore diff') == false
    runs-on: ubuntu-latest
    steps:
      - name: Set PR number
        run: echo "PR_NUMBER=${{ github.event.number }}" >> $GITHUB_ENV
      - name: Get PR info
        run: echo "PR_VIEW_JSON=$(gh pr view $PR_NUMBER --json changedFiles,additions,deletions)" >> $GITHUB_ENV
      - name: Set values
        run: |
          echo "PR_CHANGED_FILES=$(echo $PR_VIEW_JSON | jq -r '.changedFiles')" >> $GITHUB_ENV
          echo "PR_ADDITIONS=$(echo $PR_VIEW_JSON | jq -r '.additions')" >> $GITHUB_ENV
          echo "PR_DELETIONS=$(echo $PR_VIEW_JSON | jq -r '.deletions')" >> $GITHUB_ENV
      - name: Calc diff
        run: echo "PR_DIFF=$(($PR_ADDITIONS + $PR_DELETIONS))" >> $GITHUB_ENV
      - name: Diff is over
        if: ${{ env.PR_DIFF }} > ${{ env.PR_DIFF_THRESHOLD }}
        run: |
          echo "::error::This PR is too big! Please keep it under $PR_DIFF_THRESHOLD changes."
          exit 1

おまけ

Github Actionsではgithub-scriptという機能も提供しているのでコマンドでちまちま実行するスクリプトを書かずとも、小さいJavaScriptを実行して変更量を取得することもできます。

少しでも計算が発生する場合はgithub-scriptを使ったほうが、YAMLの見通しが良くなるので良さそうだと思います。

name: Diff Checker

on: [pull_request]

env:
  GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
  PR_DIFF_THRESHOLD: 400

jobs:
  setup:
    if: contains(github.event.pull_request.labels.*.name, 'ignore diff') == false
    runs-on: ubuntu-latest
    steps:
      - name: Diff check
        uses: actions/github-script@v7
        with:
          retries: 3
          retry-exempt-status-codes: 400,401,500
          script: |
            const { data } = await github.rest.pulls.get({
              owner: context.repo.owner,
              repo: context.repo.repo,
              pull_number: context.payload.pull_request.number,
            });
            const prDiff = data.additions + data.deletions;
            if (prDiff > process.env.PR_DIFF_THRESHOLD) {
              throw new Error(`This PR is too big! Please keep it under ${process.env.PR_DIFF_THRESHOLD} changes.`);
            }

参考文献

https://fujiharuka.github.io/google-eng-practices-ja/ja/review/developer/small-cls.html

https://cli.github.com/manual/gh_pr_view

https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/workflow-commands-for-github-actions#example-creating-an-annotation-for-an-error

https://zenn.dev/hashito/articles/aef4de448f341b

https://zenn.dev/snowcait/articles/0e430af5fb1e50

GitHubで編集を提案

Discussion