「AWS無料相談会」をオンラインで開催中

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

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

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

方針

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

成果物

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

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

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

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

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

type client struct {
    URL           *url.URL
    Org           string
    HTTPClient    *http.Client
    Authorization string
    Logger        *log.Logger
}

client構造体を定義し、

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 メソッドを定義します。

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 関数を作っておきます。

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 構造体は事前に作っておいてください。筆者はこちらをよく利用してます。

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が叩けます。

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側の問題で使えなくなる可能性があります。)

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