モチベーション
プルリクエストの変更が多いとレビューが辛い…
→ GitHub Actionsを使えば機械的に弾けるよね
ということで、PR作成時に、変更行数(増分)が指定した値よりも多かったときは自動的に警告してくれるworkflowを作りました。
GitHub Actionsについての基本的な説明は公式ドキュメントに任せます。
Understanding GitHub Actions - GitHub Docs
Reusing workflows - GitHub Docs
コード
name: Limit number of changed lines
on:
workflow_call:
inputs:
limit:
description: Limit of changes
required: true
type: number
extension:
description: >
File extensions that you want to count changes.
Do not include "." before extensions.
Use regular expressions if you want to specify multiple extensions.
(example) kt|yml|gradle
required: true
type: string
exclude_dir:
description: >
Directories where you don't want to count changes.
Do not add "/" at the end of directory name.
type: string
required: false
env:
LIMIT: ${{ inputs.limit }}
EXT: ${{ inputs.extension }}
EXCLUDE: ${{ inputs.exclude_dir }}
URL: ${{ github.event.pull_request.comments_url }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
jobs:
limit-changed-lines:
name: Limit changed lines in the pull request
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
ref: ${{ github.base_ref }}
- run: echo BASE=`git rev-parse HEAD` >> $GITHUB_ENV
- uses: actions/checkout@v3
- name: Count number of changed lines
id: count_changes
run: |
changed=$(git diff $BASE --numstat \
| if [ "$EXCLUDE" != "" ]; then grep -vP "^(\d+\s+)+\/?($EXCLUDE)\/"; else cat; fi \
| grep -P ".*\.($EXT)$" \
| awk '{ additions+=$1 } END { printf "%d", additions }')
echo "CHANGED=$changed" >> $GITHUB_ENV
if [ $changed -gt $LIMIT ]; then
echo "::warning::Exceeds limit. (Limit: $LIMIT, Changed: $changed)"
exit 1
fi
- name: Add comment
run: |
TEXT=":sparkles:OK:sparkles:"
curl \
--silent \
-X POST \
$URL \
-H "Content-Type: application/json" \
-H "Authorization: token $GITHUB_TOKEN" \
--data "{ \"body\": \"$TEXT\"}"
- name: Add error comment if exceeds limit
if: failure() && steps.count_changes.outcome == 'failure'
env:
CHANGED: ${{ env.CHANGED }}
run: |
TITLE="**The number of changed lines exceeds the limit.**"
INFO=":hammer_and_wrench:LIMIT: $LIMIT, :fire:Changed: $CHANGED"
curl \
--silent \
-X POST \
$URL \
-H "Content-Type: application/json" \
-H "Authorization: token $GITHUB_TOKEN" \
--data "{ \"body\": \"$TITLE\n$INFO\"}"
exit 0
使い方
リポジトリの.github/workflows以下に先程のファイルを設置して、PR時に発火する適当なworkflowを用意して、好きなタイミングで呼びます。
# 構成例
.github
└── workflows
├── limit_changed_lines.yml
└── main.yml
name: On pull request
on:
pull_request:
jobs:
limit-changes:
uses: ./.github/workflows/limit_changed_lines.yml
with:
limit: 100
extension: kt|java
exclude_dir: app/src/test
のようにします。
解説
パラメータの受け渡しなどでファイル全体としてはちょっと大きいですが、処理の流れとしては
- main.ymlがPR作成時に発火する
- limit_changed_lines.ymlが走る
-
git diff --numstat
でPR先との差分を取得する -
grep
で拡張子やディレクトリなどを絞り込む -
awk
で集計する - 集計結果を
curl
でPOSTする
という感じです。
ハマリポイント
github.base_refとのdiffがうまくとれない
最初はブランチ間でのdiffを次のようにして取ろうとしていましたが、エラーが出てうまくいってませんでした。
run: |
git diff origin/${{ github.base_ref }} --numstat
fatal: ambiguous argument 'origin/main': unknown revision or path not in the working tree.
workflow内でgit checkout
をハンドリングしてくれるactions/checkout@v3ですが、デフォルトでは高速化に重きが置かれていてヒストリーやブランチ、タグ等は取得しないようになっています。
Number of commits to fetch. 0 indicates all history for all branches and tags.
Default: 1
fetch-depth: ''
actions/checkout: Action for checking out a repo
今回はfetch-depth: 0
にして全部取ってくる必要はないなと思ったので、PR先のブランチのコミットハッシュを環境変数に保存しています。
Environment variables - GitHub Docs
# PR先のブランチをチェックアウト
- uses: actions/checkout@v3
with:
ref: ${{ github.base_ref }}
# コミットハッシュを環境変数に保存
- run: echo BASE=`git rev-parse HEAD` >> $GITHUB_ENV
# PRを作成したブランチにチェックアウト
- uses: actions/checkout@v3
# コミットハッシュを元にdiff取得
- run: |
git diff $BASE --numstat
ローカルマシンとGitHub Actions上でgrepの動作が違う
変更行数の対象外のディレクトリに存在するファイルかどうかを判定するためにgrepを使っていますが、手元のMacとGitHub Actionsが実行されるUbuntu上では微妙に動作が違いました。
$ git diff --numstat
# + - ファイル名
8 2 .github/workflows/limit_changed_lines.yml
# Ubuntuでは\dは期待通りに動かない
grep -vE "^(\d+\s+)+\/?($EXCLUDE)\/"
# -Eではなく-Pを指定する https://stackoverflow.com/a/6901221
grep -vP "^(\d+\s+)+\/?($EXCLUDE)\/"
感想
git関係の処理にはライブラリ等がいくらでもありそうでしたが、アップデートとかがめんどくさそうだしシェルで数行だしな〜と思ってやっていたらgrepの違いでハマったのでなんとも言えない気持ちです。
ともあれ、ちゃんと動いてbotからコメントが来たのは嬉しさがあります。