Hubotによるチーム開発の効率化

はじめまして。サーバーエンジニアの井上です。
今回は弊社の文化として浸透してきているCRについて、事例を交えて紹介させていただきます。

CRとは新しいチャレンジ(Challenge)とルーチンワーク(Routine)の言葉から作った造語です。また下記の
PrefabUtilityとEditorGUILayoutでViewクラス自動生成&自動アタッチする
CRハッカソンを開催しました!

でも紹介していますので参照してみて下さい。

1.改善項目を洗い出す

日々の業務のRoutineの中から改善項目を決めるにあたり、

  • 手動でやる必要がないこと
  • 依頼されることのうち、自分でなくてもできること
  • やり忘れやミスをする可能性があること

を軸に洗い出していきます。特に3つ目は他のメンバーにも影響があるので、効果は大きいです。

2.どのように改善するか

いくつか事例を紹介します。

Hubotをインストール





これで準備OK!
各種ツールをHubotを使って連携させていこうと思います!

チャットワークからデプロイ

経緯:サーバーエンジニアしかデプロイ作業が行えず、不在時などに対応できないことがあり、関係各所の作業に影響が出ていた
改善:チャットに特定の文言を投稿することで、チャットルームを監視しているbotがデプロイを実行してくれる。

→特定の部屋に限定することや、チャットからデプロイできる環境を限定することで、仮に問題が起きた場合でも影響を最小限に抑えられるようにしています。

  1. ルーム監視
  2. 「deploy」という文字列がそのルームに投稿されているか
  3. Jenkinsのデプロイジョブ実行
  4. http://hogehoge/jenkins/job/jobname/buildWithParameters?#{jenkinsParameters}

ChatWorkの通知内容

実装内容(Hubot)

module.exports = (robot) ->
  robot.hear /(\[To:123456\]*)/i, (msg) ->
    if job == "deploy"
      jenkinsParameters = "任意のパラメータ"
      path = "http://hogehoge/jenkins/job/jobname/buildWithParameters?#{jenkinsParameters}"
      req = msg.http(path)
      # Basic認証
      auth = new Buffer("user:password").toString('base64')
      req.headers Authorization: "Basic #{auth}"
      req.header('Content-Length', 0)
      req.post() (err, res, body) ->
        if err
          msg.reply "Jenkins says: #{err}"
        else if 200 <= res.statusCode < 400 # Or, not an error code.
          msg.reply "(#{res.statusCode}) Build started"
        else if 404 == res.statusCode
          msg.reply "Build not found, double check that it exists and is spelt correctly."
        else
          msg.reply "Jenkins says: Status #{res.statusCode} #{body}"
    else
      msg.send "わからないよー!"

JIRAの更新通知

経緯:不具合などのJIRAチケットを作成した際、チャットにそのJIRAのタイトル・URLなどを手動で入力していた。
改善:JIRAチケットを作成すると、特定のチャットルームに自動で投稿する。
→チケットの作成やステータスの変更を、手動で入力して共有する手間が省けた。

JIRA Webhook設定画面

ChatWorkの通知内容

実装内容 (Hubot)

JiraのWebhookをHubotで受け取り、ChatWork APIをたたく

module.exports = (robot) ->
  robot.router.post '/jira', (req, res) ->
    fields = req.body.issue.fields
    summary = fields.summary
    url = 'http://hogehoge/browse/' + req.body.issue.key
    message = summary + "\n" + url
    priority = fields.priority.name
    title = "(devil)オープン(優先度:#{priority})"
    chatworkNotification(robot, title, message)
    res.end()
chatworkNotification = (robot, title, message) ->
  notifyMessage = "[info][title]#{title}[/title]#{message}[/info]"
  robot
  .http("https://api.chatwork.com/v2/rooms/123456/messages")
  .header('Content-Type', 'application/x-www-form-urlencoded')
  .header('X-ChatWorkToken', "abcdefghijklmnopqrstuvwxyz")
  .post('body=' + notifyMessage) (err, res, body) ->

マージリクエストのタスク化と通知

経緯:マージリクエストを発行後、発行者がレビュー担当者にチャットでレビュー依頼を投げていた。
改善:マージリクエストを発行すると、自動で特定のチャットルームに、レビュー担当者に対してタスクを追加する
→タスクを発行する手間が省け、追加漏れが防げるようになりました

GitLab Webhook設定画面

実装内容(Hubot)

GitLabのMerge Request EventのWebhookを受け取り、ChatWork APIをたたく

module.exports = (robot) ->
  robot.router.post '/gitlab/webhook/opened', (req, res) ->
    json = req.body
    # ステータスOpenedかつ作成日時と更新日時が一致
    if json.object_attributes.state == "opened" and json.object_attributes.created_at == json.object_attributes.updated_at
      message = json.object_attributes.title + "\n" + json.object_attributes.url + "\n" + '[hr]' + json.object_attributes.description
      assigneeChatWorkId = getCharWorkId(json.assignee.username) # 担当者のチャットワークIDを取得
      chatworkTask(robot, assigneeChatWorkId, message)
    res.end()
