Github の mention を Slack に通知する仕組みを作った part1

概要

リサーチ・アンド・イノベーションの浜田(hamadu)です。 最近、社内では CODE のインフラを さくらのクラウド から Google Cloud Platform へと移行する流れが進んでいます。その一環ではないのですが、Google Clound Functions を使って遊んでいたところ、便利な仕組みが生えたので紹介します。GitHub の Issue や Pull Requestに mention(@(GitHubのユーザ名)) が来た時に、その人に向けて Slack で通知するというものです。

これがなぜ必要かというと、弊社の開発メンバーでは GitHubのユーザ名とSlackのユーザ名が異なる人が多く、公式の GitHubアプリで mention を上手く通知する機能が無いためです。また、自前で作れば Slack に送るタイミングやメッセージを自由にカスタマイズできる、というメリットもあります。

大まかな仕組みは以下の図のとおりです。

f:id:r-n-i:20180814175755p:plain

  • (1) リポジトリのIssueやPull Requestにコメントがあると、登録しておいた Webhooks により Google Cloud Functions が走る
  • (2) Google Cloud Functions ではメッセージのパースをし、必要に応じて @username の部分を Slack 向けに書き換える。作ったメッセージを Incoming Webhook の URL に飛ばす
  • (3) Slack の指定のチャンネルに向けてメッセージが飛ぶ

以下、順番に作成方法を解説します。長くなってしまうので2パート構成で、本稿は前半部分を説明します。

  • part1: GitHub Webhooks 経由で Google Cloud Functions が発火する仕組みを作る(上図の (1))
  • part2: Google Cloud Functions の中身を作り込み、Slackにメッセージを飛ばせるようにする(上図の (2) と (3))

Google Cloud Functions を使ってみる

まず、GitHub Webhooks の受け先である Google Cloud Functions を設定します。 開発環境のインストールおよび関数のデプロイ方法は 公式チュートリアル にあるので割愛します。

Google Cloud Functions は JavaScript(node) で記述する必要があります。中身はほぼ空で大丈夫ですが、ここではリクエストオブジェクトのヘッダ、および本文を console.log() で出力させています。

exports.githubToSlack = (req, res) => {
  console.log(req.headers);
  console.log(req.body);
  res.status(200).end();
};

この関数をデプロイすると、関数を発火するためのURLが発行されます。URLは次のような形になっているはずです。(これはデプロイ後、Google Cloud Functions の関数詳細画面 にも表示されます)

https://(リージョン名)-(プロジェクト名).cloudfunctions.net/githubToSlack

ためしに curl で関数を叩いてみましょう。今回は、Content-Type: application/jsonJSON が飛んでくるように webhooks を設定するので、適当な JSON文字列 を送ってみます。

> curl -X POST -H "Content-Type:application/json"  -d '{"test":"hoge"}' (関数のURL)

Google Cloud Functions のログを見て、ヘッダと本文が出力されていれば成功です!

GitHub Webhooks

GitHub Webhooks とは、github.com 上で起こるイベントを HTTP POST で通知する仕組みです。これを Repository や、Organization に対して設定できます。どのイベントに対して発火してほしいかが個別に指定できるので、ここでは Issue, Pull Request に対して発火するように指定します。

連携したいリポジトリの設定画面を開いて、「Add webhook」を押すと設定画面が開くので、以下の様に設定します。特に、Secret は「リクエストが確かにwebhooks経由である」ことを cloud functions に識別させるのに使います。一度設定すると再表示されないので、値をメモっておきましょう。

項目 設定値
Payload URL cloud functionsのURL
Content type application/json
Secret 文字列
Which events...webhook? Let me select individual events.
Issues, Pull Requestsを選択

ここまでできたら一旦テストしておきます。設定したリポジトリで、Issue や Pull Requestを実際に作ってみましょう。Google Cloud Functions の Console にログが出ていれば成功です。

リクエストを検証する

さて、今のままでは URL さえ分かってしまうと何処からでも関数が呼べてしまうので、先程設定した webhook 経由でのみ関数が実行されるようにします。

GitHub は webhook を送る際、リクエスト本文の HMACX-Hub-Signature というヘッダに付与してくれます。この HMAC の秘密鍵となるのが、先程設定した Secret なわけですね。ということでリクエストが正しいか調べるには、同じアルゴリズムを用いて HMAC を計算しヘッダと一致しているか調べればOK。具体的なアルゴリズムGitHubのドキュメント に書かれています。

HMACの計算及び比較を JavaScript で実行するには cryptocreateHmac関数、およびセキュアな比較を行うため secure-compare があればいいでしょう。以下は実装例です。

const crypto = require('crypto');
const secureCompare = require('secure-compare');

function validateRequest(req) {
  const cipher = 'sha1';
  const signature = req.headers['x-hub-signature'];
  const hmac = crypto.createHmac(cipher, '<Secretで設定した文字列>')
    .update(req.rawBody)
    .digest('hex');
  const expectedSignature = `${cipher}=${hmac}`;
  return secureCompare(signature, expectedSignature);
}

これでコア部分の実装はできました。しかし、まだ改善できる箇所があります。Secretを直接ソースコードに記述するのをやめて、環境変数に逃しましょう。Cloud function 内で環境変数を使うには、

process.env['VARIABLE_NAME']

のようにします。これは、nodeプロセスで環境変数の値を得る通常の方法です環境変数に値を入れるには、

> gcloud beta functions deploy FUNCTION_NAME --set-env-vars VARIABLE_NAME=xxx

とします。詳しくはドキュメント Using Environment Variables | Cloud Functions Documentation を読んでください。ここでは、GITHUB_SECRET という環境変数を使うことにします。

最終的なコードは次のようになりました。

const crypto = require('crypto');
const secureCompare = require('secure-compare');

function validateRequest(req) {
  const cipher = 'sha1';
  const signature = req.headers['x-hub-signature'];
  const hmac = crypto.createHmac(cipher, process.env['GITHUB_SECRET'])
    .update(req.rawBody)
    .digest('hex');
  const expectedSignature = `${cipher}=${hmac}`;
  return secureCompare(signature, expectedSignature);
}

exports.githubToSlack = (req, res) => {
  if (!validateRequest(req)) {
    return res.status(403).send('wrong signature.');
  }

  console.log(req.headers);
  console.log(req.body);
  res.status(200).end();
}

ここまで、うまく動いているかテストしておきましょう。Issue および Pull Request を作ると正しく発火すること、curl で適当なヘッダを付けると落とされることを確認しておきます。

まとめ

長くなってしまいましたが、GitHub <=> Google Cloud Functions 連携の基本的な手順は以上です。ここまでくれば、あとはリクエストの中身を見て、Slack に送る具体的なメッセージを構成するだけになります。これは後日、 part2 にて説明します。

リサーチ・アンド・イノベーションでは、プロダクト開発プロセスそのものをハックしていけるエンジニアを募集中です。

からご応募お待ちしております。