iimon TECH BLOG

iimonエンジニアが得られた経験や知識を共有して世の中をイイモンにしていくためのブログです

GitHub Actionsでプルリクの確認漏れを防ぐ!

みなさんこんにちは。

コードレビューにおいて、プルリク(以下、PR)の確認が遅れたり見落としたりして困ったことはありませんか?

通知が埋もれてしまったり、後回しにして結局忘れてしまったりなど、
理由は様々ですが、PRの確認が遅れるとリリースまでのスピードが低下する可能性があります。

社内では朝会前後や就業前など、時間を決めてやるように取り組んでいますが
私自身、たまに漏れてしまうことがある状態です。

今回は、こうした確認漏れを防げるような仕組みを作れればと思っています!

弊社のPRの考え方について

PRの出し方

  1. 背景を記載する
    レビュワーはさまざまなタスクに関わっているため、PRだけでは意図を把握しづらいことがあります。
    背景情報などを記載することで、スムーズなレビューにつながります。

    • 記載する内容
      • 修正の目的:なぜこの修正を行うことになったのか
      • 関連ドキュメント:チケットのURLなど
      • 影響範囲:どの部分に影響があるのか
      • 重点的に見てほしい箇所:不安な点や特にレビューをお願いしたい部分
  2. レビューアーの選定
    選び方の基準

    • 仕様に詳しい人:修正の内容が問題無いかを把握しているメンバー
    • 変更を知っておいてほしい人:開発や運用のために把握しておくべきメンバー

レビューについて

  1. コードの品質を保つ
    • バグが起きる様な書き方をしていないか
    • リファクタはテストコードがある状態(品質が担保されている)で実施されているか
    • テストコードが書かれているか。意味のあるテストになっているか
  2. コードの把握
    • コードを把握することで同じ様なメソッドの作成を防げる
  3. 他人のコードを読むことによって勉強になる
    • 分からなければ書いた本人に直接聞く事ができる。
    • システムのコードなのでネットに落ちているコードや本に書いてあるコードよりも勉強になる
  4. 質問もok
    • ここってどうなってるんですか?、この想定で書いてますか?など、コードから読み取れなかったら聞いてみるのOK
  5. 良い部分にもコメントするともっと良い
    • 「この書き方勉強になります!」や「書き方が綺麗になったね!」などをコメントするのもよし!
    • コードのコメントだけではなくてConversationの部分にもコメント書ける
    • ただし、無理にコメントはしない
      • 無理やり捻り出すくらいであれば、感謝のコメントとか残した方がより良い

レビューをする側もされる側も感謝とリスペクトを忘れない

PRの確認漏れを防ぐ方法

では本題に戻って、PRの確認漏れをどのようにするかですが

GitHub上で未レビューのPRを一覧で確認することは可能です。 https://github.com/pulls/review-requested

このURLをSlackのリマインダーで定期的に通知する方法も有効ですね。 ただ、「アクセスする」という作業が必要になるため、より手軽に確認できるよう、Slackの通知に全てが確認できるよう、仕組みが作れればと思いました!

仕様

  • GithubActionsを使用
  • レビューリクエスト中のPRを取得
  • SlackのDMで通知
  • スケジュールで実行(平日の10:30、17:30の2回実行)
    • 一応workflow_dispatchを使って手動でも実行可能ですが、個人的には実行するのであれば
      https://github.com/pulls/review-requestedを見た方がいいかなと思っています。

1. ワークフローの定義

name: PR Status Notifier Bot

on:
  workflow_dispatch:
  schedule:
    - cron: "30 1,8 * * 1-5" # 月〜金の 10:30, 17:30 に実行 (UTC)

permissions:
  contents: read
  pull-requests: read

jobs:
  notify-unapproved-prs:
    runs-on: ubuntu-latest
  • workflow_dispatch: 手動実行可能
  • schedule: 月曜〜金曜の10:30 / 17:30(JST)に自動実行
    • 指定した時刻には動作していなさそう。(2-3分遅れ?)
  • permissions: PR情報を取得するための権限を設定