chatworkTask = (robot, toId, message) ->
  body = "#{message}&to_ids=#{toId}"
  robot
  .http("https://api.chatwork.com/v2/rooms/123456/tasks")
  .header('Content-Type', 'application/x-www-form-urlencoded')
  .header('X-ChatWorkToken', "abcdefghijklmnopqrstuvwxyz")
  .post('body=' + body) (err, res, body) ->

JIRA自動更新

経緯:エンジニアがJIRAチケットを対応後、手動でJIRAのステータス・担当者・ラベルを編集していた。
改善:GitLabのマージリクエストの説明欄に、対応したJIRAのURLを記載。マージが実行されるとそのURLのJIRAチケットのステータスなどが自動で更新される。
→手動で更新する手間が省け、更新漏れが防げるようになりました。

※GitLab WebhookでMerge Request Eventを指定すると、そのマージリクエストが発行・更新・削除・実行されるたびにパラメータが送られてくるので、作成日時と更新日時を比較するなどして、hubot側でJIRAへのリクエストを止める必要があります。

GitLab Webhook設定画面

実装内容(Hubot)

GitLabのMerge Request EventのWebhookを受け取り、JIRA APIをたたく

module.exports = (robot) ->
  # /gitlab/webhook/mergedにPOSTされたらログを表示する
  robot.router.post '/gitlab/webhook/merged', (req, res) ->
    json = req.body
    if json.object_attributes.state == "merged"
      # マージリクエストの説明文
      description = json.object_attributes.description
      message = json.object_attributes.title + "\n" + json.object_attributes.url + '[hr]' + description
      sourceBranch = json.object_attributes.source_branch
      targetBranch = json.object_attributes.target_branch
      chatworkNotification(robot, sourceBranch, targetBranch, message)
      if description
        # チケット番号
        tickets = []
        for i, val of description.match(/HOGE\-\d+/g)
          # JIRAのチケット名を詰める
          tickets.push(val)
        if tickets.length > 0
          auth = new Buffer('user:password').toString('base64')
          projectName = json.object_attributes.source.name
          for i in [0..tickets.length-1]
            ticket = tickets[i]
            jiraStatusUpdateRequest(robot, ticket)
            sleep(1000)
            jiraOtherUpdateRequest(robot, auth, ticket)
    res.end()
chatworkNotification = (robot, sourceBranch, targetBranch, message) ->
  message = "[info][title]Merge #{sourceBranch} into #{targetBranch}[/title]#{message}[/info]"
  robot
  .http("https://api.chatwork.com/v2/rooms/123456/messages")
  .header('Content-Type', 'application/x-www-form-urlencoded')
  .header('X-ChatWorkToken', "abcdefghijklmnopqrstuvwxyz")
  .post('body=' + message) (err, res, body) ->
sleep = (ms) ->
  start = new Date().getTime()
  continue while new Date().getTime() - start < ms
jiraStatusUpdateRequest = (robot, ticket) ->
  updateAssigneeAndStatusUrl = "http://hogehoge/rest/api/2/issue/#{ticket}/transitions?expand=transitions.fields"
  updateAssigneeAndStatusParams = '{"fields":{"resolution":{"name":"Fixed"}},"transition":{"id":"5"}}'
  # ステータスと担当者変更
  robot
  .http("")
  .header('Content-Type', 'application/json')
  .auth('user', 'password')
  .post(updateAssigneeAndStatusParams) (err, res, body) ->
jiraOtherUpdateRequest = (robot, auth, ticket) ->
  searchParams = "{\"jql\":\"project=HOGE AND issue=#{ticket}\",\"startAt\":0,\"maxResults\":1}"
  robot
  .http('http://hogehoge/rest/api/2/search')
  .header('Content-Type', 'application/json')
  .header('Authorization', "Basic #{auth}")
  .post(searchParams) (err, res, body) ->
    if 200 <= res.statusCode < 400
      result = JSON.parse body
      title = result['issues'][0]['fields']['summary']
      assigneeName = result['issues'][0]['fields']['assignee']['name']
      reporterName = result['issues'][0]['fields']['reporter']['name']
      params = "{\"update\":{\"assignee\":[{\"set\":{\"name\":\"#{reporterName}\"}}]}}"
      url = "http://jira.applibot.co.jp/rest/api/2/issue/#{ticket}"
      # ラベル,担当者更新
      robot
      .http(url)
      .header('Content-Type', 'application/json')
      .auth('user', 'password')
      .put(params) (err, res, body) ->

3.まとめ

Hubotを導入してから、これまで手入力で対応していたことが自動化され、その時間を他のタスクに使えたり、入力漏れ等の人為的ミスを防げるなど、Routineは大幅に改善されました。

それによってできた時間で新たなChallengeもできるので、CRはプロジェクトにとっても個人にとってもいい文化だと思います。
改善には、「実装する」「フローを見直す」など様々なアプローチがあります。

実装して改善する必要はないので、今後もチームにあった方法で改善できればいいと思います。