🙌

レビュー待ちの Pull Request を Slack に通知する

に公開

Pull Request のレビュー完了までの時間を短くするため、Slack にレビュー待ち Pull Request のリンクを通知するようにしたので、実施した内容を紹介する。

この記事では、Slack の通知設定は済んでいることを前提としている。

どのようなものを作りたいと思ったのか

レビューが放置されることを避けたい。かと言って、通知が多いのもアテンションを奪われるので避けたい。

ということで、定期実行で日に数回、Slack に Pull Request を通知してみることにした。
通知内容はその時点でレビュー待ちとなっている Pull Request をリンクにしたものとし、通知頻度はまず、朝 9 時と夕方 17 時に設定した。

また、オープンされて何日放置されているかもわかるようにしたいという要望があったのでそれも表現できるものにしたいと考えた。

actions/github-script を使う

手軽さを優先して actions/github-script を使うことにした。

結果的に YAML 内に記述するスクリプトが長くなってしまったが、今回は手軽さとやっていることのわかりやすさを優先したのでこの点は許容する判断をした。

また、github-script には octocat.js が組み込まれており、Pull Request やその周辺の情報を API を通して取得できる。オープンされた Pull Request を取得してリンクを通知するだけならば REST API で十分なのだが、オープンされてからの経過日数を計算するには複数の API を組み合わせる必要があり、GraphQL で一気に取得することにした。

name: Notify Open PRs

on:
  schedule:
    - cron: "0 0,8 * * 1-5" # UTC での表記。JST では 9 時と 17 時
  workflow_dispatch:

jobs:
  list_open_prs:
    runs-on: ubuntu-latest
    timeout-minutes: 5
    permissions:
      contents: read
      pull-requests: read
    steps:
      - name: Get Open PRs
        uses: actions/github-script@v8
        id: get-prs
        with:
          script: |
            # ここにスクリプトを記述する
            # ....

経過日数の算出に必要なデータを取得する

GitHub の timeline API と Pull Request の情報を GraphQL で取得し、ready for review になってから何日経過したかを計算して日数を計算する方法を採用する。
PullRequest オブジェクトに timelineItems があるので、これの ReadyForReviewEvent を利用して算出することとした。

ready for review となってからの日数があればいいので、itemTypes を READY_FOR_REVIEW_EVENT に限定している。

また、Pull Request の情報を読み出す必要があるため、permissisons の pull-requests read が必要となる。

以下は、GraphQL で情報を取得する部分。READY_FOR_REVIEW_EVENTcreatedAt を取得して日数計算に使用する。

