SiG Staff Blog

福井と金沢にある株式会社SIG 総合研究所で働きたい方、ご連絡ください。

slack + hubot + Backlog Apiでプロジェクトの予定と実績の差をとる

色々あって便利ですよね、Backlog

Backlogいいね!

これまで、クラウドサービスの使用禁止という世間の流れから逆流した開発をさせられてて、

Redmineでタスク管理をしていたのですが、ようやくBacklogの使用許可がおりて使っております。

ticketも使えるし、wikiもあるし、Gitもあるしいいよね。社外でも見れるし。

Backlogよくないね!

Redmineと違って、サブプロジェクト作れない、チケットが親子までしか作れない、Gitのレビューコメントがちょいと見にくいとかあるんですね。

APIが微妙に使いにくいとか、webhookがちょっと微妙とか・・・。

でも、これらは無くても全然プロジェクト進行できるしいいんですよ。

困るのはサマリー系が全然ない事。予定・実績とかの管理が全然できないんです。

どうしたもんか・・・。

なければ作ればいいって偉い人が言ってるよ

Backlog API課題の情報を取得するのがあるやん。

プロジェクトの課題を全部取ってきて、自分でサマリーしてSlackに通知すればいいんじゃ?

ってことで、herokuにslack連携を済ませてあるhubot がいるので彼の仕事を増やしてあげる事にしました。

欲しいのはソースですよね・・・。

動かし方

必要な物は

  • Backlogのトーク
  • スペースID
  • プロジェクトの情報

です。これらを

  • api_key
  • space_id
  • project_list

にいい感じにセットしてあげてください。

で、botにメンションを付けて

@botName give me 【呼出用のプロジェクト名】

ってすれば動いてくれます。リクエスト投げまくるので結果が返ってくるまで少し時間かかります。

ってことで、ソース

ソース

