Takahiro Octopress Blog

-1から始める情弱プログラミング

GitHubとSlackを連携させて、効率の良いPullRequestのレビューを実行しよう!

はじめに

今回は GitHubSlack の連携を自作でやってみる話の備忘録です。

と言っても、手法は本当に簡易で、

  • GitHub 上で Webhook を設定する
  • GitHub からの POSTリクエスト を受け付けるサーバを用意する
  • 受け付けた POSTリクエスト から各種値を抽出して、Slackに通知する

を対応することで実現することが可能です。
それでは早速見ていきましょう。

GitHub上でWebhookを設定する

GitHub 上で Slack と連携したい Repository を開いて、Settings ページにアクセスします。
Settings ページの左メニューから Webhooks を選択して、Add webhook を選択します。

Add webhookを選択

続いて、Add webhook ページを編集します。

Add webhookの編集

Payload URL にPOSTリクエストの送り先を設定します
Content typeapplication/json を設定します
Which events would you like to trigger this webhook? に設定したいものを設定します
  ※筆者の場合は、個別に細かく設定可能な Let me select individual events. を選択しています。

GitHubからのPOSTリクエストを受け付けるサーバを用意する

ここは開発者の好きなインフラを用意すれば良いと思います。
AWSでもGCPでも良いかと思います。

また、APIサーバの作り込みに関しても、好きな言語で、自由にフレームワーク等を使って実装すれば良いと思います。
ポイントは、時間をかけずに作ることです。
(あくまでもチーム開発などを円滑に進めるための取り組みであって、この作り込みが目的ではないためです。)

因みに、筆者は、Node.jsExpressを利用してAPIサーバを作ることにしました。

受け付けたPOSTリクエストから各種値を抽出して、Slackに通知する

では、少しAPIサーバの実装について紹介します。

package.json

まず、package.jsonは下記のようにしました。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
  "name": "github2slack",
  "version": "1.0.0",
  "description": "GitHub Action Notification",
  "main": "server.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "dependencies": {
    "@slack/web-api": "^5.0.1",
    "express": "^4.17.0"
  }
}

先程申し上げた通り Express を用いているのと、Slack Web APIを簡単に利用できる @slack/web-apiも導入しています。

server.js

続いてメインとなるserver.jsです。

POSTリクエストを受け付けるところまでですが、下記のように実装しています。
※ただ、Expressを利用している、よくある書き方かなと思います。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var https = require('https');
var express = require('express');
var bodyParser = require('body-parser')
const { WebClient, LogLevel, retryPolicies } = require('@slack/web-api');
const slackChannel = 'XXXXXXXX';  // 通知先のSlackチャンネルのID
const serverPort = 3000;

const app = express();
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());

/// POSTリクエストの受付
app.post('/', (req, res) => {
  ...
});

/// サーバを起動
app.listen(serverPort, () => console.log('Listening on port ' + serverPort));

肝心のPOSTリクエストで受け付けた後の処理ですが、

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
app.post('/', (req, res) => {
  // GitHubからのリクエストを分析
  let message = makeMessageFromGitHubRequest(req) ---> 後ほど説明します
  if (!message) {
    // 送るべきメッセージがないため、200で返して終了する
    res.status(200).send('no message');
    return;
  }

  // Slackにメッセージを投稿
  const slack = new WebClient('', {
    slackApiUrl: 'http://xxxxxxxxxxxxxxxxxxxxxx',
    retryConfig: retryPolicies.tenRetriesInAboutThirtyMinutes,
    logLevel: LogLevel.DEBUG
  })

  const main = async () => {
    try {
      const result = await slack.chat.postMessage({
        channel: slackChannel,
        text: message
      })

      res.status(200).send('message: ' + message + '\n');
    } catch (err) {
      res.status(500).send(err.message + '\n');
    }
  }

  main()
});

のようになっています。
先程導入した @slack/web-api を利用すれば、いとも簡単にSlackへの通知が可能となっています。

そしてSlackにどんな通知を送るかについてですが、GitHub上で発生したイベント毎にメッセージが異なると考えられます。
よって、GitHubからPOSTリクエストされた値を分析して、最適なメッセージを送る処理を導入してみましょう。

今回、筆者は下記のイベントをハンドリングすることにしました。

  • PullRequestReviewerをセットした時に、レビュー依頼を流す
  • PullRequestのレビューをして、ステータスを変更した時に、レビュー依頼者に知らせる
  • PullRequestdiffに沿ってコメントを書いたことを流す
  • PullRequestConversationにコメントを書いたことを流す

それらをハンドリングするメソッドをまずは用意します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
 * GitHubからのリクエストの中身を精査して、メッセージを作成する
 * @param {object}  req   POSTリクエスト
 * @return {string}       メッセージ
 */
function makeMessageFromGitHubRequest(req) {
  const gitHubEvent = req.headers['x-github-event'];
  const gitHubBody = req.body;

  if (gitHubEvent === 'pull_request') {
    return makePullRequestMessage(gitHubBody.action, gitHubBody);
  } else if (gitHubEvent === 'pull_request_review') {
    return makeChangeReviewStateMessage(gitHubBody.action, gitHubBody);
  } else if (gitHubEvent === 'pull_request_review_comment') {
    return makePullRequestReviewCommentMessage(gitHubBody.action, gitHubBody.comment);
  } else if (gitHubEvent === 'issue_comment') {
    return makeIssueCommentMessage(gitHubBody.action, gitHubBody.comment);
  } else {
    return null;
  }
}

