GitHub Actionsで定期的なレビュー依頼を自動化する

N番煎じですが、やってみる機会があったので一般化してメモしておきます。

背景: コードレビューを素早く行なうことの重要性

チーム開発で重要なことは色々あります*1が、Pull Requestを出したときに素早くレビューをしてもらえるというのはとにかく重要なことの一つです(日本語版)。

レビュー依頼がきたら今すぐ仕事をやめてレビューをしなければならない、というわけでないですが、一日に2~3回は見るようにしようと思いながら過ごしています。レビュー依頼を手動で「XXXさん(あるいはチーム)、以下のPull Requestのレビューお願いします!」とmentionしてもいいですが、一日に何回も明示的にレビュー依頼をしていると「ちょっとうるさいかもな、まとめて依頼しよう」などを考えてしまうこともあり、案外難しいことだったりします。また、レビューの頻度が人によって異なる場合もありますし「レビュー依頼のPull Requestがあったらなるべく早く見ていくぞ」というチーム文化を作っていくことも重要です。

これを実現するための一つの手段として、定期的なレビュー依頼をBotに自動で行なわせる、というものがあります。前職までの経験でも、これがきちんと機能していたチームは開発体験がすごくよかった印象が強いです。

レビューのフローを整理する

OpenになっているPull Requestをレビュー依頼で通知させればいいかというと、もちろんそんなことはないでしょう。

  • 1: Draftになっている下書きはレビュー依頼はまだ不要
  • 2: レビューが完了して、修正待ちになっているレビューはまだ不要

特に2の状態になっているか / 本当にレビューが必要な状態かは案外把握が難しいです。GitHubに備わっている機能だけで何とかしようとすると割と面倒だったりするので、Pull Requestのラベルに「レビュー依頼」または「WIP」を用意するのが結果としてシンプルになるかもしれません。

GitHub Actionsでレビュー依頼を自動化する

以上を踏まえて、GitHub Actionsで定期的なレビュー依頼を自動化してみましょう。例えば以下のように書くことができます。GitHub Actions以外で実行してもよいですが、tokenの扱いやghコマンドがdefaultで入っているなどを考えると、GitHub Actionsが一番簡単かなと思います。

name: レビュー依頼状態のPull RequestをSlackに通知する

on:
  schedule:
    # 平日のutc1時=JST10時に実行する。複数時間でレビュー依頼をしたい場合は配列を増やせばOK
    - cron: '0 1 * * 1-5'

jobs:
  get_pull_requests_review_required:
    runs-on: ubuntu-latest
    timeout-minutes: 5
    permissions:
      contents: read
      pull-requests: read
      id-token: write
    outputs:
      pull_requests_review_required: ${{ steps.set_pull_requests.outputs.value }}
    steps:
      - name: Checkout code
        uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
      - id: set_pull_requests
        name: レビューすべきPull Requestを出力に格納する
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          PULL_REQUESTS_REVIEW_REQUIRED=$(gh pr list --state open --search "draft:false review:required -label:WIP" --json 'url,title,author' | jq -c '.')
          echo "value=$(echo ${PULL_REQUESTS_REVIEW_REQUIRED})" >> $GITHUB_OUTPUT
  greeting_slack:
    runs-on: ubuntu-latest
    timeout-minutes: 5
    needs: get_pull_requests_review_required
    if: "needs.get_pull_requests_review_required.outputs.pull_requests_review_required != '[]'"
    steps:
      - name: レビュー依頼の文面を投げる
        uses: slackapi/slack-github-action@e28cf165c92ffef168d23c5c9000cffc8a25e117 # v1.24.0
        with:
          channel-id: "C12345abcde"
          payload: |
            {
              "unfurl_links": false,
              "unfurl_media": false,
              "text": "以下のPull Requestのレビューをお願いします!"
            }
        env:
          SLACK_BOT_TOKEN: ${{ secrets.SLACL_NOTIFY_PULL_REQUEST_REVIEWS_TOKEN }}
  notify_slack:
    runs-on: ubuntu-latest
    timeout-minutes: 5
    needs:
      - get_pull_requests_review_required
      - greeting_slack
    strategy:
      matrix:
        pull_request: ${{ fromJson(needs.get_pull_requests_review_required.outputs.pull_requests_review_required) }}
    steps:
      - name: 各Pull Requestのリンクを投稿する
        uses: slackapi/slack-github-action@e28cf165c92ffef168d23c5c9000cffc8a25e117 # v1.24.0
        with:
          channel-id: "C12345abcde"
          payload: |
            {
              "unfurl_links": false,
              "unfurl_media": false,
              "text": "<${{ matrix.pull_request.url }} | ${{ matrix.pull_request.title }}> by ${{ matrix.pull_request.author.login }}"
            }
        env:
          SLACK_BOT_TOKEN: ${{ secrets.SLACL_NOTIFY_PULL_REQUEST_REVIEWS_TOKEN }}

概ね見たまんまですが、見所としてはこんな感じでしょうか。

  • ghコマンドが便利
    • OpenになっているPull Requestやその他細かい条件をさっと書ける、curlでエンドポイントをごにょごにょ叩かなくて済む
  • slackapi/slack-github-actionも便利
    • tokenとchannel-idを用意すればあとはテキストを投げるだけ
  • strategy.matrixを使って、動的にワークフローを展開する
    • 実質的なfor文で並列で処理もしてくれる
    • 今回の場合は複数のPull Requestの情報を加工してから一回SlackのAPIを叩けば十分だが、加工をjqでやると割と面倒なのでstrategy.matrixで片づけた*2

WIPのラベルが付いていないものをレビュー依頼の対象としているので、レビューが完了したらWIPのラベルを付与する必要があります。レビュアーが毎回ラベルの付与をするのは面倒なので、レビューをSubmitしたらWIPのラベルを自動的に付与するactionsも同時に用意しておくと便利でしょう。

name: レビューがsubmitされた場合、Pull Requestに付与されているWIPのラベルを自動で外す

on:
  pull_request_review:
    types:
      - submitted

jobs:
  remove_wip_label_when_review_submitted:
    runs-on: ubuntu-latest
    timeout-minutes: 5
    permissions:
      contents: read
      pull-requests: write
      id-token: write
      repository-projects: read # ref: https://github.com/cli/cli/issues/6274
    steps:
      - name: Checkout code
        uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
      - name: WIPのラベルを外す
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: gh pr edit ${{ github.event.pull_request.number }} --remove-label WIP

それではよいチーム開発を!

*1:例: ぶっ壊れても大丈夫な開発環境があるとか、何も考えずにさっとCI/CDできる環境とか

*2:API callがめっちゃ多くなりそうな場合は避けたほうがいいと思います