少し長いです。

  robot.respond /give me[ ]?(.*)/i, (msg) ->

    #Backlogのトークンを設定する。個人設定から取得する
    api_key = "【トークンの設定】"

    #BacklogのスペースIDを設定する。https://[ここの部分].backlog.jp
    space_id = "【スペースIDの設定】"

    #プロジェクトのリストを取得する
    project_list = 
      "【呼出用のプロジェクト名】":
        name: "【画面表示用のプロジェクト名】"
        id: "【backlogのprojectのID】"
        start: 1
        end: 300

    target_project = msg.match[1]
    
    if not (target_project? and target_project of project_list)
      project_names = ""
      for key of project_list
        project_names += key + ", "
      msg.send "どのプロジェクトなの? -> #{project_names}"
      return -1
    
    target_data = project_list[target_project]
    target_name = target_data["name"]
    target_id = target_data["id"]
    target_start = target_data["start"]
    target_end = target_data["end"]

    if not (target_name? and target_id? and target_start? and target_end? and isFinite(target_start) and isFinite(target_end) and target_start < target_end)
      msg.send "プロジェクトの設定値を確認してください。 -> name: #{target_name}, id: #{target_id}, start: #{target_start}, end: #{target_end}"
      return -1

    #小数点以下を切り上げするためのfunction
    floatFormat = ( number, n ) ->
      _pow = Math.pow( 10 , n )
      Math.round( number * _pow ) / _pow

    #Backlog apiを使って課題の集計をするfunction
    # @param bl_project_id プロジェクトID (プロジェクト作成した時に指定する値。課題の前に付くあれ)
    # @param start 対象にするチケットの開始位置
    # @param end 対象にするチケットの終了位置
    call_backlog = (bl_project_id, start, end) ->
      #ループのサイズ(ループをStopさせる用)
      loop_sise = end
      #ループの開始位置
      loop_start = start
      #ループの終了位置
      loop_end = loop_start + loop_sise - 1
      #課題データ
      actual = {};
      #課題データを貯めるリスト
      actual_list = [];
      #作業位置
      finish_cnt = 0

      #プロジェクトIDを取得する
      back_log_url_project = "https://#{space_id}.backlog.jp/api/v2/projects/#{bl_project_id}?apiKey=#{api_key}"

      request = robot.http(back_log_url_project)
                     .get()
      request (err0, res0, body0) ->
        json0 = JSON.parse body0
        project_id = json0["id"]
        
        #課題の数を取得する。
        #課題の取得が非同期なのでうまく止まるようにチケットの最大値を取得する。
        #Promiseを使えればきっとうまくできるはずなんですが。
        back_log_url_count = "https://#{space_id}.backlog.jp/api/v2/issues/count?apiKey=#{api_key}&projectId[]=#{project_id}"

        request = robot.http(back_log_url_count)
                      .get()
        request (err1, res1, body1) ->
          json1 = JSON.parse body1
          
          #設定されている値がチケットの数よりも多いと、複数回サマリー結果が出てしまうので
          #チケットの数までループするように変更する
          ticket_max_size = json1["count"]
          if loop_end > ticket_max_size
            loop_end = ticket_max_size
            loop_sise = loop_end - loop_start + 1

          for i in [loop_start..loop_end]
            
            #課題を取得
            back_log_url = "https://#{space_id}.backlog.jp/api/v2/issues/#{bl_project_id}-#{i}?apiKey=#{api_key}"

            request = robot.http(back_log_url)
                          .get()
            request (err, res, body) ->

              #上手くパースされないので特殊文字を消す
              body_new = body.replace(/\\n/g, "")
                          .replace(/\\'/g, "'")
                          .replace(/\\"/g, '')
                          .replace(/\\&/g, "")
                          .replace(/\\r/g, "")
                          .replace(/\\t/g, "")
                          .replace(/\\b/g, "")
                          .replace(/\\f/g, "")
              json = JSON.parse body_new
              
              
              #エラーチェック
              if ("errors" of json)
                finish_cnt = 9999
                return 0
              else
                #値が入ってこない時があるので、デフォルト設定
                eh = if json["estimatedHours"]? then json["estimatedHours"] else 0
                ah = if json["actualHours"]? then json["actualHours"] else 0
                assignee = json["assignee"]

                name = if ( assignee? && "name" of assignee) then json["assignee"]["name"] else ".未設定 "

                actual = 
                  name: name
                  estimatedHours: eh
                  actualHours: ah
                  status_unopend: 0
                  status_opened: 0
                  status_treatmentted: 0
                  status_closed: 0

                #チケットの状態を保持する
                if json["status"]["id"] == 1
                  actual["status_unopend"] = 1
                else if json["status"]["id"] == 2
                  actual["status_opened"] = 1
                else if json["status"]["id"] == 3
                  actual["status_treatmentted"] = 1
                else if json["status"]["id"] == 4
                  actual["status_closed"] = 1

                #後でサマリーをしないと値が消えてしまうので
                actual_list.push(actual);

              #処理件数をincrement
              finish_cnt += 1

              #処置が全部終わったら出力する
              #非同期処理の中で1つかこの処理が走らない
              if finish_cnt >= loop_sise

                #ユーザー単位でサマリーする
                output = {}
                for actual, idx in actual_list
                  key_name = actual["name"]
                  if key_name of output
                    data = output[key_name]
                    data["estimatedHours"] += actual["estimatedHours"]
                    data["actualHours"] += actual["actualHours"]
                    data["status_unopend"] += actual["status_unopend"]
                    data["status_opened"] += actual["status_opened"]
                    data["status_treatmentted"] += actual["status_treatmentted"]
                    data["status_closed"] += actual["status_closed"]
                    output[key_name] = data

                  else
                    output[key_name] = 
                      estimatedHours: actual["estimatedHours"]
                      actualHours: actual["actualHours"]
                      status_unopend: actual["status_unopend"]
                      status_opened: actual["status_opened"]
                      status_treatmentted: actual["status_treatmentted"]
                      status_closed: actual["status_closed"]
                
                str = " *-------- #{target_name} プロジェクト実績 --------* \n"

                #出力をする
                for k,v of output
                  str += "   * #{k} *: #{v["actualHours"]}/#{v["estimatedHours"]} H( #{floatFormat(v["actualHours"] / 8, 2) } / #{floatFormat(v["estimatedHours"] / 8, 2)} 日) [ 未:#{v["status_unopend"]}, 中:#{v["status_opened"]}, 済:#{v["status_treatmentted"]}, 完:#{v["status_closed"]} ] \n"
                
                msg.send str

                return 0

    #指定したプロジェクトのサマリーを出力
    call_backlog(target_id, target_start, target_end)

    #ヘッダとしてだす
    msg.send "ちょっと待ってね♪"

出力内容

*-------- XXXX プロジェクト実績 --------*
>   ユーザーA: 0/30 H( 0 / 5 日) [ 未:10, 中:0, 済:1, 完:0 ] 
>   未設定 : 0/1074 H( 0 / 134.25 日) [ 未:109, 中:0, 済:0, 完:0 ] 
>   ユーザーB: 0/0 H( 0 / 0 日) [ 未:0, 中:1, 済:0, 完:0 ]

もうちょっと上手くできるんでは?

Promiseとか使えばコールバックのネストが無くなるんじゃ?

coffeeが1.6だったのでPromiseが使えなかったんです。 どうすれば使えるようになるんですかね・・・。

未設定のユーザーって一番上か下に出たほうが良いんじゃ

作った後に気が付いたんですが、ソートするのがめんどくさくて。。。 最後の出力の所でごにょごにょして対応しようと思ってます。

作ってみて

そいういえば、非同期で送信するんだった。ってので思ったより嵌りました。(Promise使えなかったし)

ちょっと遅いし、Backlogにちょっとした負荷がかかってしまうのが気になるんですが、統計が取れるようになったのは便利ですね~