Stimulator

機械学習とか好きな技術話とかエンジニア的な話とかを書く

GASでGithubの自分関連のReviewer情報を定期的にSlackにPostする

- はじめに -

GithubのPull Requestを大体1日以内に処理するルールだったのだが、Repositoryが増えて全然管理できなくなったりしたので、Reviewerに入っていてApprovedしてないものだけSlackに通知しようとこねくり回したGoogle Apps Script。

Lambdaとかを使う方が楽だけど、GASは金が掛からないしポチポチで時間トリガーやSlack Bot化を進められるのが良さ。

GASでSlack botは前に書いたので、Slack投稿までは下記で出来ている前提。

vaaaaaanquish.hatenablog.com


 

- GithubのPersonal Access Tokenの取得 -

Githubをブラウザから見て、SettingsからAccessTokenを取得する。

右上のアイコンからAccount -> Settings

f:id:vaaaaaanquish:20171007161923p:plain:w300:h200

上記画像の赤枠をポチポチすると取得できるので、文字列を控えておく。


 

- GAS -

メインとなるGithub周りのスクリプトを導入する前に日付のフォーマット関連の処理をよしなに処理出来るようにしておく(GASではdatetimeの文字列処理が面倒なため)。

日付周りの処理

新しいスクリプトを追加してそれぞれファイル分けできるのでやる
f:id:vaaaaaanquish:20171007183856p:plain:w350:h220


新しく作ったスクリプトファイルにisodate関数を作っておく。
isodateとして以下コードを引用。
iso8601.js/iso8601.js at master · shumpei/iso8601.js · GitHub

/*
 * JavaScript library for ISO-8601 datetime format.
 * Copyright: 2009, Shumpei Shiraishi (shumpei.shiraishi at gmail.com)
 * License: GNU General Public License, Free Software Foundation
 *          <http://creativecommons.org/licenses/GPL/2.0/>
 * Original code and license is:
 *   Web Forms 2.0 Cross-browser Implementation <http://code.google.com/p/webforms2/>
 *   Copyright: 2007, Weston Ruter <http://weston.ruter.net/>
 *   License: GNU General Public License, Free Software Foundation
 *          <http://creativecommons.org/licenses/GPL/2.0/>
 */
var isodate=function(){function e(e,t){t||(t=2);for(var r=e.toString();r.length<t;)r="0"+r;return r}var t=/^(?:(\d\d\d\d)-(W(0[1-9]|[1-4]\d|5[0-2])|(0\d|1[0-2])(-(0\d|[1-2]\d|3[0-1])(T(0\d|1\d|2[0-4]):([0-5]\d)(:([0-5]\d)(\.(\d+))?)?(Z)?)?)?)|(0\d|1\d|2[0-4]):([0-5]\d)(:([0-5]\d)(\.(\d+))?)?)$/;return{validate:function(e,r){var n=!1,a=t.exec(e);if(!a||!r)return a;if(r=r.toLowerCase(),"week"==r)n=0===a[2].toString().indexOf("W");else if("time"==r)n=!!a[15];else if("month"==r)n=!a[5];else if(a[6]){var s=new Date(a[1],a[4]-1,a[6]);if(s.getMonth()!=a[4]-1)n=!1;else switch(r){case"date":n=a[4]&&!a[7];break;case"datetime":n=!!a[14];break;case"datetime-local":n=a[7]&&!a[14]}}return n?a:null},parse:function(e,t){if(!e)return null;var r=this.validate(e,t);if(!r)return null;var n=new Date(0),a=8;if(r[15]){if(t&&"time"!=t)return null;a=15}else{if(n.setUTCFullYear(r[1]),r[3])return t&&"week"!=t?null:(n.setUTCDate(n.getUTCDate()-(7-n.getUTCDay())+7*(r[3]-1)),n);n.setUTCMonth(r[4]-1),r[6]&&n.setUTCDate(r[6])}return r[a+0]&&n.setUTCHours(r[a+0]),r[a+1]&&n.setUTCMinutes(r[a+1]),r[a+2]&&n.setUTCSeconds(r[a+3]),r[a+4]&&n.setUTCMilliseconds(Math.round(1e3*Number(r[a+4]))),r[4]&&r[a+0]&&!r[a+6]&&n.setUTCMinutes(n.getUTCMinutes()+n.getTimezoneOffset()),n},format:function(t,r){if(!t)return null;r=String(r).toLowerCase();var n="";switch(t.getUTCMilliseconds()&&(n="."+e(t.getUTCMilliseconds(),3).replace(/0+$/,"")),r){case"date":return t.getUTCFullYear()+"-"+e(t.getUTCMonth()+1)+"-"+e(t.getUTCDate());case"datetime-local":return t.getFullYear()+"-"+e(t.getMonth()+1)+"-"+e(t.getDate())+"T"+e(t.getHours())+":"+e(t.getMinutes())+":"+e(t.getSeconds())+n+"Z";case"month":return t.getUTCFullYear()+"-"+e(t.getUTCMonth()+1);case"week":var a=this.parse(t.getUTCFullYear()+"-W01");return t.getUTCFullYear()+"-W"+e(Math.floor((t.valueOf()-a.valueOf())/6048e5)+1);case"time":return e(t.getUTCHours())+":"+e(t.getUTCMinutes())+":"+e(t.getUTCSeconds())+n;case"datetime":default:return t.getUTCFullYear()+"-"+e(t.getUTCMonth()+1)+"-"+e(t.getUTCDate())+"T"+e(t.getUTCHours())+":"+e(t.getUTCMinutes())+":"+e(t.getUTCSeconds())+n+"Z"}}}}();

 

Githubからの情報取得