2. GitHubのアカウント名とSlackのユーザーIDを取得

      - name: GitHub, Slack ユーザーIDのリスト情報を取得
        id: load_slack_github_users
        run: echo "SLACK_GITHUB_USERS=$(echo '${{ vars.SLACK_GITHUB_USERS }}' | jq -c | tr -d '\r\n')" >> $GITHUB_ENV
  • SLACK_GITHUB_USERS は、GitHub のアカウント名と Slack ユーザー IDのキーバリューのリスト
  • GitHub の シークレット (Secrets) に環境変数として設定
    • シークレットの設定には、他にもトークンなども設定しています。

こんなイメージです

{
  "github_hoge1": "U12345678",
  "github_hoge2": "U87654321"
}

3. GitHubAPIで未レビューのPRを取得

      - name: 指定されたユーザーの未承認PRを取得
        id: fetch_prs
        uses: actions/github-script@v6
        with:
          github-token: ${{ secrets.IDE_GITHUB_TOKEN }}
          script: |
            const organization = 'hogehoge';
            const slackUserMap = JSON.parse(process.env.SLACK_GITHUB_USERS);
            let slackUsers = Object.values(slackUserMap);
            let unapprovedPRs = [];

            for (const slackUserId of slackUsers) {
              const githubUser = Object.keys(slackUserMap).find(user => slackUserMap[user] === slackUserId);
              if (!githubUser) {
                continue;
              }

              // draftのPRを除外、レビュー待ちのPRを取得
              const query = `is:pr is:open org:${organization} review-requested:${githubUser} -draft:true`;
              const { data } = await github.rest.search.issuesAndPullRequests({ q: query, per_page: 100 });

              // PR情報をリストに追加
              for (const pr of data.items) {
                unapprovedPRs.push({
                  repo: pr.repository_url.split('/').pop(),
                  title: pr.title,
                  url: pr.html_url,
                  creator: pr.user.login,
                  slack_user: slackUserId
                });
              }
            }

            // 環境変数にセット
            const fs = require('fs');
            fs.appendFileSync(process.env.GITHUB_ENV, `PR_RESULT=${JSON.stringify(unapprovedPRs) || "[]"}\n`);
  • GitHub API の search.issuesAndPullRequests を活用
    • is:pr is:open → 開いているPR
    • review-requested: → レビュー待ちのPR
    • -draft:true → Draftは除外

github.com

4. SlackにPR一覧をDMで通知

1. PRのグループ化
      # Slackに未レビューのPRをDMで通知
      - name: Slackに未レビューのPRをDMで通知
        if: env.PR_RESULT != '[]'
        run: |
          if [[ -z "$PR_RESULT" || "$PR_RESULT" == "[]" || "$PR_RESULT" == "null" ]]; then
            exit 0
          fi

          # PRをリポジトリごとにグループ化
          GROUPED_JSON=$(echo "$PR_RESULT" | jq -c 'group_by(.repo) | map({repo: .[0].repo, prs: .})')

          declare -A USER_MESSAGES

          while read -r repo_group; do
            repo_name=$(echo "$repo_group" | jq -r '.repo')

            while read -r pr; do
              pr_title=$(echo "$pr" | jq -r '.title')
              pr_url=$(echo "$pr" | jq -r '.url')
              pr_creator=$(echo "$pr" | jq -r '.creator')
              slack_id=$(echo "$pr" | jq -r '.slack_user')

              # メッセージをユーザーごとにグループ化
              if [[ -z "${USER_MESSAGES[$slack_id]}" ]]; then
                USER_MESSAGES[$slack_id]="*未レビューのPR一覧*\n"
              fi

              # リポジトリ名がすでに含まれていない場合のみ追加
              if [[ ! "${USER_MESSAGES[$slack_id]}" =~ "📌 *${repo_name}*" ]]; then
                USER_MESSAGES[$slack_id]+="\n📌 *${repo_name}*\n"
              fi

              # PRをリストとして追加
              USER_MESSAGES[$slack_id]+="  - *<${pr_url}|${pr_title}>* (作成者: ${pr_creator})\n"

            done < <(echo "$repo_group" | jq -c '.prs[]')

          done < <(echo "$GROUPED_JSON" | jq -c '.[]')
  • ユーザーごとに通知メッセージを作成

    • 送信内容:
      • タイトル
      • URL
      • レビューイ
  • 同じリポジトリ内のPR は一括で表示

