7
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2019-10-26

PullRequestの通知をSlack Appで連携している方は多いでしょう
特定のチャンネルに通知していますが、エンジニアが増えて以下のような問題を抱えている人がいました

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

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

(最近は Pull Panda などのSlack Appも出てきておりますので必要に応じて使い分けるのが良いと思います)

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

GitHub APIを叩くための準備

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

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

New_personal_access_token.png

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

Slackに通知するための準備

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

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メソッド」を叩くと通知が行くと思います

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

今回はあえてGraphQLを利用して情報取得を行います
GraphQL API v4 APIからどの情報を取得するかはGitHubが提供している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にひとまず情報が吐き出されていれば問題ありません

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

いよいよ本丸です
GraphQL取得した情報をフィルタ・整形して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: GraphQLで取得した情報を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

定期実行させる

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

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

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

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

まとめ

これでキレイに通知がされるはずです
Slack_-_Fukuroulabo-2.png

※ 伏せ字だらけで分かりづらいですがユーザ名やリポジトリ名、プルリクタイトルが出てきます

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

7
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?