PullRequestをSlackから取得できるようにしてみた

MMMサーバサイドエンジニアの柳沼です。この間雪まつりに行きました。

MMMはメンバー数が多くないこともあり、ひとりが複数のプロジェクトに関わることが多くあります。
そのような状況では、自分に来ているプルリクエストを見落としがちで、たまに催促されることがあります。
あまり良くない状況なので、改善方法を考えてみました。

方針

  • Slackのスラッシュコマンドを叩くと、レビュー待ちになっている && まだレビューしていない PRを一覧で取得できるようにする。

成果物

こちらのリポジトリに置いときました。
MMMにフィットするように作ってあるので、そのままは使えません。

スラッシュコマンドの作成

Go製です。
スラッシュコマンドをつくるのは始めてでしたが、公式
参考に、簡単に作れました。
エンドポイントとポートを指定すればPostリクエストが飛んでくれるので、EC2などで作る場合はセキュリティグループの設定をいい感じにやってください。

GoでAPIクライアントを実装すること

APIクライアントの実装は、以下のように行います。

1
2
3
4
5
6
7
type client struct {
URL *url.URL
Org string
HTTPClient *http.Client
Authorization string
Logger *log.Logger
}

client構造体を定義し、

1
2
3
4
5
6
7
8
9
10
11
func newClient(org string, auth string) *client {
client := new(client)
u, _ := url.Parse("https://api.github.com")
client.URL = u
client.Org = org
client.HTTPClient = &http.Client{}
client.Authorization = auth
f, _ := os.OpenFile("access.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0755)
client.Logger = log.New(f, "logger: ", log.Ldate|log.Ltime|log.Lmicroseconds|log.Lshortfile)
return client
}

client構造体を生成する関数を定義します。
こちらの実装ではログ出力先をファイルにしていますが、引数で渡すようにすれば標準出力にもできます。
APIのバージョンなどが異なる場合は、中のURLを変えて、 clientV1 clientV2 などに構造体を分けるのがおすすめです。

そして、 newRequest メソッドを定義します。

1
2
3
4
5
6
7
8
9
10
func (c *client) newRequest(method string, spath string, body io.Reader) (*http.Request, error) {
u := *c.URL
u.Path = path.Join(c.URL.Path, spath)
req, err := http.NewRequest(method, u.String(), body)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", c.Authorization)
return req, nil
}

最後に、 decodeBody 関数を作っておきます。

1
2
3
4
5
func decodeBody(resp *http.Response, out interface{}) error {
defer resp.Body.Close()
decoder := json.NewDecoder(resp.Body)
return decoder.Decode(out)
}

後は、実際のAPIに対応するメソッドを作ります。
例えば、GithubのプルリクエストのAPIを使うには、こんな感じにできます。
pullRequests 構造体は事前に作っておいてください。筆者はこちらをよく利用してます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func (c *client) getPRs(repo string) (*pullRequests, error) {
spath := fmt.Sprintf("/repos/%v/%v/pulls", c.Org, repo)
req, _ := c.newRequest("GET", spath, nil)
res, err := c.HTTPClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var prs pullRequests
if err := decodeBody(res, &prs); err != nil {
return nil, err
}
return &prs, err
}

これらを利用すると、以下のようにAPIが叩けます。

1
2
3
4
5
6
func main() {
var c *client
c = newClient(os.Getenv("PR_GITHUB_ORG"), os.Getenv("PR_GITHUB_TOKEN"))
prs, err := c.getPRs("example_repository_name")
c.log(err)
}

このやり方であれば、別のAPIを使うときも、メソッドを増やせば対応できるし、mainの記述が減ります。
*http.Client オブジェクトを使いまわせるのもメリットです。

使ってみた様子

所感

複数のリポジトリを見に行かなくてよいのが楽です。
今までは github.com/pulls/mentionedを見たりしていたのですが、WIPのものや、
すでにレビュー済みのものも見えたりしていたので、使いやすいな〜と思っています。
社内でもけっこう使ってくれる人がいて嬉しいです。

スラッシュコマンドを自動実行する方法

普通にAPIからSlackにスラッシュコマンドを投稿しても、文字列としてポストされてしまいます。
これは、そもそもポスト先のURLが異なっているためです。
こんな感じでたたけることを確認しています。(デベロッパーツールから確認したエンドポイントなので、Slack側の問題で使えなくなる可能性があります。)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
TEAM='team_name'
USER='user_name'
CHANNEL_ID='channel_id' # SlackのWeb版からアクセスした時のURL末尾の文字列
TOKEN='your_token'

curl "https://${TEAM}.slack.com/api/chat.command" \
-H 'Content-Type: multipart/form-data; boundary=----{boundary_string}' \
--data-binary $"------{boundary_string}
Content-Disposition: form-data; name='command'

/gp
------{boundary_string}
Content-Disposition: form-data; name='text'

${USER}
------{boundary_string}
Content-Disposition: form-data; name='channel'

${CHANNEL_ID}
------{boundary_string}
Content-Disposition: form-data; name='token'

${TOKEN}
------{boundary_string}--\r\n"
このエントリーをはてなブックマークに追加