2018年11月追記: GitHub の Amazon SNS 通知機能は廃止されます。https://developer.github.com/changes/2018-04-25-github-services-deprecation/ したがって、新たに設定する場合は Webhook を利用した実装を行う必要があります。
--
Slackには元から、GitHub連携が用意されている。 しかし、あまり良い物ではないと思う。
- GitHub の Issue Comment のほうで Mention しても、Slackのほうでは通知されない。もちろん、GitHub側の通知では出ているのだが、メールを見ない人もいて気づかない場合がある。
- GitHubのアカウント名とSlackのアカウント名が違う。例えば、私の場合GitHubではkawaharaを利用しているが、Slackの場合は
@ooharabucyou
を使っている。なんとややこしい!これは、自分だけでなく他の人も言える。 - メッセージ周りを自分好みにカスタマイズしたい。
そんなこんなで、SlackのAppDirectoryにある、GitHub連携をいっそこのことやめて、自分で連携する道(なるべく最短でいきたい)を選んだ。
AmazonSNS -> Lambda -> Slack
そこで取り出したるはAWS。AmazonSNSとLambdaを組み合わることにより、サーバ無しでコードを実行させることができる。幸いなことに、GitHubは最初からAmazonSNSとの連携をサポートしている。
Slack設定
Slack側で、Incoming WebHooks 設定をしておく。
設定は適当に。
AmazonSNS設定
SNSのTopicを作成。
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 修正済み)
/* 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アカウントのマップを記す。
{
"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/
テスト時には、以下のデータを使った。SNSのテストデータのテンプレートを元に、API Document にあるPayloadを送るような挙動になっている。
{
"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側に以下の様なメッセージが投稿される。
次にAmazonSNSによってLambdaが実行されるようにするために設定。
Lambda側の設定から Event source -> Add event source からトリガを作ることができる。
topic名は、先ほど作成したものを設定。なんて楽ちんなんだ。
IAMアカウント追加
GitHubから使うためのIAMアカウントを追加する。権限は、最低限のもので以下。
Resource には、AmazonSNSのARNを適用するのを忘れずに。
{
"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を選択する。
先ほど作成したbot用IAMアカウント、SNS topic (ARNを指定)、リージョン、アカウントのSecretを登録する。
上記を設定しても、まだ 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つが必要)
以下の 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連携、下が今回作ったものにより作られたメッセージ。
まとめ
- 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
をセットしないと、リンク化・通知されないので注意。
- SlackのWebHooksで