🔔

GitHub Actionsで再利用可能なSlack通知アクションを作ってみた

2023/12/23に公開

はじめに

この記事はディップ Advent Calendar 2023の23日目の投稿です。

私はフロントエンド課に所属しており、普段はHTML, SCSS, JavaScriptを使用してWebサイトの開発を行っています。

今回の記事では、業務効率化のために最近導入したGitHub Actionsについて紹介していきます。

概要

私の所属チームでは、GitHub Actionsで様々なワークフローを動かしています。

ほとんどのワークフローで、Slackの通知が飛ぶ処理を実装しているのですが、同じような処理を各ワークフローで都度実装しているという状態でした。
そこで、関数化みたいなことをして複数のワークフローで使いまわせるようにしたいなと思い調べたところ、Composite Actionsというものを見つけて、いい感じにやりたいことを実現できたので、今回はそちらを紹介していきます。

前提

今回はGitHub Enterprise Serverの環境下で、Ubuntuのセルフホステッドランナーを使用して動作するように作成しています。

GitHub CloudやGitHubが提供しているランナーで実装する場合は、GITHUB_TOKENやランナーの指定を変更する必要がありますので、適宜読み替えてください。

Composite Actionsとは?

別名で「複合アクション」とも呼ばれています。

ワークフローのstepを別ファイルに切り出すことで、複数のワークフローで再利用することが可能となっている機能です。

プログラミング言語で関数を扱うような感じで、Composite Actionsではinputsoutputsを活用して、引数と戻り値を受け渡しすることも可能となっています。

成果物

今回作成したComposite Actionsは、以下のディレクトリ構成になっています。

.github/actions/
└── slack
    ├── post-message
    │   ├── failure
    │   │   └── action.yml
    │   └── success
    │       └── action.yml
    └── search-thread-message
        └── action.yml

実装

ワークフローの実行成功時にSlackへ通知するAction

スレッド投稿を行いたい場面があるので、chat.postMessageを使用しています。

slack/post-message/success/action.yml
name: Slackに通知(成功)
description: Slackに通知(成功)

