Slack × GAS × GitHub APIで見逃さないPullRequest

Pocket
LINEで送る

どうも、最近Redash v7で選択クエリ実行ができるようになってテンションが上っている
菊池です

PullRequestの通知をSlack Appで連携している方は多いでしょう
フクロウラボでも同様に通知していますが、エンジニアが増えて以下のような問題を抱えている人がいました

  • 通知が多く自分に対しての情報を見逃す
  • 通知が多くてちょっとうざったい

結局カスタマイズして通知しないと解決できないけど、「サーバ立てるとかめんどい…」
そんな方向けにGASとGitHubAPIを組み合わせてうまくSlackへ通知するカッコいい方法を紹介します

これでPullRequestに対してのやり取りが効率化されて余計なストレスから解消されることでしょう!

  1. GitHub APIを叩くための準備
  2. Slackに通知するための準備
  3. GASからSlackへ通知してみる
  4. GASからGitHub APIの結果を受け取る
  5. GitHub APIの情報をSlackにフィルタして通知する
  6. 定期実行させる
  7. まとめ

 

1. GitHub APIを叩くための準備

https://github.com/settings/profile から [Developer settings] – [Personal access tokens] へ遷移し
Generate new token で新しいTokenを発行します

※ 念の為、Repositoryの閲覧権限とUserの読み込み権限だけつけるようにしましょう

New_personal_access_token

発行が終わったらToken文字列を控えておきます

 

2. Slackに通知するための準備

https://slack.com/apps/A0F7XDUAZ–incoming-webhook- から
投稿用のチャンネル指定と投稿用のエンドポイントを取得します

3. GASからSlackへ通知してみる

早速、slack通知をGAS(Google App Script)から行ってみましょう

function slackWebhookUrl() {
  return PropertiesService.getScriptProperties().getProperty("SLACK_WEBHOOK_URL");
}

function send_message(message, channel, attachments) {
  var url = slackWebhookUrl();
  var payload = {
    channel: channel,
    text: message,
    username: "GitHuby",
    icon_emoji: ":zap:",
    attachments: attachments,
  }
  var option = {
    'method': 'post',
    'payload': JSON.stringify(payload),
    'contentType': 'application/x-www-form-urlencoded; charset=utf-8',
    'muteHttpExceptions': true
  };
  var response = UrlFetchApp.fetch(url, option);
  Logger.log(response);
}

function test() {
  send_message('Testだよん', '{channel idとか}');
}

※ 2.で取得したURLはスクリプトプロパティに設定しておきます(コードを公開しないならべた書きでも問題ない)

GAS上からtestメソッドを叩くと通知が行くと思います

4. GASからGitHub APIの結果を受け取る

今回はあえてGraphQL API v4 APIからどの情報を取得するかはGitHubが提供しているExplorerを利用すると理解しやすいです
https://developer.github.com/v4/explorer/

function githubAccessToken() {
  return PropertiesService.getScriptProperties().getProperty("GITHUB_ACCESS_TOKEN");
}

function fetch_pullreq_data_by_graphql(owner, repository) {
  const graphql_query = 
    '{\
  repository(owner: "' + owner + '", name: "' + repository + '") {\
    name\
    pullRequests(last: 20, states: OPEN) {\
      nodes {\
        title\
        url\
        author {\
          login\
        }\
        reviewRequests(last: 20) {\
          nodes {\
            requestedReviewer {\
              ... on User {\
                login\
              }\
            }\
          }\
        }\
        comments(last: 50) {\
          nodes {\
            author {\
              login\
              avatarUrl(size: 20)\
            }\
            body\
            createdAt\
            updatedAt\
            url\
          }\
        }\
        reviews(last: 50) {\
          nodes {\
            author {\
              login\
            }\
            url\
            comments(last: 50) {\
              nodes {\
                author {\
                  login\
                  avatarUrl(size: 20)\
                }\
                replyTo {\
                  author {\
                    login\
                  }\
                }\
                body\
                createdAt\
                updatedAt\
                url\
              }\
            }\
          }\
        }\
      }\
    }\
  }\
}';

  const option = buildRequestOption(graphql_query);
  return UrlFetchApp.fetch("https://api.github.com/graphql", option);
}

function buildRequestOption(graphql) {
  return {
    method: "post",
    contentType: "application/json",
    headers: {
      Authorization: "bearer " + githubAccessToken(),
    },
    payload: JSON.stringify({ query: graphql }),
  };
}

function test() {
  Logger.log(fetch_pullreq_data_by_graphql('{ユーザ名}', '{対象リポジトリ名}'));
}

…カオスですね。。。とりあえずgraphqlなのでしょうがないと割り切りましょう
Loggerにひとまず情報が吐き出されていれば問題ありません

5. GitHub APIの情報をSlackにフィルタして通知する

