[GitHub Actions] Reviewerに通知するためのSlackメッセージを送信する

2024-12-17 hit count image

GitHub Actionsを使ってReviewerに指定された場合、Slackメッセージを送信する方法と、毎朝ReviewerにPRリストをSlackメッセージで送信する方法について説明します。

概要

GitHub Actionsを使ってCI/CDを構築すると、ビルド、テストが成功または失敗した時にSlackにメッセージを送りたい場合があります。今回のブログでは、GitHub Actionsを使ってSlackメッセージを送る方法について説明します。

Slackアプリの作成

Slackは対話型メッセージングプラットフォームで、メッセージを受け取るという意味は誰かがメッセージを送ったことを意味します。そのため、GitHub Actionsを使ってSlackメッセージを送るためには、Slackメッセージを送るSlackアプリを作成する必要があります。

まず、次のリンクをクリックしてSlack APIにアクセスします。

すると次の画面が表示されます。

GitHub Actions Send Slack message - Slack API site

画面右上のYour appsを選択してアプリ作成画面に移動します。

GitHub Actions Send Slack message - Create new Slack app

Create New Appを選択して新しいアプリを生成します。

GitHub Actions Send Slack message - Create new Slack app options

この時、From scratchを選択して新しいアプリを生成します。

GitHub Actions Send Slack message - Enter app name and workspace

その後、アプリの名前を入力して、コナぷりを使うSlackのWorkspaceを選択してCreate Appボタンをクリックします。

GitHub Actions Send Slack message - App created

すると上記のようにアプリが生成されることを確認できます。

次はこのアプリにメッセージを送る権限を設定する必要があります。アプリ一覧画面で新しく作成したアプリの名前を選択してアプリ詳細ページに移動します。

GitHub Actions Send Slack message - OAuth and Permissions

その後、左メニューにあるOAuth & Permissionsを選択してOAuth & Permissions画面に移動します。

GitHub Actions Send Slack message - OAuth and Permissions scopes

少しスクロールしてScopesセクションに移動し、Add an OAuth Scopeボタンをクリックしてchat:write権限を追加します。

最後にOAuth Tokensセクションに移動して、Install to (workspace)ボタンをクリックしてSlackアプリをインストールします。

GitHub Actions Send Slack message - OAuth and Permissions install app

アプリをインストールすると、次のようにBot User OAuth Access Tokenが生成されることを確認できます。

GitHub Actions Send Slack message - OAuth and Permissions Bot User OAuth Access Token

次はこのように生成されたトークンをGitHub Actionsで使用するためにGitHubSecretsに保存する必要があります。Slackメッセージを送りたいGitHubリポジトリに移動します。

その後、Settings > Secrets and variables > ActionsメニューをクリックしてActions secrets and variablesページに移動します。

GitHub Actions Send Slack message - GitHub secrets and variables

画面に表示されたRepository secretsNew repository secretボタンをクリックして、NameSLACK_BOT_TOKENを入力し、ValueSlack APIサイトで生成したトークンを入力してAdd secretボタンをクリックして保存します。

チャンネルID

GitHub Actionsを使ってSlackのチャンネルにメッセージを送るためにはCHANNEL_IDが必要です。

CHANNEL_IDSlackで取得できます。個人に直接送りたい場合は、次のように個人プロフィールからCopy member IDをクリックしてCHANNEL_IDを取得することができます。

GitHub Actions Send Slack message - Copy Slack member channel ID

もし、特定のチャンネルにメッセージを送りたい場合は、Open channel detailsをクリックしてチャンネルの詳細情報に移動した後、

GitHub Actions Send Slack message - Copy Slack channel ID

下に表示されたChannel IDをコピーして使えば良いです。

slack-github-actionを使う

Slackから公式で提供されているslack-github-actionを使うとGitHub ActionsからSlackにメッセージを送ることができます。

GitHub Actionsを次のようにslack-github-actionを使うように修正するとSlackにメッセージを送ることができます。


- name: Post to a Slack channel
  id: slack
  uses: slackapi/[email protected]
  with:
    channel-id: 'CHANNEL_ID'
    payload: |
      {
        "text": "GitHub Action build result: ${{ job.status }}\n${{ github.event.pull_request.html_url || github.event.head_commit.url }}",
        "blocks": [
          {
            "type": "section",
            "text": {
              "type": "mrkdwn",
              "text": "GitHub Action build result: ${{ job.status }}\n${{ github.event.pull_request.html_url || github.event.head_commit.url }}"
            }
          }
        ]
      }
  env:
    SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}

簡単なメッセージを送る時にはこのslack-github-actionを使うと便利にSlackメッセージを送ることができます。

Composite Action

会社ではslack-github-actionを使わず、次のようにComposite Actionを使ってSlackメッセージを送る方法を使っています。


