怠惰な自分を救いたい ~GitHub Actions×WebHooksでDiscordにメンション付きの通知を送ろう~

この記事は、 KENTEM TechBlog アドベントカレンダー2024 1日目、12月1日の記事です。

はじめに

 こんにちは!今年新卒で入社したK.Mです!弊社では社内プログラミングコンテストとして、チームでアプリ開発を行う取り組みがあります。私自身も同期同士でチームを組んでアプリ開発を行っていますが、プルリクについていたメンションの通知が来なくて修正依頼に気づかない・・・なんて自体が発生しました。 GitHubのSettingからWebHooksを使ってDiscordに通知を送ることができますが、怠惰な私はそれだけではDiscordを開かず修正依頼を放置していました笑
 なので、GitHub ActionsとDiscordのWebHooksを使って メンション付きの通知 を送るようにしましたので紹介したいと思います!

GitHub Actions? WebHooks?

GitHub Actionsとは?

 GitHub Actionsは、コードのビルド、テスト、デプロイなどの自動化を行うためのワークフローを構築するための機能です。YAML形式の設定ファイル(.yml)を使ってワークフローを定義し、リポジトリ内の.github/workflowsフォルダに配置します。GitHub Actionsでワークフローを定義することで、プルリクエストが作成されるたびにテストを実行し、成功した場合のみデプロイする設定を追加することができます。

WebHooksとは?

 Webhooksとは、アプリケーションが特定のイベントに応じて別のアプリケーションに通知を送るための仕組みです。ウェブフックを使用すると、指定されたURLに対してHTTPリクエストを自動で送信し、外部サービスやシステムと連携することができます!DiscordのWebHooksを使うことで、GitHubのような外部サービスからDiscordチャンネルにメッセージを送信するための仕組みを作ることができます!先程も簡単に触れましたが、GitHubからプルリクエストが出てきたら通知を送るだけであれば、リポジトリのSettingsから設定を少し行うことで簡単に行うことができます!
 ただ、通知のフォーマットが決まっていることから、メンション付きの通知を送ることができなかったので、メンションをつけるためにGitHub Actionsと一緒に活用していきます。  

実装

WebHooksの作成

 まずはDiscordでWebHooksを作成していきます!通知を送りたいチャンネルを選択or作成します(私たちはgithub_pullrequestという専用のチャンネルを作成しました)。
 通知を送るチャンネルが決まったらチャンネル名の隣にある「歯車マーク」→「連携サービス」→「ウェブフックを作成」の順にボタンを押すとWebHooksの作成画面に移動できます。

 この画面で「ウェブフックを作成」を押すと作成完了です!WebHookを作成すると以下のような画面になります。
 このWebHook使う場合は、「CaptainHook」をクリックすると「ウェブフックURLをコピー」と出てくるのでこれでURLを取得することができます!外部サービスと連携する際にはこのURLを使えば簡単に連携させることができます。
 余談ですが、GitHubのSettingsからWebHooksを使う際には、URLの最後に「/github」とつける必要があります(今回のGitHub Actionsから使用する場合は不要です!)。

こちらの作業を行うにはウェブフックの管理権限が必要です!デフォルトでは権限がないので、サーバー管理者に権限をもらうかフックのURLをつくってもらいましょう。

WebHookのSecret化

 続いて、ワークフロー内で環境変数として扱うためにWebHookをSecretに登録します!Secretとは、APIキーやアクセスキーのような機密情報を安全かつ簡単に、環境変数のように扱うための仕組みです!
 Secretへの登録は、GitHubのタブ「Settings」を押し、左側のSecurity内の「Actions」を押します!  するとSecretsの管理画面に行きますので、「New repository Secrets」と書かれたボタンを押すとこのような画面に遷移します。  あとはSecretの名前を設定し、Secretと書かれた部分に先程コピーしたWebHookのURLを張り付け、「Add Secret」を押せばSecretの設定は完了です!
 余談ですが、一度Secretとして設定した値は見ることができません。これは後にワークフローを作成していく過程で、デバッグ用に出力しても「******」で隠されてしまいます。もし間違えて別の値を設定してしまった場合は再度作り直す必要がありますのでご注意ください!

アカウント名の紐づけ設定

 メンションを送るにはDiscordのIDとGitHubのアカウント名が紐づいている必要があります。先程記載したGitHubのSeacretを用いて環境変数とすることも考えたのですが、ワークフローの処理が複雑になる+プライベートリポジトリでメンバーも気の知れた同期という事で、紐づけ用のJSONファイルを作成します。リポジトリの直下に以下のようなJSONファイルを作成しました。

{
    "GitHubのアカウント名1" : "DiscordのID1",
    "GitHubのアカウント名2" : "DiscordのID2",
}

 GitHubのアカウント名はそのままですが、DiscordのIDはDiscordの「アカウントの設定画面」→「詳細設定」→「開発者モード(ラジオボタン)」を押すと、設定画面のアカウント名横に三点リーダーが出てきますので、ここから取得することができます。

