Y.Graceの記録

主に水槽日記、ときどきエンジニア生活の話。のんびり更新。

Slack Events API + GASで簡易的にGitHubのアクティビティを見てみる

この記事は Ubiregi Advent Calendar 2020 21日目のエントリです。

本日は、GitHub上のアクティビティを監視・集計する方法としてこんなやり方もできる、という小ネタをご紹介。

Pull Request(以下、PR)やIssueの状況をウォッチするために、GitHubのSlack Appを連携させて使っている人も多いだろう。
対象のリポジトリや項目をスラッシュコマンドで設定すれば、それぞれのアクティビティがSlack上に流れてくるというもの。

これはつまり、GitHub上のアクティビティをSlackのチャンネル投稿イベントとしてSlack Events APIで扱うことができるということでもある。
GitHub連携をしているSlackチャンネルの投稿を監視するだけで、マージ待ちのPRをリスト化したり、PRのopenからcloseまでの時間を計測したりといったことが簡易的ながら可能になる。

そんなことしなくても普通にGitHub webhooks使えばいいじゃん、と言われればまったくその通り。
そこが小ネタたる所以である。
とはいえ、Slack Events APIをすでに使い慣れている場合はもちろんのこと、

  • チャンネル投稿の読み取り権限(channels:history)さえ設定すれば、SlackのGitHub連携に流しているイベント全てが取得できる
  • リポジトリのAdmin権限が不要

といった特徴があるので、こちらの方が使いやすい場面ももしかしたらあるかもしれない。

ここでは、GASでSlack上のGitHub通知の内容を取得できるようにするまでの流れと、取得した情報のどこに何が入っているかまでを紹介する。

GASプロジェクトの準備

Slack Appの実装にここではGoogle Apps Script(以下、GAS)を使用する。

GASを選択するメリットとして次のようなものが挙げられる。

  • Googleアカウントがあればすぐに使い始められる
  • 実行回数が無制限
    • 1回あたりの実行時間には6分までという制限があるが、Slackの投稿イベントで飛んでくるjsonはとても小さいので、よほど複雑で重たい集計をしようとしない限りは収まるはず
  • スプレッドシートなど他のGoogleサービスとの連携が容易
    • 集計結果をそのまま共有可能な形にしたり、スプレッドシートの数式も組み合わせて集計自体を楽にすることもできる

※V8ランタイム登場前に書いた少し古いコードであること、元々jsが専門外であることなどから、洗練されたコードにはなっていない点は何卒ご容赦いただきたい。

プロジェクトの作成

GASのホームから「新しいプロジェクト」を押して新規のGASプロジェクトを作成する。
スプレッドシートと組み合わせて使うことが決まっていれば、まずスプレッドシートを作成して ツール>スクリプトエディタ と進んでシートに紐づいたプロジェクトとして作成してもいい。

URL確認イベントへの対応

このプロジェクトでSlackからのイベント通知を扱っていくわけだが、イベント通知の送信先URLを設定すると同時にURLの確認イベントが飛んでくる。
中身を実装する前に、まずその対応を先に入れておくと設定がスムーズである。

GASでは doPost() という名前の関数を実装しておくと、WebアプリとしてデプロイしたときのPOSTリクエストの受け口になる。
こんな感じで、url_verificationイベントが飛んできたらその中に含まれるchallengeというフィールドの値をそのまま返すようにする。

function doPost(e) {
  if (e.postData == "FileUpload") {
    var contents = JSON.parse(e.postData.contents);
    var type = contents.type;

    //URL確認
    if (type == "url_verification") {
      return ContentService.createTextOutput(JSON.stringify(contents.challenge)).setMimeType(ContentService.MimeType.TEXT);
    }
  }
}

ここまででいったん、右上の デプロイ>新しいデプロイ から、現在の状態をWebアプリとしてデプロイする。
ここで「アクセスできるユーザー」は「全員」になっている必要がある。

一度デプロイすると、 デプロイ>デプロイを管理 からURLが取得できるようになる。

Slack Appの準備

Slack Events APIのイベントを受け取るにはSlack Appを作成することになる。
ここでは今回やること向けに特化したポイントに絞って説明していくので、Slack App自体のセットアップ方法などについては別途ドキュメントを参照されたい。

Enabling interactions with bots | Slack

ワークスペースで利用するボットの作成 | Slack

スコープの設定

作成したSlack AppのOAuth & Permissionsのメニューからスコープを設定する。