inputs:
  slack-channel-id:
    description: SlackのチャンネルID
    required: true
  slack-bot-oauth-token:
    description: SlackのBotのOAuth Token
    required: true
  slack-chat-post-message-url:
    description: Slackのchat.postMessageのURL
    required: true
  slack-color:
    description: Slackの通知色
    required: false
    default: good  # good, warning, danger, or any hex color code (eg. #439FE0)
  slack-title:
    description: Slackのメッセージタイトル
    required: false
    default: '*<@${{ github.actor }}> workflowの実行に成功しました*'
  slack-message:
    description: Slackのメッセージ
    required: false
    default: 以下のリンクから実行結果をご確認ください\n${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
  slack-thread-ts:
    description: Slackのスレッドのts
    required: false
    default: 'null'
  slack-reply-broadcast:
    description: Slackのスレッド投稿をチャンネルにも投稿
    required: false
    default: 'false'

runs:
  using: "composite"
  steps:
    - id: slack-post-message-success
      shell: bash
      run: |
        SLACK_DATA=$(cat <<EOF
        {
        $(if [ "${{ inputs.slack-thread-ts }}" != "null" ]; then
          echo "  \"thread_ts\": \"${{ inputs.slack-thread-ts }}\","
          echo "  \"reply_broadcast\": \"${{ inputs.slack-reply-broadcast }}\","
        fi)
          "channel": "${{ inputs.slack-channel-id }}",
          "attachments": [
            {
              "fallback": "${{ inputs.slack-title }}",
              "pretext": "${{ inputs.slack-title }}",
              "color": "${{ inputs.slack-color }}",
              "author_name": "${GITHUB_REPOSITORY#${GITHUB_REPOSITORY_OWNER}/}",
              "author_link": "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}",
              "text": "${{ inputs.slack-message }}",
            }
          ]
        }
        EOF
        )
        curl -X POST \
          -H 'Content-type: application/json' \
          -H "Authorization: Bearer ${{ inputs.slack-bot-oauth-token }}" \
          --data "${SLACK_DATA}" \
          "${{ inputs.slack-chat-post-message-url }}"

inputs

値の受け渡しが必須なものにはrequired: trueを指定し、
逆に必須でないものにはrequired: falsedefalt:にデフォルト値を指定しています。

# 必須
slack-channel-id:
    description: SlackのチャンネルID
    required: true

# 任意
slack-color:
    description: Slackの通知色
    required: false
    default: good  # good, warning, danger, or any hex color code (eg. #439FE0)

runs

ワークフローでjobsに記述していた処理を、runsに記述します。
using: "composite"を宣言しておくことで、Composite Actionsとして使用できるようになります。

runs:
  using: "composite"  # Composite Actionsとして宣言
  steps:
    ...

steps

stepsでは、シェルスクリプトを記述する際にshell: bashという形で、
シェルの種類を宣言しておく必要があります。

steps:
  - id: slack-post-message-success
    shell: bash  # シェルの種類を宣言
    run: |
      ...

ワークフローの実行失敗時にSlackに通知するAction

失敗時はスレッド投稿を行わずに通知を行うため、webhookを使用しています。

slack/post-message/failure/action.yml
name: Slackに通知(失敗)
description: Slackに通知(失敗)

inputs:
  slack-webhook-url:
    description: SlackのWebhook URL
    required: true
  slack-color:
    description: Slackの通知色
    required: false
    default: danger # good, warning, danger, or any hex color code (eg. #439FE0)
  slack-title:
    description: Slackのメッセージタイトル
    required: false
    default: '*<@${{ github.actor }}> workflowの実行に失敗しました*'
  slack-message:
    description: Slackのメッセージ
    required: false
    default: 以下のリンクから実行結果をご確認ください\n${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}

runs:
  using: "composite"
  steps:
    - id: slack-post-message-failure
      shell: bash
      run: |
        SLACK_DATA=$(cat <<EOF
          {
            "attachments": [
              {
                "fallback": "${{ inputs.slack-title }}",
                "pretext": "${{ inputs.slack-title }}",
                "color": "${{ inputs.slack-color }}",
                "author_name": "${GITHUB_REPOSITORY#${GITHUB_REPOSITORY_OWNER}/}",
                "author_link": "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}",
                "text": "${{ inputs.slack-message }}",
              }
            ]
          }
        EOF
        )
        curl -X POST -H 'Content-type: application/json' --data "${SLACK_DATA}" "${{ inputs.slack-webhook-url }}"

Slackのスレを検索し、thread_tsを取得するAction

スレッド投稿を行う際に、thread_tsを取得する必要があるため、
inputsで検索条件を受け取り、outputsでtsを返すようにしています。

thread_tsとは、スレッドごとに割り振られる一意の値です。
投稿時にthread_tsを指定することで、スレッドに投稿することができます。

slack/search-thread-message/action.yml
name: Slackのスレッドメッセージを検索
description: Slackのスレッドメッセージを検索

inputs:
  slack-channel-id:
    description: SlackのチャンネルID
    required: true
  slack-user-oauth-token:
    description: SlackのユーザーのOAuth Token
    required: true
  slack-search-messages-url:
    description: Slackのsearch.messagesのURL
    required: true
  query-param:
    description: 検索クエリパラメータ
    required: true

outputs:
  slack-thread-ts:
    description: Slackのスレッドのts
    value: ${{ steps.slack-search-thread-message.outputs.slack-thread-ts }}

runs:
  using: "composite"
  steps:
    - id: slack-search-thread-message
      shell: bash
      run: |
        SEARCH_QUERY="in:<#${{ inputs.slack-channel-id }}> ${{ inputs.query-param }}"
        MAX_RETRIES=3
        RETRY_COUNT=0
        TS=null
        while [[ $TS == null && $RETRY_COUNT -lt $MAX_RETRIES ]]; do
          SEARCH_RESPONSE=$(curl -s "${{ inputs.slack-search-messages-url }}" \
            -H "Authorization: Bearer ${{ inputs.slack-user-oauth-token }}" \
            -d "query=$SEARCH_QUERY" \
            -d "sort=timestamp" \
            -d "sort_dir=asc" \
            -d "count=1")
          TS=$(echo $SEARCH_RESPONSE | jq -r '.messages.matches[0].ts')
          if [[ $TS == null ]]; then
            echo "スレッドが見つかりませんでした。3秒後に再検索します"
            RETRY_COUNT=$((RETRY_COUNT + 1))
            sleep 3
          fi
        done
        if [[ $TS != null ]]; then
          echo "slack-thread-ts=$TS" >> $GITHUB_OUTPUT
        else
          echo "スレッドが見つかりませんでした"
          echo "slack-thread-ts=null" >> $GITHUB_OUTPUT
        fi

ワークフローでの使用例

PRのイベントをトリガーに、Slackに通知するワークフローです。

弊社の場合、GitHubのユーザーIDとSlackのユーザーIDが同一であるため<@${{ github.actor }}>という形でメンションを飛ばしています。
GitHubのユーザーIDがメールアドレスのドメインより前の部分である場合に、このような形でメンションを飛ばすことができます。

name: PRのイベントをSlackに通知

on:
  pull_request:
    types: [opened, ready_for_review, reopened, closed]

env:
  GH_ENTERPRISE_TOKEN: ${{ secrets.GITHUB_TOKEN }}
  GH_HOST: ${{ secrets.GH_HOST }}
  PR_NUMBER: ${{ github.event.pull_request.number }}
  PR_LINK: ${{ github.event.pull_request.html_url }}
  PR_ACTION: ${{ github.event.action }}

jobs:
  pr-events:
    runs-on: self-hosted
    steps:
      - uses: actions/checkout@v3
      - name: PRのレビュアーを取得
        id: get-reviewers
        if: ${{ env.PR_ACTION != 'closed' }}
        run: |
          gh_output=$(gh pr view ${{ env.PR_NUMBER }} --json reviewRequests -q '.reviewRequests[].login')
          if [ -z "$gh_output" ]; then
            echo "reviewers=" >> $GITHUB_OUTPUT
          else
            echo "reviewers=$(echo "$gh_output" | awk '{print "<@" $0 ">"}' | tr '\n' ' ')" >> $GITHUB_OUTPUT
          fi
      #-- 以下Slack通知 --#
      - name: Slackのスレッドメッセージを検索
        id: search-thread-message
        uses: ./.github/actions/slack/search-thread-message
        with:
          slack-channel-id: ${{ secrets.SLACK_CHANNEL_ID }}
          slack-user-oauth-token: ${{ secrets.SLACK_USER_OAUTH_TOKEN }}
          slack-search-messages-url: ${{ secrets.SLACK_SEARCH_MESSAGES_URL }}
          query-param: ${{ env.PR_LINK }}
      - name: Slackのメッセージを作成
        id: create-slack-message
        if: ${{ success() }}
        run: |
          SLACK_TITLE=""
          SLACK_MESSAGE="PR作成者: <@${{ github.actor }}>\n以下のリンクからPRを確認してください\n${{ env.PR_LINK }}"
          REPLY_BROADCAST="false"
          if [ "${{ env.PR_ACTION }}" = "ready_for_review" ] || [ "${{ env.PR_ACTION }}" = "opened" ]; then
            SLACK_TITLE="${{ steps.get-reviewers.outputs.reviewers }}*PRがオープンされました*"
            REPLY_BROADCAST="true"
          elif [ "${{ env.PR_ACTION }}" = "reopened" ]; then
            SLACK_TITLE="${{ steps.get-reviewers.outputs.reviewers }}*PRが再オープンされました*"
            REPLY_BROADCAST="true"
          elif [ "${{ env.PR_ACTION }}" = "closed" ]; then
            if [ "${{ github.event.pull_request.merged }}" = "true" ]; then
              SLACK_TITLE="*PRがマージされました*"
            else
              SLACK_TITLE="*PRがクローズされました*"
            fi
            SLACK_MESSAGE="作業者: <@${{ github.actor }}>\n${{ env.PR_LINK }}"
          fi
          echo "slack-title=${SLACK_TITLE}" >> $GITHUB_OUTPUT
          echo "slack-message=${SLACK_MESSAGE}" >> $GITHUB_OUTPUT
          echo "reply-broadcast=${REPLY_BROADCAST}" >> $GITHUB_OUTPUT
      - name: Slackに通知(成功)
        if: ${{ success() }}
        uses: ./.github/actions/slack/post-message/success
        with:
          slack-channel-id: ${{ secrets.SLACK_CHANNEL_ID }}
          slack-bot-oauth-token: ${{ secrets.SLACK_BOT_OAUTH_TOKEN }}
          slack-chat-post-message-url: ${{ secrets.SLACK_CHAT_POST_MESSAGE_URL }}
          slack-title: ${{ steps.create-slack-message.outputs.slack-title }}
          slack-message: ${{ steps.create-slack-message.outputs.slack-message }}
          slack-thread-ts: ${{ steps.search-thread-message.outputs.slack-thread-ts }}
          slack-reply-broadcast: ${{ steps.create-slack-message.outputs.reply-broadcast }}
      - name: Slackに通知(失敗)
        if: ${{ failure() }}
        uses: ./.github/actions/slack/post-message/failure
        with:
          slack-webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}

