Visual Studio Team Services と Rocket.Chat の連携 (プルリクエストだけ)

平間ソンでできたこと1つ目。

背景

  • 開発チームはVSTSを使っている
  • ちょっとした連絡にはRocket.Chatを使っている
  • VSTSでプルリクエストを作った後、いちいち手動でRocket.Chatに連絡しているので、面倒
  • Rocket.Chatのサーバは 社内イントラ にある

やりたいこと

  • VSTSでプルリクエストを作ったら、自動でRocket.Chatにメッセージを投げるようにしたい

実現案

a. VSTSのRoom使う (Service Hooks)

多分一番素直なやり方。今回は使えないけど。

連携方法

Roomを開いて、Manage Events -> Pull requestsを選択。 出てくるダイアログで、プロジェクトとリポジトリ、変更した人(チャットメンバーまたは誰でも)を選べば通知ができる。(チェックボックスをONにするのを忘れずに)

詳しくは https://msdn.microsoft.com/en-us/library/vs/alm/work/productivity/collaborate-in-a-team-room あたりを見ればわかる。

対応しているイベント

作成と、ステータス変更。タイトルなどの修正は対応してない。

連携イメージ

f:id:couger:20160503152620p:plain

b. Service Hooksを使う

Service Hooksについて

Service Hooksを使うと、外部ツールと連携ができる。 https://www.visualstudio.com/en-us/get-started/integrate/service-hooks/webhooks-and-vso-vs

連携可能なツール

一覧がどこにあるのかわからなかった。 https://www.visualstudio.com/en-us/integrate/explore/explore-vso-vsi.aspx で紹介されているアイコンでなんとなくわかるのでよし。

Rocket.Chatとの連携

Rocket.ChatのIncomming WebHookはSlackと互換性があるので、Service Hooksの設定で連携先にSlackを選択、URLにRocket.ChatのIncomming WebHookのURLを設定すれば良い。 ただし、https必須。

Rocket.Chatをhttps

独自ドメインも証明書も持ってないので、AzureのWeb Appsでも使おうかと思ったけど、はまりそうだったので、Herokuを利用。Herokuだと本家リポジトリにあるリンクから一発でデプロイできる。

https://github.com/RocketChat/Rocket.Chat

でも、ここまでするなら素直にSlack使ったほうがいいよなぁ...と思ったことは内緒にしておく。

連携方法

Rocket.Chat

管理 -> サービス連携 -> 新しいサービス連携 -> Incomming WebHookで作成。 作成したら、Webhook URLに表示されているURLをメモしておく。

VSTS

プロジェクトの設定 -> Service Hooks -> "+"ボタンでHookを追加する。

  • Serivice: Slack
  • Trigger: Pull request created
  • Action:
    • Slack Webhook URL: (Rocket.Chatで作成したWebhookのURLを設定)

Trigger "Pull request updated" についても同じようにHookを作る。

f:id:couger:20160503152221p:plain

対応しているイベント

Roomと同じ。 作成と、ステータス変更。タイトルなどの修正は対応してない。

連携イメージ

f:id:couger:20160503152822p:plain

URLのリンクがうまくいってないのは、SlackとRocket.Chatで書式が違うから。(だと思う)
Incommig WebHookの設定にScriptという項目があるので、そこでごにょごにょすれば直るかもしれない?

c. REST APIを使う

背景の Rocket.Chatのサーバは社内イントラにある のおかげで、a,b案は使えない。 ので、結局ここに落ち着くはず。

HubotでVSTSREST APIを叩き、拾ってきた情報をRocket.Chatに流す。

HubotとRocket.Chatの連携

Rocket.ChatもRocket.Chat連携用のHubotもコンテナが用意されてるのでそれを使うと楽。 RocketChat/hubot-rocketchat: Rocket.Chat Hubot adapter

VSTSREST API

Pull Requestに関しては以下のページを参照。 Git Pull Requests | REST API Reference for Visual Studio Team Services and Team Foundation Server

APIに必要な認証

Basicのみ対応。
GitHubにあるRailsアプリをVSTSでビルドして、Azureにデプロイする with Docker - cougerの日記 を見て、Alternate authentication credentials を有効にする。
Personal Access Tokenに対応しているので、そちらを使おう。ScopeはCode(Read)のみでOKっぽい。
指定する場合はこんな感じになる。ユーザ名はなんでもOK、パスワードにPersonal Access Tokenを指定すれば良いようだ。

http://[なんでもOK]:[Personal Access Token]@hogefuga.visualstudio.com/....

Personal Access Tokenの有効期間は180日なのでご注意をば。設定で1年間にすることも可能。

Personal Access Token、有効期間について @kkamegawa さんにアドバイスもらいました。ありがとーございます。

できたもの

だんだん疲れてきたので、できたものをペタペタ。

最初はActiveなものを全部出して、その後は、新規作成したものだけ表示。
vsts show pr [Active|Completed|Abandoned] のコマンドをhubotに投げると指定したステータスのプルリクを全部表示。
あと、チャーハン。