投稿イベントの取得自体は、Bot Tokenのチャンネル投稿の読み取り権限 channels:history さえあれば動作する。
集計結果を投稿させたい場合などは書き込み権限 chat:write も必要になる。

User Tokenはここでは使用しない。

f:id:y_grace:20201221230849p:plain

イベントへの登録

Event Subscriptionsのメニューから Enable Events のスイッチをOnにし、受け取るイベントを設定する。

前述の通りチャンネル投稿を見たいだけなので、Bot User Eventsの message.channels を選べばOK。

channels:history スコープが必要なことはここで言われるので、こちらを先に設定しても良い。

f:id:y_grace:20201221230909p:plain

イベントの通知先URLの設定

同じくEvent Subscriptionsのメニューで、イベントの通知先URLを設定する。
Request URL の欄に先ほど作ったGASプロジェクトのURLを貼り付ける。

先ほど作っておいたURL確認の実装が正しければ、すぐに確認が完了し Verified というラベルと緑色のチェックマークがつく。

f:id:y_grace:20201221230950p:plain

これで、Slackからチャンネル投稿を受け取る準備ができた。

GitHub通知の投稿内容の取得

message.channels イベントで飛んでくるjsonの中身はこのようになっている。
ここから event 要素を取り出して見れば良いのだが、全てのチャンネルに投稿があるたびにこの関数が呼び出されることになるので、まず扱うケースを適切に絞り込んでいく必要がある。
チャンネルIDが監視したいチャンネルでない・発言者がGitHub Appではない投稿は全て除外してしまおう。

function doPost(e) {
  if (e.postData == "FileUpload") {
    var contents = JSON.parse(e.postData.contents);
    var type = contents.type;

    //URL確認
    if (type == "url_verification") {
      return ContentService.createTextOutput(JSON.stringify(contents.challenge)).setMimeType(ContentService.MimeType.TEXT);
    }
    else if (type == "event_callback") {
      var event = contents.event;
      var event_type = event.type;
      
      if (event_type == "message") {
        var user = event.user; //投稿したユーザーID
        var channel = event.channel; //投稿があったチャンネルID
        if(channel == <監視したいチャンネルID> && user == <GitHub AppのユーザーID>){
          //投稿内容を取得
          var attachment = event.attachments[0];
        }
        //それ以外は何もしない
        return null;
      }
    }
  }
}

GitHub Appの投稿から情報を取得する際にわかりにくいのが、内容が全て attachments 扱いであるという点。
attachments は配列として返されるので、内容にアクセスする前にまず配列として取得してから1つ目の要素を取り出す必要がある。

Pull request opened by hogehoge のメッセージなどいかにも本文、 text であるように見えるのに、実際は attachments の中の pretext 要素である。

attachments 要素のフィールド一覧は公式ドキュメントの以下のセクションで見ることができる。
Creating rich message layouts | Slack

GitHubの通知の項目はそれぞれ以下のフィールドに格納されている。

項目 attachments内のフィールド名
本文(Pull request opened by hogehoge など) pretext
PRやIssueのauthor名 author_name
PRやIssueのタイトル title
PRやIssueへのリンク title_link
PRやIssueの本文 text
リポジトリ footer
イベントが作成された日時のタイムスタンプ ts

さらに、タイトルについては #id タイトル の形式にまとめられているので、少し乱暴だが

  var title = attachment.title;
  var id = title.match(/[0-9\s+]/g).join('');
  id = id.split(' ')[0];

こんなことをやると、PRやIssueのIDだけを取り出すことも可能になる。

ステータスについては pretextopenedmergedclosed などの単語が含まれているかどうか、で簡易的に判別が可能である。

全てのイベントは日時も一緒にわかっているので(タイムスタンプからの変換は必要だが)、これらの項目を使った集計となるとけっこういろんなことができる。
集計以外にも、例えばPRがマージされたらデプロイ手順を投稿する、といった使い方もできる。

簡易的に、を繰り返しているのはこれらの仕様は変わる可能性があるためである。
Slack向けGitHub Appのソースコードは以下で公開されているので、「なんか挙動変わったかな」と思ったら覗いてみると良いかもしれない。
(この記事は過去の実装経験に加え現時点でのGitHub Appのコードもざっと確認した上で書いている。が、もし見落としがあればご容赦願いたい)
https://github.com/integrations/slack

応用例についてはまた機会があれば触れる……かもしれない。