⚙️

Draft Pull RequestがOpenされたタイミングでSlackに通知する

2021/05/23に公開

背景

GithubのPull RequestのアクティビティをSlackの通知する為の手段としてSlack向けのGithub Appという物があります。
このSlack向けのGithub Appは、今までの物については2021/7/15に無効化される為、新しいGithub Appにアップグレードが必要なようです
僕が働いている職場では、先んじて新しいGithub Appにアップデートしたのですが、今まで来ていたDraft PRがOpenした際のSlackへの通知が来なくなってしまいました。

僕が所属するチームではDraft PRを比較的多用している為、この変更は他のメンバーの作業が見えなくなってしまういという点で個人的に非常に困っていました。

しばらく経ったら直るかなと思っていたのですが、今の所その気配もないのでGithub Actionsを使って、Slackに通知が来るようにしてみました。

どうやってやるか

Github Actionsで行うので、通知をしたいリポジトリ上に以下のようなファイルを置いて動かしてみました。

name: Draft PR Notification
on:
  pull_request:
    types: [opened]
jobs:
  build:
    runs-on: ubuntu-latest
    if: ${{ github.event.pull_request.draft == true }}
    steps:
    - name: Use Node.js 16.x
      uses: actions/setup-node@v1
      with:
        node-version: 16.x
    - name: Install Markdown to mrkdwn tool
      run: npm install slackify-markdown
    - name: Notify slack pr open
      env:
        SLACK_WEBHOOK_URL : ${{ secrets.SLACK_WEBHOOK_URL }}
        PULL_REQUEST_AUTHOR_NAME : ${{ github.event.pull_request.user.login }}
        PULL_REQUEST_NUMBER : ${{ github.event.pull_request.number }}
        PULL_REQUEST_TITLE : ${{ github.event.pull_request.title }}
        PULL_REQUEST_AUTHOR_ICON_URL : ${{ github.event.pull_request.user.avatar_url }}
        PULL_REQUEST_URL : ${{ github.event.pull_request.html_url }}
        PULL_REQUEST_BODY : ${{ github.event.pull_request.body }}
      run: |
          MRKDWN_BODY=`node -e "const slackifyMarkdown = require('slackify-markdown'); console.log(slackifyMarkdown(process.env.PULL_REQUEST_BODY))"`
          if [ -z "$MRKDWN_BODY" ]; then
            MRKDWN_BODY=" "
          fi
          PAYLOAD="{
              \"blocks\": [
                {
                  \"type\": \"context\",
                  \"elements\": [
                      {
                        \"type\": \"image\",
                        \"image_url\": \"$PULL_REQUEST_AUTHOR_ICON_URL\",
                        \"alt_text\": \"icon\"
                      },
                      {
                        \"type\": \"mrkdwn\",
                        \"text\": \"*$PULL_REQUEST_AUTHOR_NAME*\"
                      }
                    ]
                },
                {
                    \"type\": \"section\",
                    \"text\": {
                      \"type\": \"mrkdwn\",
                      \"text\": \"*<$PULL_REQUEST_URL|#$PULL_REQUEST_NUMBER $PULL_REQUEST_TITLE>*\"
                  }
                },
                {
                    \"type\": \"section\",
                    \"text\": {
                      \"type\": \"mrkdwn\",
                      \"text\": \"$MRKDWN_BODY\"
                  }
                }
              ]
          }"
          PAYLOAD="${PAYLOAD//$'\r'/' '}"
          STATUS=`curl -X POST -H 'Content-type: application/json' --data "$PAYLOAD" $SLACK_WEBHOOK_URL -o /dev/null -w '%{http_code}' -s`
          if [ "$STATUS" != "200" ]; then
            exit 1
          fi

Draft PRをOpenすると以下のような形でSlackに通知が来ます。

出来てないこと

前のGithub Appでは、PR側のタイトルやDescriptionが更新されると、それに伴いSlackへ通知された内容も更新をしてくれていました。
Github Actionsを使った上記の仕組みではこれは出来ていません。

パッと思いつく限りでは、pull_requestのeditedイベントをフックしてGithub Actionsを動かすようにした上で、Slack APIを使うようにすればこれは実装出来そうです。

ただ、どのPRがどのSlackの投稿をしたかを紐付けるすべがないので、スマートにやるなら独自のプログラムをGithub Actions外に置いてそこで処理する形になるのかなと思っています。

技術的な解説

自分もどのような仕組みで動いているのか分からなくなりそうなので、記憶が新鮮なうちに記録を残して置きたいと思います。

Github Actionsが動く条件

まず最初の部分ですが、ここはこのGithub Actionsが動く条件を決めています。

name: Draft PR Notification
on:
  pull_request:
    types: [opened]

これで pull_request が開かれたときにこのGithub Actionsが動き出します。
しかしこれだと、Draft PR以外のpull requestが開かれたときもこの処理が動いてしまいます。
そこで、以下の処理でDraft PRのときだけGithub Actionsの後続処理が動くようにしています。

jobs:
  build:
    runs-on: ubuntu-latest
    if: ${{ github.event.pull_request.draft == true }}
    steps:

if: ${{ github.event.pull_request.draft == true }} でDraft PRのときだけ、steps に定義された処理が動くようになっています。
なお、github.event.pull_request.draft についてはドキュメントを探したんですが、ここWebhook payload exampleに記載がある位なので、ソースがある訳ではないです。

slackify-markdownを入れる

steps以降の Use Node.js 16.xInstall Markdown to mrkdwn tool はその名前の通りで、Github上のMarkdownをSlackのmrkdwnに変換するための slackify-markdown を入れています。

    steps:
    - name: Use Node.js 16.x
      uses: actions/setup-node@v1
      with:
        node-version: 16.x
    - name: Install Markdown to mrkdwn tool
      run: npm install slackify-markdown