いよいよ本丸です
4.で取得した情報をフィルタ・整形してSlackに通知します

function notice_relation_comments() {
  if (isHoliday())
    return;

  // ref. https://api.slack.com/methods/channels.list/test
  var target_users = [
    {name: "hogehoge", channel: "{channel_id}"}
  ];

  // 現在日時を取得してメッセージ取得する時間幅を計算する
  var now = new Date();
  var logtime = now.setMinutes(now.getMinutes() - 5);
    
  target_users.forEach(function(target_user) {
    all_repositories().forEach(function(repository_name){
      var json = fetch_repostiroy_json(repository_name);
      var repository = json.data.repository;

      var attachments = [];
      repository.pullRequests.nodes.forEach(function(pullRequest) {
        Array.prototype.push.apply(attachments, build_relation_comments_attachment(repository, pullRequest, logtime, target_user.name));
      });
      if (!attachments.length)
        return;
    
      send_message("", target_user.channel, attachments);
    });
  });
}

function fetch_repostiroy_json(repo) {
  var response = fetch_pullreq_data_by_graphql(repo);
  var json = response.getContentText();
  return JSON.parse(json);
}

function build_relation_comments_attachment(repository, pullRequest, logtime, user_name) {
    result = []
    pullRequest.comments.nodes.forEach(function(comment) {
      if (!is_delivery_target(pullRequest, user_name, comment, logtime))
        return;
      result.push(create_attachment(repository, pullRequest, comment));
    });
  
    pullRequest.reviews.nodes.forEach(function(review) {
      review.comments.nodes.forEach(function(comment) {
        if (!is_delivery_target(pullRequest, user_name, comment, logtime))
          return;
        result.push(create_attachment(repository, pullRequest, comment));
      });
    });
    return result;
}
    
function create_attachment(repository, pullRequest, comment) {
  return {
    "color": "#a8bdff",          
    "author_name": comment.author.login,
    "author_icon": comment.author.avatarUrl,
    "title": "comment link",
    "title_link": comment.url,
    "text": comment.body,
    "footer": "[" + repository.name + "] " + pullRequest.title,
    "footer_icon": "https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png",
    "footer_link": pullRequest.url,
    "mrkdwn_in": ["text", "footer"]
  }
}
    
function is_delivery_target(pullRequest, user_name, comment, logtime) {
  // コメントの時間をフィルタして通知する
  if (new Date(comment.createdAt) < logtime)
    return false;

  // コメント記入者なら通知しない
  if (comment.author.login === user_name)
    return false;

  // コメントのリプライ先なら通知する
  if (comment.replyTo && comment.replyTo.author.login === user_name)
    return true;
  
  // プルリクの作成者なら通知
  if (pullRequest.author.login === user_name)
    return true;
    
  // 本文に自分宛てのメンションがあるなら通知
  if (comment.body.indexOf(user_name) !== -1)
    return true;
    
  return false;
}

いよいよ手に負えないレベルで煩雑になってきましたね…
ざっくり各メソッドの役割を明記しておきます

notice_relation_comments: メイン処理系
fetch_repostiroy_json: 4.で取得した情報をJSONにパースします
build_relation_comments_attachment: フィルタ処理から整形処理をになう処理系
is_delivery_target: フィルタ処理、自分に関連あるとされる情報のみを選定する
create_attachment: slackにかっこよく出すためにGitHub APIから取得した情報をアタッチメントに整形します

姑息にも複数ユーザ×複数リポジトリに対応しています
isHoliday() / all_repositories()は未実装ですが、
「休日は通知しないように判定」 と 「通知対象のリポジトリ配列を返す」と読み替えてもらえればいいと思います

※ isHolidayの詳細はリポジトリを見てもらえるといいです https://github.com/kichion/gas-github-notifier/blob/master/holiday.gs

6. 定期実行させる

コードはできましたがこれを手動で叩くだけならかっこよくないですよね?
GASはトリガーを設定して特定のスクリプトを定期実行させることが可能です
ref. [AD]【Google Apps Script(GAS)】トリガーを設定してスクリプトを実行する

今回はコードに意味深なマジックナンバーを忍ばしてるので、5分おきに実行させてみましょう

  // (中略)
  // 現在日時を取得してメッセージ取得する時間幅を計算する
  var now = new Date();
  var logtime = now.setMinutes(now.getMinutes() - 5);

1分おきに実行させたいなら…言うまでもありませんね

7. まとめ

これでキレイに通知がされるはずです
Slack_-_Fukuroulabo-2
※ 伏せ字だらけで分かりづらいですがユーザ名やリポジトリ名、プルリクタイトルが出てきます

コード上のフィルタ実装を変えるだけでお好みの通知が実現できるのでぜひやってみてください!!
よいプルリクライフを!!

Pocket
LINEで送る