2. SlackのDMチャンネルのIDを取得
          # Slackへ一括送信(1 ユーザー 1 メッセージ)
          declare -A DM_CHANNELS

          for slack_id in "${!USER_MESSAGES[@]}"; do
            DM_CHANNEL_ID=$(curl -s -X POST -H "Authorization: Bearer ${{ secrets.SLACK_TOKEN }}" \
                -H "Content-Type: application/json" \
                -d "{\"users\": \"$slack_id\"}" \
                https://slack.com/api/conversations.open | jq -r '.channel.id')

            if [[ -z "$DM_CHANNEL_ID" || "$DM_CHANNEL_ID" == "null" ]]; then
              continue
            fi

            DM_CHANNELS["$slack_id"]="$DM_CHANNEL_ID"
  • SlackのユーザーIDではDM送信のAPIでは使えないため、使用可能なDMのチャンネル IDを取得する。
3. SlackにDM送信
            # メッセージを送信
            MESSAGE="${USER_MESSAGES[$slack_id]}"

            RESPONSE=$(curl -s -X POST -H "Authorization: Bearer ${{ secrets.SLACK_TOKEN }}" \
                -H "Content-Type: application/json" \
                -d "{
                  \"channel\": \"$DM_CHANNEL_ID\",
                  \"text\": \"$MESSAGE\"
                }" https://slack.com/api/chat.postMessage)

          done
  • 各ユーザーに対して、1 通のメッセージ で未レビューのPR一覧を送信
  • APIはchat.postMessageを使用し、DM チャンネルにメッセージを送信

api.slack.com

送信結果

📌で 各リポジトリごとにまとまり、未対応のPRが送信されました! 6件も残っていますね!!しっかりしましょう!

また、単純に、プルリクを確認しましょう!という 定期的なリマインド通知より、 全ての情報が分かる今回の方が 効果がある通知になるのではと思いました。

APIの利用制限について

GitHub API

API 用途 頻度 利用回数の制限
search/issues レビュー待ちのPRを取得 回/ユーザー 30リクエスト/分, 5000リクエスト/時 (GitHub API Rate Limits)

・Slack API

API 用途 頻度 利用回数の制限
conversations.open DMチャンネルIDを取得 1回/ユーザー 50リクエスト/分 (Slack API Rate Limits)
chat.postMessage DMでメッセージを送信 1回/ユーザー 50リクエスト/秒 (Slack API Rate Limits)

GitHub側の利用制限が厳しいため、対象者が30人以上になる場合は、送信時間を分けるようにするなど、色々と考慮が必要ですね。。

まとめ

APIの利用制限が厳しいため、代替可能なAPIの検討や、処理や仕様の改善など、まだ工夫できそうではあるかなと思っていますが やりたかったことは実現できたので、良かったです!

色々と触ってみてGitHub Actions について少し理解を深めることができ、良い機会になりました。

最後になりますが、現在弊社ではエンジニアを募集しています!

この記事を読んで少しでも興味を持ってくださった方は、ぜひカジュアル面談でお話ししましょう!

iimon採用サイト / Wantedly / Green

最後まで読んでいただきありがとうございました!!!

参考

conversations.open method | Slack

chat.postMessage method | Slack

REST API endpoints for search - GitHub Docs

Slack API を利用してユーザーにダイレクトメッセージを送信する | DevelopersIO

plugin-rest-endpoint-methods.js/docs/search/issuesAndPullRequests.md at main · octokit/plugin-rest-endpoint-methods.js · GitHub