3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

GitHub Actionsでプルリクエストの変更行数を制限する

Last updated at Posted at 2022-03-13

モチベーション

プルリクエストの変更が多いとレビューが辛い…
→ GitHub Actionsを使えば機械的に弾けるよね
ということで、PR作成時に、変更行数(増分)が指定した値よりも多かったときは自動的に警告してくれるworkflowを作りました。

GitHub Actionsについての基本的な説明は公式ドキュメントに任せます。
Understanding GitHub Actions - GitHub Docs
Reusing workflows - GitHub Docs

1000行追加したときに怒ってくれるgithub-actionsボットの図
PRがデカイと自動的に怒ってくれるgithub-actionsくん.png

コード

.github/workflows/limit_changed_lines.yml
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
.github/workflows/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

のようにします。

解説

パラメータの受け渡しなどでファイル全体としてはちょっと大きいですが、処理の流れとしては

  1. main.ymlがPR作成時に発火する
  2. limit_changed_lines.ymlが走る
  3. git diff --numstatでPR先との差分を取得する
  4. grepで拡張子やディレクトリなどを絞り込む
  5. awkで集計する
  6. 集計結果を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.sh
$ git diff --numstat
# +       -       ファイル名
  8       2       .github/workflows/limit_changed_lines.yml
grep.sh
# Ubuntuでは\dは期待通りに動かない
grep -vE "^(\d+\s+)+\/?($EXCLUDE)\/"

# -Eではなく-Pを指定する https://stackoverflow.com/a/6901221
grep -vP "^(\d+\s+)+\/?($EXCLUDE)\/"

感想

git関係の処理にはライブラリ等がいくらでもありそうでしたが、アップデートとかがめんどくさそうだしシェルで数行だしな〜と思ってやっていたらgrepの違いでハマったのでなんとも言えない気持ちです。

ともあれ、ちゃんと動いてbotからコメントが来たのは嬉しさがあります。

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?