id

Composite Actionsのoutputsを参照する際に、ワークフロー側でidを指定する必要があります。

- name: Slackのスレッドメッセージを検索
  id: search-thread-message # idを指定
  uses: ./.github/actions/slack/search-thread-message
  ...
- name: Slackに通知(成功)
  if: ${{ success() }}
  uses: ./.github/actions/slack/post-message/success
  with:
    ...
    slack-thread-ts: ${{ steps.search-thread-message.outputs.slack-thread-ts }} # outputsを参照
    ...

uses

リポジトリのルートディレクトリからの相対パスで指定することができます。

uses: ./.github/actions/slack/post-message/success

if

ifを使用することで、条件に応じてステップの実行を制御することができます。

- name: Slackに通知(成功)
  if: ${{ success() }}  # 成功時のみ実行
  uses: ./.github/actions/slack/post-message/success
  with:
    ...
- name: Slackに通知(失敗)
  if: ${{ failure() }}  # 失敗時のみ実行
  uses: ./.github/actions/slack/post-message/failure
  with:
    ...

実行結果

作成したComposite Actionsを使用したワークフローの実行結果です。

ワークフローの実行成功時にSlackへ通知するAction

PRのDraftを作成した際に投稿されるスレッドに投稿しています。

ワークフローの実行失敗時にSlackに通知するAction

まとめ

今回はComposite Actionsを使用して、GitHub Actionsで再利用可能なSlack通知アクションを作成してみました。
Composite Actionsを使用することで、メンテナンス性の高いワークフローを作成することができるようになるので、ぜひ活用してみてください。

https://docs.github.com/ja/actions/creating-actions/creating-a-composite-action

GitHubで編集を提案

Discussion