Github Actionsでプルリクエストの変更量を取得して、変更量が多かったらプルリクエストを分割するように促す
なにを解決したいのか
日々の開発でレビューしたり、してもらうときに、レビューするのが辛いと感じることがありました。
個人的な感覚として、プルリクエストの変更量が多いときに辛さを感じてしまいます。
この辛さを、技術で解決できないかと考えました。
この記事では、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
変更量を計算する
PR_VIEW_JSON
の値を元に、変更量を計算していきます。セットされている値はJSONなので、それぞれの値をjq
コマンドで環境変数に格納していきます。
それぞれの環境変数に値をセットしたら、変更量を計算します。追加した行と削除した行を足した数を変更量としたいので、PR_ADDITIONS
とPR_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.`);
}
参考文献
Discussion