[GitHub Actions] Reviewer 알림을 위한 Slack 메시지 보내기

2024-12-17 hit count image

GitHub Actions를 사용하여 Reviewer에 지정된 경우 Slack 메시지를 보내는 방법과 매일 아침에 리뷰어에게 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 저장소(Repository)로 이동합니다.

그 다음 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 Actions에서 Slack 메시지를 보내는 부분을 공통화하여 사용할 수 있습니다. 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에 지정된 Reviewers 모두에게 메시지를 보내게 됩니다.


...
            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 Actions는 평일(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를 사용하여 리뷰어에 지정된 경우 Slack 메시지를 보내는 방법과 매일 아침에 리뷰어에게 PR 목록을 Slack 메시지로 발송하는 방법에 대해서 알아보았습니다.

리뷰어에 지정된 것이 인식되지 않아서 리뷰가 계속 지연되는 문제가 있는 경우, 이러한 방법을 사용하여 리뷰어에게 메시지를 보내어 리뷰를 유도해 보시기 바랍니다.

제 블로그가 도움이 되셨나요? 하단의 댓글을 달아주시면 저에게 큰 힘이 됩니다!

앱 홍보

책 홍보

블로그를 운영하면서 좋은 기회가 생겨 책을 출판하게 되었습니다.

아래 링크를 통해 제가 쓴 책을 구매하실 수 있습니다.
많은 분들에게 도움이 되면 좋겠네요.

스무디 한 잔 마시며 끝내는 React Native, 비제이퍼블릭
스무디 한 잔 마시며 끝내는 리액트 + TDD, 비제이퍼블릭
[심통]현장에서 바로 써먹는 리액트 with 타입스크립트 : 리액트와 스토리북으로 배우는 컴포넌트 주도 개발, 심통
Posts