Githubでmentionされた通知だけをLambdaでまとめてSlackに通知したい!

  • 20
    いいね
  • 3
    コメント

概要

Githubの自分宛てのNotificationをまとめてSlackに通知するLambda Functionを作った。

2017/03/19 追記

こちらのGithub通知ツール、新しく作り直しました! こちらを参照いただければと思います。
Githubでmentionされた通知だけをLambdaでまとめてSlackに通知したい! (改) - Qiita

作ったきっかけ

自分のPull RequestやIssueに対して誰かがコメントしてくれた時やメンションしてくれた時、気づくのが遅くなってしまうケースが多々あった :sweat: 自分へのコメントにはできるだけ早く返信したい!

GithubのWebhookから通知を拾ってやSlackアプリを使ってSlackに通知する方法があるが、その場合プロジェクトごとに設定することになってしまうし、それはどちらかというとチームのための通知という感じ。プロジェクトごとにではなく、自分宛ての通知はまとめて(複数プロジェクトに所属している場合もすべて)Slackの自分専用channelに通知したい。

ということで自分で作ってみることにした。

作成過程

構成

サーバレスでやりたいということで、AWS Lambda(Node.js)を使う。Slackへの通知はIncoming Webhooksを使う。Scheduled Eventで定期的にLambda Functionを実行する。

LambdaのRuntimeはつい最近(2016/04/07)サポートされたNode.js 4.3を使う。v4.3ならES6が使える!
AWS Lambda Supports Node.js 4.3

Github通知データの取得

Githubの通知はGithub API で定期的に取得することにした。
Notifications | GitHub Developer Guide

以下のようにパラメータにparticipating=trueをつければ、自分宛ての通知のみに限定できる。

curl -i -H "Authorization: token xxxxxxxxxxxx" "https://api.github.com/notifications?participating=true"

通知は一度画面上で見てしまえば次回は取得されないので、繰り返し行ってもずっと取得され続けることはない。

また、APIを使うためのOAuth TokenはLambdaのEvent Dataでパラメータとして渡すことにする(コードに直書きしたくないため)。

Slackへの通知

Slackへの通知はIncoming Webhooksを使う。
Webhook URLはEvent Dataでパラメータとして渡すことにする(同じくコードに直書きしたくないため)。

Lambda Functionの作成

Github

Github APIを使う部分は以下のような感じ。

'use strict';
const github = require('octonode');
const moment = require('moment');
const async = require('async');
const _ = require('underscore');

class Github{
  constructor(oauthToken){
    this.client = github.client(oauthToken);
  }

  fetchNotifications(callback){
    let params = {
      participating: true,
      since: moment().add(-3, 'day').format('YYYY-MM-DDTHH:mm:00Z')
    };
    this.client.me().notifications(params, (err, records)=>{
      if (err){ return callback(err); }
      let notifications = [];
      async.forEach(records, (record, cb)=>{
        let notification = {
          repositoryName: record.repository.name,
          id: record.id,
          updateAt: record.updated_at,
          issueTitle: record.subject.title,
          commentUrl: record.subject.latest_comment_url
        };
        this.fetchComment(record.subject.latest_comment_url, (err, comment)=>{
          if (err){ return callback(err); }
          notifications.push(_.extend(notification, { comment: comment }));
          notifications = _.sortBy(notifications, (n)=> { return n.id; } );
          cb();
        });
      }, ()=>{
        callback(null, notifications);
      });
    });
  }

  fetchComment(url, callback){
    this.client.get(url, {}, (err, status, data)=>{
      if (err){ return callback(err); }
      callback(null, {
        body: data.body,
        user: data.user,
        createdAt: data.created_at,
        htmlUrl: data.html_url
      });
    });
  }
}

module.exports = Github;

Github APIはpksunkara/octonodeを使う。participating通知を取得した後、その際のコメントも一緒に取得する。必要な情報だけ整理して格納する。
※async使っている部分はいずれPromiseに変えたい..

Slack

Slackに通知する部分。

'use strict';

const request = require("request");
const _ = require("underscore");

class Slack{
  constructor(webhookUrl){
    this.webhookUrl = webhookUrl;
  }

  sendMessageAboutGithubNotification(notifications, callback){
    let data = {
      username: "Github Notification",
      icon_emoji: ":speech_balloon:",
      attachments: _.map(notifications, (item)=>{
        return {
          title: `[${item.repositoryName}] ${item.issueTitle}`,
          title_link: item.comment.htmlUrl,
          text: item.comment.body,
          thumb_url: item.comment.user.avatar_url,
          color: '#4C4C4C'
        };
      })
    };
    request.post(
      {uri: this.webhookUrl, form: JSON.stringify(data)},
      (err, response, body) =>{
        if(err){ return callback(err); }
        callback(null);
      }
    );
  }
}

module.exports = Slack;

渡された通知データを元にWebhook URLにPOSTリクエストを投げる。通知内容はAttachmentを使って装飾。

エントリポイント

