SlackのGitHub連携をやめGitHub->AmazonSNS->Lambda->Slackで連携する

2018年11月追記: GitHub の Amazon SNS 通知機能は廃止されます。https://developer.github.com/changes/2018-04-25-github-services-deprecation/ したがって、新たに設定する場合は Webhook を利用した実装を行う必要があります。

--

Slackには元から、[GitHub連携が用意されている。](https://slack.com/apps/A0F7YS2SX-github) しかし、あまり良い物ではないと思う。

- GitHub の Issue Comment のほうで Mention しても、Slackのほうでは通知されない。もちろん、GitHub側の通知では出ているのだが、メールを見ない人もいて気づかない場合がある。
- GitHubのアカウント名とSlackのアカウント名が違う。例えば、私の場合GitHubでは[kawahara](https://github.com/kawahara/)を利用しているが、Slackの場合は`@ooharabucyou`を使っている。なんとややこしい！これは、自分だけでなく他の人も言える。
- メッセージ周りを自分好みにカスタマイズしたい。

そんなこんなで、SlackのAppDirectoryにある、GitHub連携をいっそこのことやめて、自分で連携する道（なるべく最短でいきたい)を選んだ。

# AmazonSNS -> Lambda -> Slack

そこで取り出したるはAWS。AmazonSNSとLambdaを組み合わることにより、サーバ無しでコードを実行させることができる。幸いなことに、GitHubは最初からAmazonSNSとの連携をサポートしている。

## Slack設定

Slack側で、Incoming WebHooks 設定をしておく。

https://slack.com/apps/A0F7XDUAZ-incoming-webhooks

設定は適当に。

<img width="300" alt="スクリーンショット 2016-01-27 15.28.32.png" src="https://qiita-image-store.s3.amazonaws.com/0/61997/23873646-7bbe-1d2b-fca2-d16e44b3bc2e.png">

## AmazonSNS設定

SNSのTopicを作成。

<img width="365" alt="スクリーンショット 2016-01-27 15.31.12.png" src="https://qiita-image-store.s3.amazonaws.com/0/61997/aa8d6492-7dee-36dc-8c36-a32e489758fa.png">

<img width="664" alt="スクリーンショット 2016-01-27 15.31.23.png" src="https://qiita-image-store.s3.amazonaws.com/0/61997/cc0aaa90-dc80-e232-19fb-5f2b79aeca12.png">

<img width="910" alt="スクリーンショット 2016-01-27 15.31.42.png" src="https://qiita-image-store.s3.amazonaws.com/0/61997/f2461791-4a22-c600-aafe-10783edcdb1d.png">

<img width="716" alt="スクリーンショット 2016-01-27 15.31.56.png" src="https://qiita-image-store.s3.amazonaws.com/0/61997/3a40683d-fbb0-60d2-4662-d87ae42e0851.png">

Lambdaとの連携はあとでできるので、とりあえずここまで作成して終わり。ここで作成した、TopicARNが後々必要になる。

## Lambdaコード

(追記 2016-01-30)
https://github.com/kawahara/github2slack-lambda
GitHub にコードを追加しました。デプロイ用のgulpタスクも用意しております
(追記終わり)

ローカルに適当なフォルダを作って、その中に `index.js` を作成。今のところ、4つのEventにのみ対応させているが、以下のコードを書き換えるだけで、いろいろ対応は可能。EventTypeに関しては、SNS Message ではなく、MessageAttributes のほうに乗るので注意が必要。
~~結構適当に書いたので、Issueをアサイン者・ラベル付きで開いた時に、opended, labeled, assigned の3回メッセージ投稿されてしまう挙動になっている。後で治す予定。~~　(1/30 修正済み)

```js:index.js
/* jshint: indent:2 */
var request = require('request'),
    config  = require('./config.json');

var convertName = function (body) {
  return body.replace(/@[a-zA-Z0-9_\-]+/g, function (m) {
    return config.account_map[m] || m;
  });
};

var link = function (url, text) {
  return '<' + url + '|' + text + '>';
};

exports.handler = function (event, context) {
  console.log('Received GitHub event: ' + event.Records[0].Sns.Message);
  var msg = JSON.parse(event.Records[0].Sns.Message);
  var eventName = event.Records[0].Sns.MessageAttributes['X-Github-Event'].Value;
  var text = '';

  switch (eventName) {
    case 'issue_comment':
    case 'pull_request_review_comment':
      var comment = msg.comment;
      text += comment.user.login + ": \n";
      text += convertName(comment.body) + "\n";
      text += comment.html_url;
      break;
    case 'issues':
      var issue = msg.issue;
      if (msg.action == 'opended' || msg.action == 'closed') {
          text += 'Issue ' + msg.action + "\n";
          text += link(issue.html_url, issue.title);
      }
      break;
    case 'push':
      text += 'Pushed' + "\n";
      text += msg.compare + "\n";
      for (var i = 0; i < msg.commits.length; i++) {
        var commit = msg.commits[i];
        text += link(commit.url, commit.id.substr(0, 8)) + ' ' + commit.message + ' - ' + commit.author.name + "\n";
      }
      break;
    case 'pull_request':
      var pull_request = msg.pull_request;
      if (msg.action == 'opended' || msg.action == 'closed') {
          text += 'Pull Request ' + msg.action + "\n";
          text += pull_request.title + "\n";
          text += pull_request.body + "\n";
          text += pull_request.html_url;
      }
      break;
  }

  if (!text) {
    context.done();
    return;
  }

  request({
    url: config.slack_web_hook_url,
    method: 'POST',
    headers: {'Content-Type': 'application/json'},
    json: {text: text, link_names: 1}
  }, function () {
    context.done();
  });
};
```

うっかり、クセで request を使ってしまったので (別に使わないでhttpを使う手もある), 以下のコマンドを叩き、依存解決をする

```
npm init
npm install --save request
```

設定ファイルは、以下のような感じで作るとよろし。WebHookURLと、GitHubアカウントとSlackアカウントのマップを記す。

```json:config.json
{
  "slack_web_hook_url": "https://hooks.slack.com/services/dummy",
  "account_map": {
    "@kawahara": "@ooharabucyou",
    "@shotaatago": "@shota-a"
  }
}
```

以下でパッケージ化して、`github-bot.zip` をアップロードする。 ~~この辺は、http://dev.classmethod.jp/cloud/aws/how-to-deploy-a-lambda-function-with-gulp/ とか使えば楽そうね。~~ (1/30 対応しました。GitHubリポジトリにあるほうでは、以下のコマンドではなく `gulp deploy` で作成・更新ができます。)

```
zip -r github-bot.zip index.js config.json node_modules/
```

<img width="956" alt="スクリーンショット 2016-01-27 18.42.00.png" src="https://qiita-image-store.s3.amazonaws.com/0/61997/85426bae-a9dc-f95c-e491-1990f1975f88.png">


テスト時には、以下のデータを使った。SNSのテストデータのテンプレートを元に、[API Document にあるPayload](https://developer.github.com/v3/activity/events/types/#commitcommentevent)を送るような挙動になっている。

```
{
  "Records": [
    {
      "EventVersion": "1.0",
      "EventSubscriptionArn": "arn:aws:sns:EXAMPLE",
      "EventSource": "aws:sns",
      "Sns": {
        "SignatureVersion": "1",
        "Timestamp": "1970-01-01T00:00:00.000Z",
        "Signature": "EXAMPLE",
        "SigningCertUrl": "EXAMPLE",
        "MessageId": "95df01b4-ee98-5cb9-9903-4c221d41eb5e",
        "Message": "{\"action\":\"created\",\"issue\":{\"url\":\"https://api.github.com/repos/baxterthehacker/public-repo/issues/2\",\"labels_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/issues/2/labels{/name}\",\"comments_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/issues/2/comments\",\"events_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/issues/2/events\",\"html_url\":\"https://github.com/baxterthehacker/public-repo/issues/2\",\"id\":73464126,\"number\":2,\"title\":\"Spelling error in the README file\",\"user\":{\"login\":\"baxterthehacker\",\"id\":6752317,\"avatar_url\":\"https://avatars.githubusercontent.com/u/6752317?v=3\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/baxterthehacker\",\"html_url\":\"https://github.com/baxterthehacker\",\"followers_url\":\"https://api.github.com/users/baxterthehacker/followers\",\"following_url\":\"https://api.github.com/users/baxterthehacker/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/baxterthehacker/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/baxterthehacker/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/baxterthehacker/subscriptions\",\"organizations_url\":\"https://api.github.com/users/baxterthehacker/orgs\",\"repos_url\":\"https://api.github.com/users/baxterthehacker/repos\",\"events_url\":\"https://api.github.com/users/baxterthehacker/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/baxterthehacker/received_events\",\"type\":\"User\",\"site_admin\":false},\"labels\":[{\"url\":\"https://api.github.com/repos/baxterthehacker/public-repo/labels/bug\",\"name\":\"bug\",\"color\":\"fc2929\"}],\"state\":\"open\",\"locked\":false,\"assignee\":null,\"milestone\":null,\"comments\":1,\"created_at\":\"2015-05-05T23:40:28Z\",\"updated_at\":\"2015-05-05T23:40:28Z\",\"closed_at\":null,\"body\":\"It looks like you accidently spelled 'commit' with two 't's.\"},\"comment\":{\"url\":\"https://api.github.com/repos/baxterthehacker/public-repo/issues/comments/99262140\",\"html_url\":\"https://github.com/baxterthehacker/public-repo/issues/2#issuecomment-99262140\",\"issue_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/issues/2\",\"id\":99262140,\"user\":{\"login\":\"baxterthehacker\",\"id\":6752317,\"avatar_url\":\"https://avatars.githubusercontent.com/u/6752317?v=3\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/baxterthehacker\",\"html_url\":\"https://github.com/baxterthehacker\",\"followers_url\":\"https://api.github.com/users/baxterthehacker/followers\",\"following_url\":\"https://api.github.com/users/baxterthehacker/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/baxterthehacker/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/baxterthehacker/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/baxterthehacker/subscriptions\",\"organizations_url\":\"https://api.github.com/users/baxterthehacker/orgs\",\"repos_url\":\"https://api.github.com/users/baxterthehacker/repos\",\"events_url\":\"https://api.github.com/users/baxterthehacker/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/baxterthehacker/received_events\",\"type\":\"User\",\"site_admin\":false},\"created_at\":\"2015-05-05T23:40:28Z\",\"updated_at\":\"2015-05-05T23:40:28Z\",\"body\":\"You are totally right! I'll get this fixed right away.\"},\"repository\":{\"id\":35129377,\"name\":\"public-repo\",\"full_name\":\"baxterthehacker/public-repo\",\"owner\":{\"login\":\"baxterthehacker\",\"id\":6752317,\"avatar_url\":\"https://avatars.githubusercontent.com/u/6752317?v=3\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/baxterthehacker\",\"html_url\":\"https://github.com/baxterthehacker\",\"followers_url\":\"https://api.github.com/users/baxterthehacker/followers\",\"following_url\":\"https://api.github.com/users/baxterthehacker/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/baxterthehacker/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/baxterthehacker/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/baxterthehacker/subscriptions\",\"organizations_url\":\"https://api.github.com/users/baxterthehacker/orgs\",\"repos_url\":\"https://api.github.com/users/baxterthehacker/repos\",\"events_url\":\"https://api.github.com/users/baxterthehacker/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/baxterthehacker/received_events\",\"type\":\"User\",\"site_admin\":false},\"private\":false,\"html_url\":\"https://github.com/baxterthehacker/public-repo\",\"description\":\"\",\"fork\":false,\"url\":\"https://api.github.com/repos/baxterthehacker/public-repo\",\"forks_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/forks\",\"keys_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/keys{/key_id}\",\"collaborators_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/collaborators{/collaborator}\",\"teams_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/teams\",\"hooks_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/hooks\",\"issue_events_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/issues/events{/number}\",\"events_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/events\",\"assignees_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/assignees{/user}\",\"branches_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/branches{/branch}\",\"tags_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/tags\",\"blobs_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/git/blobs{/sha}\",\"git_tags_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/git/tags{/sha}\",\"git_refs_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/git/refs{/sha}\",\"trees_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/git/trees{/sha}\",\"statuses_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/statuses/{sha}\",\"languages_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/languages\",\"stargazers_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/stargazers\",\"contributors_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/contributors\",\"subscribers_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/subscribers\",\"subscription_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/subscription\",\"commits_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/commits{/sha}\",\"git_commits_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/git/commits{/sha}\",\"comments_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/comments{/number}\",\"issue_comment_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/issues/comments{/number}\",\"contents_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/contents/{+path}\",\"compare_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/compare/{base}...{head}\",\"merges_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/merges\",\"archive_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/{archive_format}{/ref}\",\"downloads_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/downloads\",\"issues_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/issues{/number}\",\"pulls_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/pulls{/number}\",\"milestones_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/milestones{/number}\",\"notifications_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/notifications{?since,all,participating}\",\"labels_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/labels{/name}\",\"releases_url\":\"https://api.github.com/repos/baxterthehacker/public-repo/releases{/id}\",\"created_at\":\"2015-05-05T23:40:12Z\",\"updated_at\":\"2015-05-05T23:40:12Z\",\"pushed_at\":\"2015-05-05T23:40:27Z\",\"git_url\":\"git://github.com/baxterthehacker/public-repo.git\",\"ssh_url\":\"git@github.com:baxterthehacker/public-repo.git\",\"clone_url\":\"https://github.com/baxterthehacker/public-repo.git\",\"svn_url\":\"https://github.com/baxterthehacker/public-repo\",\"homepage\":null,\"size\":0,\"stargazers_count\":0,\"watchers_count\":0,\"language\":null,\"has_issues\":true,\"has_downloads\":true,\"has_wiki\":true,\"has_pages\":true,\"forks_count\":0,\"mirror_url\":null,\"open_issues_count\":2,\"forks\":0,\"open_issues\":2,\"watchers\":0,\"default_branch\":\"master\"},\"sender\":{\"login\":\"baxterthehacker\",\"id\":6752317,\"avatar_url\":\"https://avatars.githubusercontent.com/u/6752317?v=3\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/baxterthehacker\",\"html_url\":\"https://github.com/baxterthehacker\",\"followers_url\":\"https://api.github.com/users/baxterthehacker/followers\",\"following_url\":\"https://api.github.com/users/baxterthehacker/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/baxterthehacker/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/baxterthehacker/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/baxterthehacker/subscriptions\",\"organizations_url\":\"https://api.github.com/users/baxterthehacker/orgs\",\"repos_url\":\"https://api.github.com/users/baxterthehacker/repos\",\"events_url\":\"https://api.github.com/users/baxterthehacker/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/baxterthehacker/received_events\",\"type\":\"User\",\"site_admin\":false}}",
        "MessageAttributes": {
          "X-Github-Event": {
            "Type": "String",
            "Value": "issue_comment"
          }
        },
        "Type": "Notification",
        "UnsubscribeUrl": "EXAMPLE",
        "TopicArn": "arn:aws:sns:EXAMPLE",
        "Subject": "TestInvoke"
      }
    }
  ]
}
```

テストに成功すると、Slack側に以下の様なメッセージが投稿される。


<img width="611" alt="スクリーンショット 2016-01-27 18.47.14.png" src="https://qiita-image-store.s3.amazonaws.com/0/61997/b7b501f9-0e75-0a1e-bdce-c055dfde8695.png">


次にAmazonSNSによってLambdaが実行されるようにするために設定。

Lambda側の設定から Event source -> Add event source からトリガを作ることができる。

<img width="820" alt="スクリーンショット 2016-01-27 18.44.27.png" src="https://qiita-image-store.s3.amazonaws.com/0/61997/65dbbf0a-19c4-c0f2-3905-9ad69a8a5ecc.png">

topic名は、先ほど作成したものを設定。なんて楽ちんなんだ。

<img width="869" alt="スクリーンショット 2016-01-27 18.44.38.png" src="https://qiita-image-store.s3.amazonaws.com/0/61997/5a92b9b8-c220-e490-aca2-6eb1a1c401df.png">



## IAMアカウント追加

GitHubから使うためのIAMアカウントを追加する。権限は、最低限のもので以下。

Resource には、AmazonSNSのARNを適用するのを忘れずに。

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": [
        "sns:Publish"
      ],
      "Sid": "Stmt0000000000000",
      "Resource": [
        "arn:aws:sns:ap-northeast-1:xxxxxxx:github2slack"
      ],
      "Effect": "Allow"
    }
  ]
}
```

## GitHub側設定

最後にGitHubからの通知を、AmazonSNSに転送するための設定を行う。対象のリポジトリの設定画面から、Webhooks & Service を選び、Add Services から AmazonSNSを選択する。

<img width="744" alt="スクリーンショット 2016-01-27 18.51.21.png" src="https://qiita-image-store.s3.amazonaws.com/0/61997/d69bfb35-c0e1-9abf-5114-0e40cf6b192b.png">

先ほど作成したbot用IAMアカウント、SNS topic (ARNを指定)、リージョン、アカウントのSecretを登録する。


<img width="782" alt="スクリーンショット 2016-01-27 18.51.56.png" src="https://qiita-image-store.s3.amazonaws.com/0/61997/68f48efc-a515-5010-d804-fb45b1774da9.png">


上記を設定しても、まだ `push` イベントしか受け取れない問題がある。そこで、GitHubのAPIを直接コールして、`issues`, `issue_comment`, `pull_request_review_comment` のイベントも送るような設定を送る。

設定画面URLから hook IDが取得できるので、それを元にAPIをコール。(アクセストークンは、https://help.github.com/articles/creating-an-access-token-for-command-line-use/ を参考に発行できる。scope は organization の場合、 `read:org`, `repo` の2つが必要)

<img width="274" alt="スクリーンショット 2016-01-27 19.03.37.png" src="https://qiita-image-store.s3.amazonaws.com/0/61997/ac166660-ef92-01dc-ff80-2011abe3855f.png">

以下の *YOUR_TOKEN*, *OWNER*, *REPOSITORY_NAME*, *HOOK_ID* は置き換える。

```
curl -X PATCH -H "Authorization: Bearer YOUR_TOKEN" -H "Content-Type: application/json" -d '{
    "active": true,
    "events": ["push", "issues", "issue_comment", "pull_request", "pull_request_review_comment"]
}' 'https://api.github.com/repos/OWNER/REPOSITORY_NAME/hooks/HOOK_ID'
```

# 完成

これで、`@kawahara` がGitHub上で呼びかけられたときに Slack上で `@ooharabucyou` に変換して呼んでくれるbotが出来上がった。
上が、デフォルトのGitHub連携、下が今回作ったものにより作られたメッセージ。

<img width="694" alt="スクリーンショット 2016-01-27 18.56.29.png" src="https://qiita-image-store.s3.amazonaws.com/0/61997/2d75edec-26da-cb70-c7b3-1cbdd9d2d11f.png">


# まとめ

- botづくりに関してはHubotもいいけど、サーバ用意しない&メンテしたくないならLambdaを使えば楽そう。API Gateway + Lambda + Slack Outgoing WebHook で何らかのコメントに応答するものを作ったり、CloudWatch Scheduler + Lambda で定期的に何か調査して Slack に通知したりと、サーバレスでいろいろできることがあり良い。
- GitHub側のAmazonSNS設定は、Event設定をAPIから直接やることを忘れない。これで私の1時間くらいがもってかれ、一時期はAPIGateway経由でWebhook受けようかと迷ったほどであった。

# 【追記】料金について

こちらを適用して、1ヶ月間様子を見たところ無料でした。利用頻度としてはこんな感じ。

- SNSへの通知 = GitHubの活動量: 3,000回くらい
    - 1,000,000回まで無料
    - Lambdaへの通知は無料
- Lambda の実行時間: 100秒くらい
    - 1,000,000回実行まで無料
    - 400,000秒まで無料 (厳密には1GBのメモリを400,000秒専有という感じで計算する。今のところ、128MBのメモリ設定で元気いっぱい動いている)


ということで、2016年3月現在あと300倍稼働しても無料だと思われます。社内で使う分には全然無料で行けそう。

# 参考にした記事

- http://maxbeatty.com/blog/2015/05/custom-github-events-slack-aws-sns-lambda/
- https://groups.google.com/forum/#!topic/slack-api/HBmj2XaG_g0
    - SlackのWebHooksで `link_names` に `1` をセットしないと、リンク化・通知されないので注意。