name: 'Send Slack messages'
description: 'Send Slack messages'

inputs:
  GITHUB_TOKEN:
    description: 'GitHub token to use GitHub API'
    required: true
  SLACK_BOT_TOKEN:
    description: 'Token for Slack bot'
    required: true
  MESSAGES:
    description: '(JSON) Multiple users and multiple Slack messages'
    required: true

runs:
  using: 'composite'
  steps:
    - name: Send Slack messages
      uses: actions/github-script@v7
      with:
        github-token: ${{ inputs.GITHUB_TOKEN }}
        script: |
          const slackToken = process.env.SLACK_TOKEN
          const messages = JSON.parse(Buffer.from(process.env.MESSAGES, 'base64').toString('utf-8'));
          const channelIDs = {
            'GITHUB_USER_NAME_1': 'USER_CHANNEL_ID_1',
            'GITHUB_USER_NAME_2': 'USER_CHANNEL_ID_2',
            'GITHUB_USER_NAME_2': 'USER_CHANNEL_ID_2',
            'GITHUB_USER_NAME_3': 'USER_CHANNEL_ID_3',
            'GITHUB_USER_NAME_3': 'USER_CHANNEL_ID_3',
            'GITHUB_USER_NAME_4': 'USER_CHANNEL_ID_4',
          }

          for (const message of messages) {
            const { userName, messages: blocks } = message
            const channel = channelIDs[userName]
            fetch('https://slack.com/api/chat.postMessage', {
              method: 'POST',
              headers: {
                'Content-Type': 'application/json',
                'Authorization': `Bearer ${slackToken}`,
              },
              body: JSON.stringify({
                channel,
                blocks,
              })
            })
            .then(response => response.json())
            .then(data => {
              if (!data.ok) {
                throw new Error(`Slack API error: ${data.error}`);
              }
              console.log('Message sent to Slack successfully');
            })
            .catch(error => {
              console.error('Error sending message to Slack:', error);
            });
          }
      env:
        SLACK_TOKEN: ${{ inputs.SLACK_BOT_TOKEN }}
        MESSAGES: ${{ inputs.MESSAGES }}

Composite Actionを使うとGitHub ActionsSlackメッセージを送る部分を共通化して使うことができます。Composite Actionについては次のリンクを参考にしてください。

Reviewer通知

このように作ったComposite Actionを使ってGitHub Actionsを使ってReviewerに指定された場合、Slackメッセージを送る方法について説明します。

GitHub ActionsでReviewerに指定された場合、Slackの個人チャンネルにメッセージを送るためには次のようにGitHub Actionsを作成することができます。


name: '[Slack] Reviewer assigned'

on:
  pull_request:
    types: [review_requested]

jobs:
  notify_for_reviewer:
    runs-on: ubuntu-latest
    timeout-minutes: 1
    steps:
      - name: Checkout Repository
        uses: actions/checkout@v4
      - name: Make Slack messages
        id: make-slack-messages
        uses: actions/github-script@v7
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            const prNumber = context.payload.pull_request.number;
            const prTitle = context.payload.pull_request.title;
            const prLink = context.payload.pull_request.html_url;
            const { data: reviewers } = await github.rest.pulls.listRequestedReviewers({
              owner: context.repo.owner,
              repo: context.repo.repo,
              pull_number: prNumber,
            });

            let slackMessages = []
            const reviewerLogins = reviewers.users.map(user => user.login);
            for (const userName of reviewerLogins) {
              const message = `*Reviewer notification*\n\nYou are assigned to new PR.\n\n- title: ${prTitle}\n- link: ${prLink}`
              slackMessages.push({
                userName,
                messages: [
                  {
                    type: 'section',
                    text: {
                      type: 'mrkdwn',
                      text: message,
                    }
                  }
                ]
              })
            }
            const encodedMessages = Buffer.from(JSON.stringify(slackMessages)).toString('base64');
            core.setOutput('MESSAGES', encodedMessages);
      - name: Send Slack messages
        uses: ./.github/actions/send_slack_messages
        with:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
          MESSAGES: ${{ steps.make-slack-messages.outputs.MESSAGES }}

このGitHub Actionsをもっと詳しく見ていきます。


name: '[Slack] Reviewer assigned'

on:
  pull_request:
    types: [review_requested]
...

このGitHub Actionspull_requestイベントのreview_requestedを使ってPRにReviewerが指定された場合にのみ実行されます。


...
    steps:
      - name: Checkout Repository
        uses: actions/checkout@v4
...

Composite ActionGitから管理されているため、actions/checkout@v4を使ってリポジトリをチェックアウトする必要があります。