GitHub Actionsでワークフロー作成

 ここからメインディッシュであるGitHub Actionsでワークフローを作成していきます。GitHub上で「Actions」→「New workflow」を押して、次の画面で「set up a workflow yourself」をクリックします。  するとmain.ymlの作成画面が出てきます!作成画面で一度コミットしてからVSCodeで編集作業を進めてもよいですし、そのままブラウザ上でYMLファイルの中身を書いていってもよいです。
 続いてワークフローは以下のように実装しました。

name: Notify Discord on PR Comment

on:
  issue_comment:
    types: [created]
  pull_request_review_comment:
    types: [created]  

env:
  WEBHOOK_URL: ${{ secrets.WEBHOOK_URL }}

jobs:
  notify_discord:
    runs-on: ubuntu-latest
    timeout-minutes: 5
    steps:
      - name: Check out the repository
        uses: actions/checkout@v4

      - name: Set up jq
        run: sudo apt-get install jq
        
      - name: Get Discord IDs
        env:
          COMMENT_BODY: ${{ github.event.comment.body }}
        run: |
          # コメントからGitHubアカウント名を抽出(例: @username)
          GITHUB_USERNAMES=$(echo "$COMMENT_BODY" | grep -oP '@[\w.-]+' || echo "")
          
          # メンションを取得できるか調べる.できなかったら処理終了
          if [ -z "$GITHUB_USERNAMES" ]; then
            echo "No mentions found in the comment."
            exit 0
          fi

          # @ごとに取得したGITHUB_USERNAMESをもとに,各DISCORD_USER_IDを取得し,1行の空白区切りに整える
          DISCORD_USER_IDS=$(echo "$GITHUB_USERNAMES" | while read -r GITHUB_USERNAME; do
          jq -r ".\"$GITHUB_USERNAME\" // \"\"" < ./username.json
          done | tr '\n' ' ')
          echo "DISCORD_USER_IDS=$DISCORD_USER_IDS" >> $GITHUB_ENV
          
      - name: Send notification to Discord
        env : 
          DISCORD_USER_IDS : ${{ env.DISCORD_USER_IDS }}
          PR_URL: ${{ github.event.pull_request.html_url || github.event.issue.pull_request.html_url }}
        run : |
          # Discordで紐付けされたアカウントのメンションが見つかった場合のみ通知を送信
          if [ -n "$DISCORD_USER_IDS" ]; then
          
            #空白区切りの文字列を<@id>という形になるように整形
            MENTION_STRING=$(echo "$DISCORD_USER_IDS" | xargs | sed 's/ /> <@/g')
            MENTION_STRING="<@$MENTION_STRING>"

            echo "Mention found, sending notification to <@$DISCORD_USER_ID>..."
            curl -H "Content-Type: application/json" \
                 -X POST \
                 -d "$(jq -n \
                       --arg content "$MENTION_STRING コメントが来たよ~[View PR]($PR_URL)" \
                       '{ "content": $content }')" \
                 "$WEBHOOK_URL"
          else
            echo "No valid GitHub username found, skipping notification."
          fi

 以下は上記の実装について解説していきます!

イベントトリガーの設定

 イベントトリガーは書いたワークフローをどのようなタイミングで実行するかを設定するもので、YMLファイルのトップレベルにon:と書き、その中のネストでイベントの種類を書いていきます。イベントトリガーはいろんなものがありますが、今回は「プルリクにコメントがついたとき」にワークフローを実行したいので、以下のように書きました。

on:
  #プルリクにコメントがついた時
  issue_comment:
    types: [created]
  #差分にレビューコメントがついた時
  pull_request_review_comment:
    types: [created]  

 issue_commentはイシューまたはプルリクのコメントが作成、編集、または削除された時に発動するトリガーです。issueとついていますが、プルリクのコメントもこちらのトリガーで発動するようになっています。また、今回はコメント作成時にのみ通知がくればよいので、さらに下のネストでtypes: [created]と書いています。ただ、issue_commentはプルリクの差分に紐づいたレビューコメントはトリガーの対象外なので、別途pull_request_review_commenttypes: [created]を追加しています!(ややこしいですね・・・笑)

secretsの取得

 続いて先程設定したWebHookを取得します!

env:
  # secretsを取得し、環境変数として設定(secrets.<secretsName>)
  WEBHOOK_URL: ${{ secrets.WEBHOOK_URL }}

 私は今回WEBHOOK_URLとしてsecretを作成したのでこのように書いていますが、取得したいsecret名をsecret.の後に続けることで取得することができます。これによって後の処理の中でWEBHOOK_URLという変数名でsecretsを使用することができます。

jobsの設定

 jobsはワークフロー内で実行する一連の処理を記述する箇所です。今回はDiscordに通知を送る処理を書くので、notify_discordというjob_idを設定しています。

