post Image
FeedbackManでJIRAチケットを作りたかったのでやってみた

はじめに

こんにちは、nirazoです。ホットペッパービューティのアプリの中の人です。

先日、弊社リポジトリでFeedbackManというライブラリを公開しました(not宣伝)。

capture.gif
↑こんなやつです

アプリに組み込むと簡単にキャプチャを撮ってSlackにフィードバックが送信できるという素敵ライブラリなのですが、使ってみて思いました。
「普段バグチケットはJIRAで管理してるし、明らかにバグだったらさっさとチケットを作ってしまいたい…」

ということで、FeedbackManを拡張して簡単にJIRAチケットが切れるようにしてみました。

やりたいこと

上のgifのdebugボタンを押した際に出てくるModalから、スクリーンキャプチャを添付したJIRAチケットを作ります。

スクリーンショット 2017-12-15 2.37.01.png
↑こんなチケットを作る

どうやるのか

JIRAには公開されているAPIが存在するのでこちらを使います。
ちなみに上記リンクはJIRA CloudのAPIで、JIRA ServerのAPIは別途こちらにあります。
差分はちゃんと見てないので宿題ということで…

作ってみた

事前準備

JIRAプロジェクトが無ければ始まらないので、プロジェクトを作成しておきます。
SwiftJiraSampleなどという名前にしたらプロジェクトキーが「SWIF」という残念な感じになりました。
スクリーンショット 2017-12-15 2.29.26.png

チケットを切る

まずはシンプルにチケットを切るところです。
Issueを切るためのAPIはこちら
チケットのタイトル(summary)、説明文(description)、報告者(reporter)などMustなものから、見積もり時間等(timetracking)、期限(duedate)など、あらゆる項目を設定できます。
ただしファイルを添付した状態でいきなりチケット作成はできないようなので、いったんシンプルにチケット作成だけ行います。

APIの準備

FeedbackManにはAPIプロトコルが用意されているので、JIRA用のAPIをenumで作ります。
チケット作成用のcreateIssueとファイル添付用のattachFileを用意しています。

API.swift
protocol API {
    var buildURL: String { get }
    var baseURL: String { get }
    var path: String { get }
    var parameters: [String: Any] { get }
    var body: Data { get }
}

enum JiraAPI: API {

    case createIssue()
    case attachFile(issueKey: String, image: UIImage)

    static let host = JiraManager.JiraConstants.host
    static let projectID = JiraManager.JiraConstants.projectID
    var buildURL: String {
        return "\(baseURL)\(path)"
    }

    var baseURL: String {
        return "https://\(JiraAPI.host)"
    }

    var path: String {
        switch self {
        case .createIssue(): return "/rest/api/2/issue"
        case .attachFile(issueKey: let issueKey, _): return "/rest/api/2/issue/\(issueKey)/attachments"
        }
    }

    var parameters: [String: Any] {
        switch self {
        case .createIssue(): return [:]
        case .attachFile(_, image: let image):
            let params: [String: Any] = ["file": image]
            return params
        }
    }

    var body: Data {
        var project = [String: String]()
        project["id"] = JiraAPI.projectID
        var issueType = [String: String]()
        issueType["id"] = "10004"

        var body = [String: Any]()
        body["project"] = project
        body["summary"] = "Sample bug"
        body["description"] = "バグが発生しました!"
        body["issuetype"] = issueType

        var fields = [String: Any]()
        fields["fields"] = body

        let jsonData = try! JSONSerialization.data(withJSONObject: fields, options: [])
        return jsonData
    }
}

projectIDやissueTypeがわからない!というときはこちらの記事を参考にすると良いかと思います。
body部分はAPIドキュメントのEXAMPLEを参考にして、Dictionaryを上手く組み立ててJSONの形にします。

APIクライアントの準備

APIClient.swift
static func createJiraIssue(api:JiraAPI, completion: @escaping (Data?, URLResponse?, Error?) -> Void) {
        let url = URL(string: api.buildURL)
        var request = URLRequest(url: url!)
        request.httpMethod = "POST"
        let login = "{email}:{pass}".data(using: .utf8)
        let base64Login = login!.base64EncodedString(options: [])
        request.addValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type")
        request.addValue("application/json", forHTTPHeaderField: "Accept")
        request.addValue("Basic \(base64Login)", forHTTPHeaderField: "Authorization")
        request.httpBody = api.body
        let task = URLSession.shared.dataTask(with: request, completionHandler: completion)
        task.resume()
    }