...
      - name: Make Slack messages
        id: make-slack-messages
        uses: actions/github-script@v7
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            const prNumber = context.payload.pull_request.number;
            const prTitle = context.payload.pull_request.title;
            const prLink = context.payload.pull_request.html_url;
            const { data: reviewers } = await github.rest.pulls.listRequestedReviewers({
              owner: context.repo.owner,
              repo: context.repo.repo,
              pull_number: prNumber,
            });

            let slackMessages = []
            const reviewerLogins = reviewers.users.map(user => user.login);
            for (const userName of reviewerLogins) {
              const message = `*Reviewer notification*\n\nYou are assigned to new PR.\n\n- title: ${prTitle}\n- link: ${prLink}`
              slackMessages.push({
                userName,
                messages: [
                  {
                    type: 'section',
                    text: {
                      type: 'mrkdwn',
                      text: message,
                    }
                  }
                ]
              })
            }
            const encodedMessages = Buffer.from(JSON.stringify(slackMessages)).toString('base64');
            core.setOutput('MESSAGES', encodedMessages);
...

actions/github-script@v7を使ってReviewerにメッセージを送るためのメッセージを作成します。PRのタイトル、リンクを取得してReviewer用のメッセージを作成します。

Reviewer notification
You are assigned to new PR.
- title: PR title
- link: https://...

このメッセージはPRに指定されたReviewer全員にメッセージを送ることになります。