以下メインのスクリプト
userにReviewerかどうか知りたいユーザ(つまり自分)、TeamはそのRepository製作者やチーム名、githubAccessTokenに上記で取得したアクセストークンを入れる。
pullsNameListにはチェックしたいRepositoryの名前をリストで入れておく感じ。

// GithubのプルリクをチェックしてApprovedしてないやつを確認する
// 各設定
var user = "testersp";
var team = "vaaaaanquish";
var githubAccessToken = "hogehoge";
var baseUrl = "https://api.github.com/repos/" + team + "/{}/pulls";
var pullsNameList = ["pull-request-test"];

// pull requestsのURLを生成
var pullsUrlList = [];
for (var i = 0; i < pullsNameList.length; i++) {
  pullsUrlList.push(baseUrl.replace("{}", pullsNameList[i]));
}

// Httpオプションとヘッダ
var httpOptions = {
    method: "GET",
};
var httpOptionsReviews = {
    method: "GET",
    headers: {"Accept":"application/vnd.github.black-cat-preview+json"}
};

// main
function github_main() {
    // 土日は実行しない
    var today = new Date();
    if (today.getDay() == 0 || today.getDay() == 6) return;
    // GithubAPIを走査しSlack用の文字列allContentsを生成する
    var allContents = '';
    for (var i = 0; i < pullsUrlList.length; i++) {
        allContents += doOneRepository(pullsUrlList[i]);
    }
    return allContents;
}

// 1つのリポジトリから情報習得
var doOneRepository = function(githubUrl) {
    // リポジトリの基本情報を習得(プルリク取得)
    var response = getResponse(githubUrl, httpOptions);
    if (response === null || response.length == 0)
        return "";

    // 1リポジトリ分の文字列を生成  
    var allContents = '';
    for (var i = 0; i < response.length; i++) {
        var r = response[i];
        var createdAt = isodate.parse(r["created_at"]);
        var info = {};
        info.prNumber = r["number"];
        info.prTitle = r["title"];
        info.owner = r["user"]["login"];
        info.hourCreate = r["created_at"];
        // requested_reviewersに入っているユーザ
        // (recviewersに入っていてCommentもApproveもしていない)
        info.notApprovedPeople = getNotApproved(githubUrl+"/"+r["number"]+"/requested_reviewers");
        // 文字列生成
        var content = buildSlackPostingString(info);
        if(content.length > 1){
            allContents += (content + "\n");
        }
    }
    // 文字列を整形して返す
    allContents.replace(/.+\n$/g,"")
    var repository = githubUrl.split("/")[5];
    if(allContents.length > 1){
      return repository + '\n' + allContents + '\n';
    }else{
      return "";
    }
}

// requested_reviewersに入っている人を返す
var getNotApproved = function (GithubUrlReviews) {
    var res = getResponse(GithubUrlReviews, httpOptionsReviews);
    var result = [];
    for (var i = 0; i<res.length; i++){
      result.push(res[i]["login"]);
    }
    Logger.log(result);
    return result;
}

// HTTP GETリクエストを投げて返却値をJSONとして得る
var getResponse = function (url, options) {
    var urlToken = url + "?access_token=" + githubAccessToken;
    var response = UrlFetchApp.fetch(urlToken, options);
    if (response.getResponseCode() != 200)
        return null;
    return JSON.parse(response.getContentText());
};

// Slackに投稿する文字列の生成
var buildSlackPostingString = function (info) {
  if(info.notApprovedPeople.indexOf(user) > -1 ){
      var content = '> •  #' + info.prNumber + ' ' + info.prTitle;
      content += ' - @' + info.owner;
      content += '\n>     > CREATED: *' + info.hourCreate.split("T")[0] + '*';
      return content;
  }else{
    return "";
  }
}


具体的にはRepositoryを走査して、requested_reviewersエッジの情報に自分が入っているかをチェックするというもの。

土日は仕事をすべきではないので通知しないようにしている。

requested_reviewers自体はまだPreviewなので注意。以下Reference。
Review Requests | GitHub Developer Guide


以下のような状態でのみ反応する。
f:id:vaaaaaanquish:20171007184928p:plain:w350:h170

CommentかApproveするとrequested_reviewersには入って来ない。

 
この結果を冒頭に書いた記事内にある、GASとSlackの連携を行ったスクリプトに導入する。
Slack botをGASでつくる方法で一番楽そうなやつ - Stimulator

function postSlack(text){
  var url = "https://hooks.slack.com/services/hogehogehoge~";
  var options = {
    "method" : "POST",
    "headers": {"Content-type": "application/json"},
    "payload" : '{"text":"' + text + '"}'
  };
  UrlFetchApp.fetch(url, options);
}

function test(){
  s = github_main();
  postSlack("未読プルリク情報\n" + s);
}

以下のようにSlackに通知される。
f:id:vaaaaaanquish:20171007185438p:plain

GASの設定で適当に1日数回、時間トリガーで発火するようにしておけば大体大丈夫。
もしくはSlackのbotにして応答させれば良い。


 

- おわりに -

かつてrequested_reviewersが動かなかった頃は、review_comments_urlからcommentをそれぞれ見に行ってcommentしているかチェックしたりしていたんですが、Reviewer機能が実装されたことでこの辺のチェックも本当に簡単になりました。
良かったです。


GAS便利だけどちょっと書くのがしんどいです。

GASエディタもStylishCSS書き換えで少し見やすくしているけど、それでもやっぱりローカル開発環境で十二分に色々やりたいという気持ちになってしまう。
Stylish - ウェブサイト用カスタムテーマ - Chrome ウェブストア
Google Apps Script Dark - Monokai | Userstyles.org


出来ることなら使わずに金を払って既存サービスに任せたい所です。

本末転倒ですが以上です。