- name: Get Open PRs
  uses: actions/github-script@v8
  id: get-prs
  with:
    script: |
      try {
        const query = `
          query($owner: String!, $repo: String!) {
            repository(owner: $owner, name: $repo) {
              pullRequests(first: 50, states: OPEN, orderBy: {field: CREATED_AT, direction: DESC}) {
                nodes {
                  number
                  title
                  url
                  createdAt
                  isDraft
                  labels(first: 5) {
                    nodes {
                      name
                    }
                  }
                  timelineItems(first: 50, itemTypes: READY_FOR_REVIEW_EVENT) {
                    nodes {
                      ... on ReadyForReviewEvent {
                        createdAt
                      }
                    }
                  }
                }
              }
            }
          }
        `;

        const result = await github.graphql(query, {
          owner: context.repo.owner,
          repo: context.repo.repo
        });

        # -- 以下、長いので一旦略 --

オープンからの経過日数算出と不要な情報のフィルタ、メッセージ文の作成

取得した Pull Request からドラフトのもの (isDraft: true)や、特定のラベルが貼られているものはレビュー対象外として除外している。
日数はあくまで目安なので、Date の差分で取得したミリ秒を 1 日分のミリ秒で割って、整数値で取得するようにした。

これらの情報を使って、通知用の文字列を作成する。改行を 2 つ並べているのは、Slack では改行を 2 つ並べないと認識されないため。

作った文字列は core.setOutput('pr_list', ...) で設定しておき、以降の step で steps.get-prs.outputs.pr_list で参照できるようにしておく。

        // -- 上の続き --

        const result = await github.graphql(query, {
          owner: context.repo.owner,
          repo: context.repo.repo
        });

        const nonSortedPulls = result.repository.pullRequests.nodes
        .filter(pr => !pr.isDraft)
        .map(pr => {
          const readyForReviewEvent = pr.timelineItems.nodes[0];
          const openDate = readyForReviewEvent ? readyForReviewEvent.createdAt : pr.createdAt;
          const daysOpen = Math.floor((Date.now() - new Date(openDate)) / (1000 * 60 * 60 * 24));

          return {
            number: pr.number,
            title: pr.title,
            html_url: pr.url,
            labels: pr.labels.nodes.map(label => ({ name: label.name })),
            daysOpen: daysOpen
          };
        });

        const noPrMessage = 'レビュー対象のPRはありません:tada:';
        if (nonSortedPulls.length === 0) {
          console.log('No open pull requests found.');
          core.setOutput('pr_list', noPrMessage);
          return;
        }

        // 日数が経っているものは早めに確認してもらいたいため、リストの上部にくるように並び替えている。
        const prs = nonSortedPulls.sort((a, b) => b.daysOpen - a.daysOpen);

        // 通知対象を特定のラベルのものに絞りたいならば絞ってしまう。
        const filteredPRs = prs.filter(pr =>
          pr.labels.some(label => ['bug', 'help wanted'].includes(label.name)) &&
          !pr.labels.some(label => label.name === 'dependencies')
        );

        // 経過日数の装飾
        const formatPRList = (prs) => {
          if (prs.length === 0) return '';
          return prs.map(pr => {
            const bombs = ':bomb:'.repeat(pr.daysOpen);
            return `•  <${pr.html_url}|${pr.title}> - ${pr.daysOpen}日経過 ${bombs}`;
          }).join('\n\n'); // Slack が改行を認識できるよう 2 つ重ねている
        };

        const formatStr = formatPRList(filteredPRs);

        let combinedList = '';

        if (formatStr) {
          combinedList += '*レビュー対象 PRs*\n\n' + formatStr + '\n\n';
        }

        console.log('Open Pull Requests:');
        console.log(combinedList);

        core.setOutput('pr_list', combinedList || noPrMessage);
      } catch (error) {
        console.error('Error fetching pull requests:', error);
        core.setFailed(error.message);
        throw error;
      }

Slack で通知する

あとは上で設定しておいた、steps.get-prs.outputs.pr_list を使ってメッセージを作って送信するだけである。

- name: Notify PRs to Slack
  uses: slackapi/slack-github-action@v2.1.1
  with:
    method: chat.postMessage
    token: ${{ secrets.SLACK_BOT_TOKEN }}
    payload: |
      channel: "XXXXXXXXX" # Slack の channel ID
      text: "レビュー対象の Pull Requests :eyes:"
      blocks:
        - type: "section"
          text:
            type: "mrkdwn"
            text: "レビュー対象の Pull Requests :eyes: \n\n${{ steps.get-prs.outputs.pr_list }}"

まとめ

レビュー完了までの時間を短縮するため、以下のことを考え、実践した。

  • レビュー待ち Pull Request の定期通知の実現を考えた
  • github-script で GitHub Actions 上で API を使い、通知に必要な情報を取得する
  • Pull Request に紐づく timeline の API を使い、ready for review にした後の経過日数を算出した
  • 取得した情報を元にフィルタして、必要な Pull Request 情報のみメッセージに出力した

GitHub から取得した各値を使って、フィルタを変更して特定のものだけ出力する、メッセージ上別パートにして特別扱いする等応用も効くので、目的を達成できるよう工夫していきたい。

あしたのチーム Tech Blog

Discussion