Backlogの特定条件下にある処理済み課題棚卸しをPythonを使ってコマンド化してみた

Backlogの特定条件下での処理済み課題の棚卸しをスクリプトにてコマンドにしてみました。Backlog APIをPythonのクライアントライブラリ経由で操作しています。
2018.10.16

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

Backlogには「まとめて操作」という便利なメニューが存在します。登録日や登録者等の絞り込みを行い、該当した複数の課題をまとめて更新できる機能です。

ただ、以下のような複雑な要件となると「まとめて操作」による手順が複数回に及び、見落としが発生する可能性も高くなります。

  1. 最終更新日が指定日以前特定のスタッフ以外が担当処理済み扱いとなっている課題全てに対して
  2. 特定のスタッフによる操作特定のコメントを入れた後完了扱いにする

該当条件下にある課題に対して全て同じ操作を行う前提で、Backlog APIを経由することでまとめて操作を行うスクリプトを作ってみました。

pybacklogの導入

Python製クライアントライブラリ pybacklog を利用しました。今回の実行環境はPython 3系です。

% python -V
Python 3.6.5

% pip install pybacklog

最低限必要と思われるインターフェイスは実装されています。万が一pybacklogでサポートされていないAPIリクエストを行う場合も、doメソッド経由でカバーすることが可能です。

client = BacklogClient(space_name, api_key)
client.do("patch", "issues/{}".format(issue["issueKey"]), query_params={"statusId":4})

担当者の絞り込みはAPIクエリパラメータへの assigneeId[] 指定では行いませんでした。

  • Backlogのユーザ名が指定ルール下での運用になっており、基本的に変更が発生しない
  • 万が一変更が発生しても、変更の発生及び変更後の表記が比較的認識しやすい

という前提で、レスポンス内のassignee.nameを担当者の判定に用いています。

Backlog APIのAPIKEY発行

実行にはBacklogのAPIKEY発行が必須となります。

APIの設定 - backlog.com

呼び出し方と実装

スクリプト内に各種パラメータを埋め込まずに引数として渡します。使用頻度が高い場合はエイリアスとして設定するのも手です。

以下のパラメータは指定が必須となります。

  • space: スペース名
  • apikey: APIKEY
  • project: プロジェクト名
% python main.py --space ZZZ --apikey XXX --project YYY --ignore_member_names A,B,C,D
対象期間:最終更新日がYYYY-MM-DD以前
自動完了チケット数:1
id, issueKey, summary, url
IIII,YYY-1,○○○○○,https://zzz.backlog.jp/view/YYY-1

dryrunオプションが渡されると、コメントの追加と課題のクローズが実施されません。主に動作確認で利用します。

% python main.py --dryrun --space ZZZ --apikey XXX --project YYY --ignore_member_names A,B,C,D

実行時にリダイレクトを行うことで、CSVファイルとして結果を保存することも可能です。

% python main.py --space ZZZ --apikey XXX --project YYY > result.csv

main.py

#-*- coding: utf-8 -*-
from pybacklog import BacklogClient
from datetime import datetime, timedelta
from argparse import ArgumentParser
import re

def batch_exec(space, apikey, project, border_days, ignore_person_names="", dryrun=False):
    ignore_persons_name_list = ignore_person_names.split(",")
    client = BacklogClient(space, apikey)
    project_id = client.get_project_id(project)
    issues = _get_issues(client, project_id, border_days)
    target_issues = []
    for issue in issues:
        if ("assignee" not in issue) or (issue["assignee"] is None):
            continue
        if ("name" not in issue["assignee"]) or (issue["assignee"]["name"] is None):
            continue
        if _is_equal_arg_and_issue(issue["assignee"]["name"], ignore_persons_name_list):
            continue
        target_issues.append(issue)
        if dryrun == False:
            print("work at:{}".format(issue["issueKey"]))
            client.add_issue_comment(issue["issueKey"], u"一定期間経ちましたのでクローズします")
            client.do("patch", "issues/{}".format(issue["issueKey"]), request_params={"statusId":4})

        print("自動完了チケット数:{}".format(len(target_issues)))
        if len(target_issues) > 0:
            print("id, issueKey, summary, url")
            for issue in target_issues:
                print("{id},{issueKey},{summary},https://{space}.backlog.jp/view/{issueKey}".format(
                    id = issue["id"], issueKey = issue["issueKey"], summary = issue["summary"], space = space))

def _is_equal_arg_and_issue(issue_name, arg_names):
    prog = re.compile(issue_name.rstrip())
    for name in arg_names:
        if prog.match(name.rstrip()):
            return True
    return False

# 対象の課題取得
def _get_issues(client, project_id, border_days):
    date_str = (datetime.now() - timedelta(border_days)).strftime("%Y-%m-%d")
    print ("対象期間:最終更新日が{}以前".format(date_str))
    return client.issues({
        "projectId[]":[project_id], "updatedUntil": date_str, "statusId[]": 3, "sort": "dueDate", "count": 100
    })

if __name__ == '__main__':
    parser = ArgumentParser()
    parser.add_argument("--space", type=str, help="Backlog space", required=True)
    parser.add_argument("--apikey", type=str, help="Backlog api key", required=True)
    parser.add_argument("--project", type=str, help="Backlog project id", required=True)
    parser.add_argument("--border", type=int, help="last update border days", default=7)
    parser.add_argument("--ignore_member_names", default="", type=str, help="Not target person in charge")
    parser.add_argument("--dryrun", action='store_true', help="Not target person in charge")
    args = parser.parse_args()
    batch_exec(args.space, args.apikey, args.project, args.border, args.ignore_member_names, args.dryrun)

Backlogは操作時に迷うことがない作りになっています。とはいえ、操作する課題が多くなると見落としの可能性も高まります。もれなく確実にこなすためにはAPIの併用も有効な一手です。