jobs:
  notify_discord: 
    runs-on: ubuntu-latest
    timeout-minutes: 5

 ネスト内のruns-onはジョブを実行するOSを指定するもので今回はubuntuの最新版を指定しています。他にもwindows-latestmacos-latestなどがありますが、ubuntu-latestが一番安いと先輩に教えていただいたのでこちらを使用しています(今更ですが、PrivateリポジトリでGitHub Actionsを使いまくるとお金がかかります・・・😢。今回のDiscordに通知を送る程度の処理であれば無料枠に収まるので問題ないのですが、メンバー同士で熱烈なメンション合戦が今後出てくるかもしれないので最安のubuntu-latestを使用しました笑)
 timeout-minutesは、読んで字のごとくワークフローのタイムアウト時間を設定します。こちらは、何らかの原因で処理が止まってしまった際に時間を消費しすぎないように設定しています。

stepsの設定

 jobのネスト内にあるstepsは、job内で実行したい一つ一つの処理を書いていきます!

    steps:
      - name: Check out the repository
        uses: actions/checkout@v4

      - name: Set up jq
        run: sudo apt-get install jq
        
      - name: Get Discord IDs
        env:
          COMMENT_BODY: ${{ github.event.comment.body }}
        run: |
          # コメントからGitHubアカウント名を抽出(例: @username)
          GITHUB_USERNAMES=$(echo "$COMMENT_BODY" | grep -oP '@[\w.-]+' || echo "")
          
          # メンションがあるかどうか調べる.できなかったら処理終了
          if [ -z "$GITHUB_USERNAMES" ]; then
            echo "No mentions found in the comment."
            exit 0
          fi

          # @ごとに取得したGITHUB_USERNAMESをもとに,各DISCORD_USER_IDを取得し,1行の空白区切りに整える
          DISCORD_USER_IDS=$(echo "$GITHUB_USERNAMES" | while read -r GITHUB_USERNAME; do
          jq -r ".\"$GITHUB_USERNAME\" // \"\"" < ./username.json
          done | tr '\n' ' ')
          echo "DISCORD_USER_IDS=$DISCORD_USER_IDS" >> $GITHUB_ENV
          
      - name: Send notification to Discord
        env : 
          DISCORD_USER_IDS : ${{ env.DISCORD_USER_IDS }}
          PR_URL: ${{ github.event.pull_request.html_url || github.event.issue.pull_request.html_url }}
        run : |
          # Discordで紐付けされたアカウントのメンションが見つかった場合のみ通知を送信
          if [ -n "$DISCORD_USER_IDS" ]; then
          
            #空白区切りの文字列を<@id>という形になるように整形
            MENTION_STRING=$(echo "$DISCORD_USER_IDS" | xargs | sed 's/ /> <@/g')
            MENTION_STRING="<@$MENTION_STRING>"

            echo "Mention found, sending notification to <@$DISCORD_USER_ID>..."
            curl -H "Content-Type: application/json" \
                 -X POST \
                 -d "$(jq -n \
                       --arg content "$MENTION_STRING コメントが来たよ~[View PR]($PR_URL)" \
                       '{ "content": $content }')" \
                 "$WEBHOOK_URL"
          else
            echo "No valid GitHub username found, skipping notification."
          fi

 それぞれの処理について簡単に、

  • Check out the repository
     リポジトリへのチェックアウトを行う処理
  • Set up jq
     JSONファイルを読み込むのに必要なライブラリのインストール
  • Get Discord IDs
     コメントからメンションの有無を判定。メンションがあればDiscordのidが入っているusername.jsonファイルからidを取得し、環境変数として入力。
  • Send notification to Discord
     環境変数として設定したディスコードのidを取得し、メンション付きのコメントとしてDiscordに送る。 といった処理となっています!

 あとはこのコードをmainにプッシュすれば、このようにメンション付きの通知をおくることができます!
 ここでも余談ですが、今回bashでスクリプトを書きましたが、pythonやnode.jsでスクリプトを書くことができるようです。慣れないbashにめちゃくちゃ苦労したのに、先に教えてよ、ChatGPT・・・😢

まとめ

 今回はGithub ActionsとDiscordのWebHooksを使って、メンション付きの通知を送るワークフローを実装しました。前回書いたeslint plugin importの記事でもそうでしたが、かなりChatGPTのお世話になりました(笑)。ただ、すべてがChatGPTの指示通りとも行かず、その都度公式ドキュメントを見たり、先輩に聞いたりしながらなんとか期待通りのものを作り上げることができました。この通知機能の力で、怠惰な自分が少しでもメンバーのレビュー指摘に迅速に対応できるようになれればよいなぁと思います(サボれなくなったのは痛いですが・・・笑)

 最後になりましたが、ここまで読んでくださってありがとうございます!次回以降のアドベントカレンダーも面白い記事がたくさん出てきますのでぜひぜひ見てください!

私は最終日(クリスマス)にまた投稿します🎅

 

KENTEMでは、様々な拠点でエンジニアを大募集しています!
建設×ITにご興味頂いた方は、是非下記のリンクからご応募ください。
recruit.kentem.jp career.kentem.jp