Takahiro Octopress Blog

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

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

はじめに

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

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

  • 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 type は application/json を設定します
③ Which events would you like to trigger this webhook? に設定したいものを設定します
  ※筆者の場合は、個別に細かく設定可能な Let me select individual events. を選択しています。

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

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

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

因みに、筆者は、Node.jsとExpressを利用して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リクエストされた値を分析して、最適なメッセージを送る処理を導入してみましょう。

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

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

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

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つずつメソッドを切り出しました。
『PullRequestにReviewerをセットした時に、レビュー依頼を流す』場合の処理は下記です。

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, "<$&>");
}

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

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;
}

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

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