...
            const reviewerLogins = reviewers.users.map(user => user.login);
            for (const userName of reviewerLogins) {
...

このように作ったメッセージをJSONをそのまま渡すと問題が発生します。そのため、Bufferを使ってbase64にエンコードしてcore.setOutputを使ってMESSAGESに保存して渡すようにしました。


...
            const encodedMessages = Buffer.from(JSON.stringify(slackMessages)).toString('base64');
            core.setOutput('MESSAGES', encodedMessages);
...

最後は、前に作ったComposite Actionを使ってSlackメッセージを送りました。


...
      - name: Send Slack messages
        uses: ./.github/actions/send_slack_messages
        with:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
          MESSAGES: ${{ steps.make-slack-messages.outputs.MESSAGES }}
...

毎朝メッセージ送信

レビュワーがSlackメッセージを受け取った後、PRをレビューする場合もありますが、忙しくてPRをレビューできない場合もあります。このような場合、毎朝レビュワーにメッセージを送ってPRをレビューするように促すことができます。

次のようにGitHub Actionsを作成すると、平日の朝(月〜金)9時30分(日本時間)にレビュワーにメッセージを送ることができます。


name: '[Slack] Every weekday at 9:30 AM'

on:
  schedule:
    - cron: '30 0 * * 1-5'

jobs:
  notify_reviewers_every_weekday:
    runs-on: ubuntu-latest
    timeout-minutes: 1
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
      - name: Make Slack messages
        id: make-slack-messages
        uses: actions/github-script@v7
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            const userList = [
              'GITHUB_USER_NAME_1',
              'GITHUB_USER_NAME_2',
              'GITHUB_USER_NAME_2',
              'GITHUB_USER_NAME_3',
              'GITHUB_USER_NAME_3',
              'GITHUB_USER_NAME_4',
            ]
            const escapeForSlack = (text) => {
              return text
                .replace(/&/g, '&')   // & → &
                .replace(/</g, '&lt;')    // < → &lt;
                .replace(/>/g, '&gt;')    // > → &gt;
                .replace(/"/g, '&quot;')  // " → &quot;
                .replace(/'/g, '&#39;');  // ' → &#39;
            }

            // Get All PRs
            const prList = []
            let pageIndex = 1
            let hasMorePages = true
            while (hasMorePages) {
              const result = await github.rest.pulls.list({
                owner: context.repo.owner,
                repo: context.repo.repo,
                state: 'open',
                per_page: 100,
                page: pageIndex,
              })
              if (result.data.length > 0) {
                prList.push(...result.data)
                pageIndex += 1
              } else {
                hasMorePages = false
              }
            }
            let slackMessages = []
            for (const userName of userList) {
              const reviewerPRList = prList.filter(pr => pr.user.login !== 'dependabot[bot]' &&
                pr.requested_reviewers.some(reviewer => reviewer.login === userName)
              );
              if (reviewerPRList.length > 0) {
                let message = '*Reviewer notification*\n\nYou have assigned PRs. Please review when you have time.\n'
                reviewerPRList.forEach(pr => {
                  message += `\n- <${pr.html_url}|${escapeForSlack(pr.title)}>`;
                });
                slackMessages.push({
                  userName,
                  messages: [
                    {
                      type: 'section',
                      text: {
                        type: 'mrkdwn',
                        text: message,
                      }
                    }
                  ]
                })
              }
            }
            const encodedMessages = Buffer.from(JSON.stringify(slackMessages)).toString('base64');
            core.setOutput('MESSAGES', encodedMessages);
      - name: Send Slack messages
        uses: ./.github/actions/send_slack_messages
        with:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
          MESSAGES: ${{ steps.make-slack-messages.outputs.MESSAGES }}

このGitHub Actionsをもっと詳しく見ていきます。


name: '[Slack] Every weekday at 9:30 AM'

on:
  schedule:
    - cron: '30 0 * * 1-5'
...

このGitHub Actionsscheduleを使って平日(1-5)の朝9時30分(日本時間)に実行されます。


...
    steps:
      - name: Checkout Repository
        uses: actions/checkout@v4
...

Composite ActionGitから管理されているため、actions/checkout@v4を使ってリポジトリをチェックアウトする必要があります。


...
      - name: Make Slack messages
        id: make-slack-messages
        uses: actions/github-script@v7
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            const userList = [
              'GITHUB_USER_NAME_1',
              'GITHUB_USER_NAME_2',
              'GITHUB_USER_NAME_2',
              'GITHUB_USER_NAME_3',
              'GITHUB_USER_NAME_3',
              'GITHUB_USER_NAME_4',
            ]
            const escapeForSlack = (text) => {
              return text
                .replace(/&/g, '&amp;')   // & → &amp;
                .replace(/</g, '&lt;')    // < → &lt;
                .replace(/>/g, '&gt;')    // > → &gt;
                .replace(/"/g, '&quot;')  // " → &quot;
                .replace(/'/g, '&#39;');  // ' → &#39;
            }

            // Get All PRs
            const prList = []
            let pageIndex = 1
            let hasMorePages = true
            while (hasMorePages) {
              const result = await github.rest.pulls.list({
                owner: context.repo.owner,
                repo: context.repo.repo,
                state: 'open',
                per_page: 100,
                page: pageIndex,
              })
              if (result.data.length > 0) {
                prList.push(...result.data)
                pageIndex += 1
              } else {
                hasMorePages = false
              }
            }
            let slackMessages = []
            for (const userName of userList) {
              const reviewerPRList = prList.filter(pr => pr.user.login !== 'dependabot[bot]' &&
                pr.requested_reviewers.some(reviewer => reviewer.login === userName)
              );
              if (reviewerPRList.length > 0) {
                let message = '*Reviewer notification*\n\nYou have assigned PRs. Please review when you have time.\n'
                reviewerPRList.forEach(pr => {
                  message += `\n- <${pr.html_url}|${escapeForSlack(pr.title)}>`;
                });
                slackMessages.push({
                  userName,
                  messages: [
                    {
                      type: 'section',
                      text: {
                        type: 'mrkdwn',
                        text: message,
                      }
                    }
                  ]
                })
              }
            }
            const encodedMessages = Buffer.from(JSON.stringify(slackMessages)).toString('base64');
            core.setOutput('MESSAGES', encodedMessages);
...

actions/github-script@v7を使ってReviewerにメッセージを送るためのメッセージを作成します。各GitHubユーザーが割り当てられたすべてのPRを取得して、レビュワーに送るメッセージを作成します。

Reviewer notification
You have assigned PRs. Please review when you have time.
- PR title1: https://...
- PR title2: https://...
- PR title3: https://...
- PR title4: https://...

このメッセージはすでに作成したGitHubユーザーのリストにある全てのユーザーにメッセージを送ることになります。


...
            const userList = [
              'GITHUB_USER_NAME_1',
              'GITHUB_USER_NAME_2',
              'GITHUB_USER_NAME_2',
              'GITHUB_USER_NAME_3',
              'GITHUB_USER_NAME_3',
              'GITHUB_USER_NAME_4',
            ]
...

このように作ったメッセージをJSONをそのまま渡すと問題が発生します。そのため、Bufferを使ってbase64にエンコードしてcore.setOutputを使ってMESSAGESに保存して渡すようにしました。


...
            const encodedMessages = Buffer.from(JSON.stringify(slackMessages)).toString('base64');
            core.setOutput('MESSAGES', encodedMessages);
...

最後は、前に作ったComposite Actionを使ってSlackメッセージを送りました。


...
      - name: Send Slack messages
        uses: ./.github/actions/send_slack_messages
        with:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
          MESSAGES: ${{ steps.make-slack-messages.outputs.MESSAGES }}
...

完了

これでGitHub Actionsを使ってReviewerに指定された場合、Slackメッセージを送る方法と、毎朝レビュワーにPRリストを送る方法について説明しました。

レビュワーに指定されたことが認識されなくて、レビューが遅れている場合、この方法を使ってレビュワーにメッセージを送ってレビューを促してみてください。

私のブログが役に立ちましたか?下にコメントを残してください。それは私にとって大きな大きな力になります!

アプリ広報

今見てるブログを作成たDekuが開発したアプリを使ってみてください。
Dekuが開発したアプリはFlutterで開発されています。

興味がある方はアプリをダウンロードしてアプリを使ってくれると本当に助かります。

Posts