特段変わったことはしてないですね。
Content-Type等を適切に指定し、bodyにJSON(Data)を渡すだけです。
こいつを呼んであげれば、チケットが作成されます。

func createJiraIssue(completion: @escaping (Result<Bool, APIError>) -> Void) {
        APIClient.createJiraIssue(api: JiraAPI.createIssue(), completion: { data, response, error in
            if let data = data, let response = response {
                completion(.success(true))
            } else {
                completion(.failure(.other))
            }
        })
    }

スクリーンショット 2017-12-15 19.20.33.png
シンプルなチケットが作成されました。
簡単ですね!

チケットに画像を添付する

チケット自体は作成できたのですが、キャプチャ画像がないと何のこっちゃわかりません。というわけでキャプチャを貼りましょう。

やり方としては、チケット作成のコールバック内で、作成したチケットのkeyを取得してそのまま画像添付用のAPIをコールします。

func attachImageToIssue(issueKey: String, image: UIImage, completion: @escaping (Result<Bool, APIError>) -> Void) {
    let jiraApi = JiraAPI.attachFile(issueKey: issueKey, image: image)

    APIClient.multipartPost(api: jiraApi, completion:
        { data, response, error in
            if let _ = data, let response = response {
                completion(.success(true))
            } else {
                completion(.failure(.other))
            }
    }
    )
}

画像添付用のメソッドです。
画像を送信することになるので、multipartで送ります。

    static func multipartPost(api: API, completion: @escaping (Data?, URLResponse?, Error?) -> Void) {

        let url = URL(string: api.buildURL)
        let uniqueId = ProcessInfo.processInfo.globallyUniqueString
        let boundary = "---\(uniqueId)" //値を長くしすぎないように!
        var request = URLRequest(url: url!)
        request.httpMethod = "POST"
        let login = "\(JiraManager.JiraConstants.mail):\(JiraManager.JiraConstants.pass)".data(using: .utf8)
        let base64Login = login!.base64EncodedString(options: [])
        request.addValue("no-check", forHTTPHeaderField: "X-Atlassian-Token")
        request.addValue("Basic \(base64Login)", forHTTPHeaderField: "Authorization")
        request.addValue("application/json", forHTTPHeaderField: "Accept")
        request.addValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
        request.httpBody = createBody(parameters: api.parameters, boundary: boundary)
        let task = URLSession.shared.dataTask(with: request, completionHandler: completion)
        task.resume()
    }

ここで1個ハマったこととして、Content-Typeヘッダの値を長くしすぎるとAPIのレスポンスで何故か500が返ってくるため、長くしすぎないように注意して下さい(FeedbackManの元々のコードのままでは送れなかった…)。

チケット作成リクエストのリクエストのコールバックでissueKeyを取得し、そのチケットに画像ファイルを添付するコードが下記です。

func createJiraIssue(image: UIImage?, completion: @escaping (Result<Bool, APIError>) -> Void) {
    APIClient.createJiraIssue(api: JiraAPI.createIssue(), completion: { [weak self] data, response, error in
        if let data = data, let response = response {
            do {
                let json = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.allowFragments) as? [String: Any]
                if let json = json, let key = json["key"] as? String, let image = image {
                    self?.attachImageToIssue(issueKey: key, image: image) { result in
                        DispatchQueue.main.async {
                            switch result {
                            case .success:
                                completion(.success(true))
                            case .failure:
                                completion(.failure(.other))
                            }
                        }
                    }
                }
            } catch {
                completion(.failure(.other))
            }
            completion(.success(true))
        } else {
            completion(.failure(.other))
        }
    })
}

ネストが深くなってしまっているのはご容赦下さい。
これで、FeedbackManから画像ファイル付きのバグチケット作成ができました!

FeedbackJira.gif

最後に

こうやって簡単にJIRAチケットが作れると便利ですね!JIRAチケットだけでなく、他にも様々なプロジェクト管理ツールにも対応すると業務用途でもどんどん導入できるかと思います。
まだまだ荒いコードで本家FeedbackManにPull Requestを送ったりはしていないですが、ソースコードをgithubに置いています。興味のある方は見てみて下さい!


『 Swift 』Article List