Lambdaから直接呼び出される箇所は以下。

"use strict";

const Github = require('./github');
const Slack  = require('./slack');

var handler = (event, context, callback)=> {
  let github = new Github(event.githubOAuthToken);
  let slack = new Slack(event.slackWebhookUrl);
  github.fetchNotifications((err, notifications)=>{
    if(err){ return context.fail(err); }
    slack.sendMessageAboutGithubNotification(notifications, (err)=>{
      callback(err);
      if (!err){
        console.log(`This process has finished successfully! notiifcations: notifications.length`);
      }
    });
  });
};

exports.handler = handler;

以下であるように、コールバックを利用したモデルが推奨されるようになったため、処理の最後にはcontext.succeed()ではなく、 callback(err)を呼び出す。

AWS Solutions Architect ブログ: AWS LambdaでNode.js 4.3.2が利用可能になりました

プログラミングモデルの変更

今回のリリースを機にAWS LambdaにおけるNode.jsのプログラミングが一部変更になります。変更になるというより拡張されるといったほうが正しいかも知れません。具体的にはこれまでファンクションの終了時に明示的にコールしていたcontext.done(), context.succeed(), context.fail()の3つのメソッドの代わりにコールバックを利用する形になります。これらのメソッドは今後も利用可能ですが今後はコールバックを利用したモデルが推奨となります。なお、Node.js 0.10ではこの新しいモデルは利用できないので気をつけてください。

テスト

テストはmocha & power-assertを使った。テストコードは以下のように書いた。

'use strict';
const assert = require('power-assert');
const Github = require('../src/github');

const token = process.env.GITHUB_OAUTH_TOKEN;
let github = null;

describe("Github.fetchNotifications", ()=>{
  before(()=>{
    github = new Github(token);
  });
  it("runs successfully", (done)=>{
    github.fetchNotifications((err)=>{
      if (err){ console.error(err); }
      assert(err == undefined);
      done();
    });
  });
});

describe("Github.fetchComment", ()=>{
  before(()=>{
    github = new Github(token);
  });
  it("runs successfully", (done)=>{
    let url = "https://api.github.com/repos/gaishimo/garbage-repo1/issues/comments/170316774";
    github.fetchComment(url, (err)=>{
      assert(err == undefined);
      done();
    });
  });
});

OAuth TokenやWebhook URL等の動的に変わる部分は.envから読み込む。package.jsonに以下のように記述して、npm testで実行。

  "scripts": {
    "test": "export $(cat .env | xargs) && node_modules/.bin/mocha test/**/*.js"
  },

参考

Lambda Functionのデプロイ

デプロイはGulp + node-aws-lambda を使う。
node-aws-lambdaはAWS Lambda APIのラッパで、設定ファイルを書いて実行すればLambdaへのデプロイを行ってくれる優れもの。

lambda-config.js を自分用に編集。

module.exports = {
    //accessKeyId: <access key id>,  // optional
    //secretAccessKey: <secret access key>,  // optional
    //profile: <shared credentials profile name>, // optional for loading AWS credientail from custom profile
    region: 'ap-northeast-1',
    runtime: 'nodejs4.3',
    handler: 'index.handler',
    description: "App for sending message to slack about Github notifications",
    role: "arn:aws:iam::xxxxx:role/xxxxxx",
    functionName:"tool-github-notification",
    timeout: 30,
    memorySize: 128
    //eventSource: {
    //  EventSourceArn: "",
    //  BatchSize: 200,
    //  StartingPosition: "TRIM_HORIZON"
    //}
}

デプロイは以下のコマンドで。

gulp deploy

参考

Lambda Functionの実行テスト

デプロイを行うとLambda Functionが登録されているので、AWS Consoleの該当のFunctionの画面で[Action]-[Configure Test Event]からeventデータのJsonを設定しテスト実行する。

{
    "githubOAuthToken": "<your github token>",
    "slackWebhookUrl": "<your webhook url of slack>"
}

AWS Cloud Watch Eventでスケジューリングの設定

CloudWatch Eventのページから[Create Rule]でスケジューリング設定が行える。Event SourceにScheduleを指定してスケジュールを指定する。今回は5分おきにしてみる(Fixed rate of 5 minutes)。 Targetには作成済みのLambda Functionを指定する。
Lambda Functionの指定の際に、Configure inputでConstantとして上のテスト実行の際に使ったJSONを指定。

※この操作はLambdaの画面側からも可能。

雑感

実際にこれを動かした状態でしばらく仕事してみたが、メンションコメント等に気づくタイミングが早くなったと思う。

ただ、今のままだと通知画面を見ない限りその通知はずっと出続けるので、そこは改善したい。

また、AWS Lambdaはとても便利だと実感。こんな感じで思いついたものをすぐサーバレスで作ることができる。あとはそれを迅速に作るフローを確立していけば、自分やチームのためのツールをどんどん作っていくことができると思う。それによって色々改善して業務効率を上げて自分の時間を捻出し、また新しいものを作りとよい循環になりそう。