僕はこのツールを作っていて初めて知ったのですが、slackで使えるMarkdownのような物はmrkdwnという物です。
これは普通(普通とは何んだというのはあるのですが)のMarkdownとは毛色の違う物で、GithubのPRのDescriptionをそのままをSlackに流すと整形されないテキストとして流れてしまいます。

環境変数の設定

次の部分は実際にslackに実際通知するステップの部分です。

    - name: Notify slack pr open
      env:
        SLACK_WEBHOOK_URL : ${{ secrets.SLACK_WEBHOOK_URL }}
        PULL_REQUEST_AUTHOR_NAME : ${{ github.event.pull_request.user.login }}
        PULL_REQUEST_NUMBER : ${{ github.event.pull_request.number }}
        PULL_REQUEST_TITLE : ${{ github.event.pull_request.title }}
        PULL_REQUEST_AUTHOR_ICON_URL : ${{ github.event.pull_request.user.avatar_url }}
        PULL_REQUEST_URL : ${{ github.event.pull_request.html_url }}
        PULL_REQUEST_BODY : ${{ github.event.pull_request.body }}

上記の部分では各種環境変数にSlackへの通知に必要な内容をセットしています。
どのような物があるかはここ あたりから辿って見ていく形になります。

なお、SLACK_WEBHOOK_URLについては一点注意点があって、このやり方ではFork repositoryからのPRの場合、シークレットが取れないという問題があります。

https://github.blog/2020-08-03-github-actions-improvements-for-fork-and-pull-request-workflows/ private repositoryであれば Send secrets to workflows from fork pull requests. を有効に、public repositoryであれば pull_request_target を使うようにすれば回避できるかもなのですが、今回はめんどくさくてそれで出来るかまでは見ていません。

fork運用をやめるのが一番確実な気はしますが、最悪private repositoryであれば、secretsを使わないという手もあるかもしれません。

Paylaodの作成とSlackへの投稿

最後の部分はPRのDescriptionの内容をMarkdownからmrkdnwに変換した後、CRを取ってSlackのWebhookを使ってリクエストを投げています。

MRKDWN_BODYを入れている箇所では slackify-markdownのUsageに従って、nodeのワンライナーを使ってPRのDescriptionに対してMarkdown -> mrkdwnの変換をしています。

          MRKDWN_BODY=`node -e "const slackifyMarkdown = require('slackify-markdown'); console.log(slackifyMarkdown(process.env.PULL_REQUEST_BODY))"`
          if [ -z "$MRKDWN_BODY" ]; then
            MRKDWN_BODY=" "
          fi

process.env.PULL_REQUEST_BODY の部分がPRのDescriptionです。

ifの部分はPRのDescriptionが空の場合に、変換結果に空文字を入れるという処理をしています。
これは MRKDWN_BODY が後続のSlackへのPAYLOADを作る処理の際に何も定義されていない場合エラーになる為です。
(この処理を同僚にレビューして貰った際に、「Descriptionが空だったらPAYLOADを分ければいいのでは」という最もな指摘を受けたのですが、PAYLOAD自体がそこそこ複雑な構造になってしまっているので、それを2つに分けるのはテストも出来ないし辛いなと思いこのままにしています)

PAYLOADの部分はSlackのwebhookに渡して投稿する内容が定義されています。

          PAYLOAD="{
              \"blocks\": [
                {
                  \"type\": \"context\",
                  \"elements\": [
                      {
                        \"type\": \"image\",
                        \"image_url\": \"$PULL_REQUEST_AUTHOR_ICON_URL\",
                        \"alt_text\": \"icon\"
                      },
                      {
                        \"type\": \"mrkdwn\",
                        \"text\": \"*$PULL_REQUEST_AUTHOR_NAME*\"
                      }
                    ]
                },
                {
                    \"type\": \"section\",
                    \"text\": {
                      \"type\": \"mrkdwn\",
                      \"text\": \"*<$PULL_REQUEST_URL|#$PULL_REQUEST_NUMBER $PULL_REQUEST_TITLE>*\"
                  }
                },
                {
                    \"type\": \"section\",
                    \"text\": {
                      \"type\": \"mrkdwn\",
                      \"text\": \"$MRKDWN_BODY\"
                  }
                }
              ]
          }"
	  PAYLOAD="${PAYLOAD//$'\r'/' '}"

PAYLOADに指定出来る構文はここらへんを見ると分かると思います。

PAYLOAD="${PAYLOAD//$'\r'/' '}" はCRを空行に変換しています。
僕が確認した限りではGithubのDescriptionの改行コードはCRLF形式で入ってくるようで、CR(\r)がある状態でSlackにリクエストを送ると invalid_payload と言われてしまいます。
本来であれば MRKDWN_BODY だけ変換すれば事足りると思うのですが、他の要素でCRが入らないと断言が出来なかったのでPAYLOAD全体へかけるようにしています。

最後の部分は実際にSlackにリクエストを送りステータスコードが200でない場合はjobを失敗させるようにしています。

          STATUS=`curl -X POST -H 'Content-type: application/json' --data "$PAYLOAD" $SLACK_WEBHOOK_URL -o /dev/null -w '%{http_code}' -s`
          if [ "$STATUS" != "200" ]; then
            exit 1
          fi

今回はSlackのwebhookの仕組みを使ってSlackへメッセージを投稿しているのですが、これについては最近Slack appを作った上で、設定する方式に変わったので、注意が必要です。
curlに -w というオプションがあるのは今回始めてしりました。

Discussion