ごちゃごちゃしているのはなんとかならないものかなぁ。

Hubotスクリプト

Personal Access Tokenを使うように変更。

# Description:
#   VSTSのPull Requestが作成されたらメッセージを投下します
#
# Configuration
#   vsts... の変数を適宜変更してください
#
# Commands:
#   hubot vsts show pr [Active|Completed|Abandoned] - 指定したステータスのPull Requestを全部表示
#   hubot チャーハン - チャーハン作ります

cronJob = require('cron').CronJob
startDate = new Date

vstsAccount = "[VSTSアカウント名]"
vstsProject = "[VSTSプロジェクト名]"
vstsRepository = "[VSTSリポジトリ名]"
vstsPersonalAccessToken = "[VSTS Personal Access Token]"
vstsRemoteUrlBase4PR = "https://#{vstsAccount}.visualstudio.com/DefaultCollection/#{vstsProject}/_git/#{vstsRepository}/pullrequest"
vstsRestApiUrlBase4PR = "https://hubotvstsnotifier:#{vstsPersonalAccessToken}@#{vstsAccount}.visualstudio.com/defaultcollection/#{vstsProject}/_apis/git/repositories/#{vstsRepository}/pullrequests\?api-version\=1.0"

module.exports = (robot) ->
  createRestApiUrl4PR = (status, top) ->
    url = vstsRestApiUrlBase4PR
    if status?
      url = "#{url}\&status\=#{status}"
    if top?
      url = "#{url}\&$top=#{top}"
    url

  searchPullRequests = (status, top, callBack) ->
    url = createRestApiUrl4PR status, top
    robot.http(url)
      .get() (err, res, body) ->
        responseJson = JSON.parse body
        callBack responseJson.value

  createPRMessage = (pullReq) ->
    prUrl = "#{vstsRemoteUrlBase4PR}/#{pullReq.pullRequestId}"
    "[#{pullReq.pullRequestId}](#{prUrl}) #{pullReq.title} (#{pullReq.sourceRefName} -> #{pullReq.targetRefName})"

  showPullRequests = (status, top) ->
    callBack = (pullReqs) ->
      prMsgs = for pullReq in pullReqs
        createPRMessage pullReq
      if prMsgs.length is 0
        msg = "@all: プルリクエストはなかったよ"
      else
        msg = "@all: プルリクエストが#{prMsgs.length}個あるよ\n#{prMsgs.join('\n')}"
      robot.send {room: 'general'}, msg
    searchPullRequests status, top, callBack

  showRecentPullRequests = (status, top) ->
    callBack = (pullReqs) ->
      prMsgs = for pullReq in pullReqs
        creationDate = new Date pullReq.creationDate
        continue if startDate > creationDate
        createPRMessage pullReq
      startDate = new Date
      if prMsgs.length is 0
        return
      msg = "@all: 新しいプルリクエストが#{prMsgs.length}個あるよ\n#{prMsgs.join('\n')}"
      robot.send {room: 'general'}, msg
    searchPullRequests status, top, callBack

  robot.respond /vsts show pr (.*)/i, (msg) ->
    showPullRequests msg.match[1], null
    
  robot.respond /チャーハン/, (msg) ->
    msg.send """
```` <- 本当は3個
チャーハン作るよ!!
  ∧_∧
 (`・ω・)  。・゚・⌒)
 /  o━ヽニニフ))
 しーJ
```` <- 本当は3個
"""

  # 初回のみActiveなものを全部出す
  showPullRequests "Active"

  # 以降は新しいもののみ出す
  new cronJob('*/30 * * * * *', () ->
    showRecentPullRequests "Active"
  ).start()

実行環境

カレントディレクトリにscriptsフォルダを作成して、上記のCoffee Scriptを配置したあと docker-compose up -d でRocket.ChatとHubotが立ち上がり、VSTSの連携が始まります。

フォルダ構成

+ root
   + bot
       + Dockerfile
   + scrpts
      + hubot-vsts-notifier.coffee
   + docker-compose.yml

docker-compose.yml

chat:
  image: rocketchat/rocket.chat
  environment:
    - MONGO_URL=mongodb://db/rocketchat
  ports:
    - "3000:3000"
  links:
    - db

db:
  image: mongo
  ports:
    - 27017

bot:
  build: ./bot
  links:
    - chat
  environment:
    ROCKETCHAT_URL: http://chat:3000
    ROCKETCHAT_ROOM: ''
    LISTEN_ON_ALL_PUBLIC: true
    ROCKETCHAT_USER: [RocketChat認証ユーザ名]
    ROCKETCHAT_PASSWORD: [RocketChat認証パスワード]
    ROCKETCHAT_AUTH: password
    BOT_NAME: bot
  volumes:
    - ./scripts:/home/hubot/scripts

Dockerfile

FROM rocketchat/hubot-rocketchat

RUN npm install cron time

連携イメージ

f:id:couger:20160503182847p:plain