それぞれイベントによって作成するメッセージが異なるため、1つずつメソッドを切り出しました。
PullRequestReviewerをセットした時に、レビュー依頼を流す』場合の処理は下記です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
/**
 * PullRequest関連のメッセージを作成する
 * @param {string}  action      アクション種別
 * @param {object}  body        GitHubのBody情報
 * @return {string}             メッセージ
 */
function makePullRequestMessage(action, body) {
  let mention;
  let from;
  let content;
  let title = body.pull_request.title;

  let message;
  let linkUrl = body.pull_request.html_url;

  switch(action) {
    case 'created':
      mention = makeMentionTextAtPullRequestCreated(body.pull_request);
      from = '「' + body.pull_request.user.login + '」さんから';
      content = 'PullRequestのレビュー依頼が届きました\n\n';
      message = mention + from + content + title;
      break;
    case 'review_requested':
      mention = makeMentionText(body.requested_reviewer);
      from = '「' + body.pull_request.user.login + '」さんから';
      content = 'PullRequestのレビュー依頼が届きました\n\n';
      message = mention + from + content + title;
      break;
    default:
      break;
  }

  if (!message) {
    return null;
  }
  return message + '\n' + linkUrl;
}

/**
 * PullRequest作成時にレビューを要求された場合にメンションテキストを作成する処理
 * @param {object} pullRequest  PullRequest情報
 * @return {string}             メンションテキスト
 */
function makeMentionTextAtPullRequestCreated(pullRequest) {
  let result = '';
  const requested_reviewers = pullRequest.requested_reviewers;
  requested_reviewers.forEach(reviewer => {
    // Slack APIでは、<@username>の形でメッセージを作らないとメンションできない
    const mention = "<@" + reviewer.login + "> ";
    result = result + mention;
  });
  result = result + '\n';
  return result;
}

/**
 * PullRequestレビュワー追加時にメンションテキストを作成する処理
 * @param {object} user  ユーザ情報
 * @return {string}      メンションテキスト
 */
function makeMentionText(user) {
  const mention = "<@" + user.login + "> ";
  return mention + '\n';
}

PullRequestのレビューをして、ステータスを変更した時に、レビュー依頼者に知らせる』場合は下記です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/**
 * PullRequestのReview関連のメッセージを作成する
 * @param {string}  action  アクション種別
 * @param {object}  body    GitHubのBody情報
 * @return {string}         メッセージ
 */
function makeChangeReviewStateMessage(action, body) {
  let mention;
  let from;
  let content;
  let title = convertCommentWithMention(body.review.body);

  let message;
  let linkUrl = body.review.html_url;

  switch(action) {
    case 'submitted':
      mention = makeMentionText(body.pull_request.user);
      from = '「' + body.review.user.login + '」さんから'
      content = 'レビュー結果: *' + body.review.state + '* が返ってきました。\n\n';
      message = mention + from + content + title;
      break;
    default:
      break;
  }

  if (!message) {
    return null;
  }
  return message + '\n' + linkUrl;
}

/**
 * コメント内にメンションがあった場合に、Slackが検知できるメンションテキストに置き換える処理
 * @param {object} comment  コメント
 * @return {string}         メンションテキスト
 */
function convertCommentWithMention(comment) {
  return comment.replace(/@[A-Za-z0-9-/]+/g, "<$&>");
}

『* PullRequestdiffに沿ってコメントを書いたことを流す』場合は下記です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
 * PullRequestのReviewコメント関連のメッセージを作成する
 * @param {string}  action  アクション種別
 * @param {object}  comment コメント情報
 * @return {string}         メッセージ
 */
function makePullRequestReviewCommentMessage(action, comment) {
  let from;
  let content;
  let title = convertCommentWithMention(comment.body);

  let message;
  let linkUrl = comment.html_url;

  switch(action) {
    case 'created':
      // PullRequestのFileChanges内でコメントした場合
      from = '「' + comment.user.login + '」さんから'
      content = 'コメントが届きました。\n\n';
      message = from + content + title;
      break;
    default:
      break;
  }

  if (!message) {
    return null;
  }
  return message + '\n' + linkUrl;
}

『* PullRequestConversationにコメントを書いたことを流す』場合は下記です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
 * PullRequestやIssueのConversation欄のコメント関連のメッセージを作成する
 * @param {string}  action  アクション種別
 * @param {object}  comment コメント情報
 * @return {string}         メッセージ
 */
function makeIssueCommentMessage(action, comment) {
  let from;
  let content;
  let title = convertCommentWithMention(comment.body);

  let message;
  let linkUrl = comment.html_url;

  switch (action) {
    case 'created':
      from = '「' + comment.user.login + '」さんから';
      content = 'コメントが届きました。\n\n';
      message = from + content + title;
      break;
    default:
      break;
  }

  if (!message) {
    return null;
  }
  return message + '\n' + linkUrl;
}

まとめ

さて、割と手軽に実装できることがわかったところで本日はここまで。
今後、もっと様々なイベントをキャッチしてSlackに流す必要性が出てきたら、どんどん足していこうかな。

Comments