##はじめに
こんにちは、nirazoです。ホットペッパービューティのアプリの中の人です。
先日、弊社リポジトリでFeedbackManというライブラリを公開しました(not宣伝)。
アプリに組み込むと簡単にキャプチャを撮ってSlackにフィードバックが送信できるという素敵ライブラリなのですが、使ってみて思いました。
「普段バグチケットはJIRAで管理してるし、明らかにバグだったらさっさとチケットを作ってしまいたい…」
ということで、FeedbackManを拡張して簡単にJIRAチケットが切れるようにしてみました。
やりたいこと
上のgifのdebugボタンを押した際に出てくるModalから、スクリーンキャプチャを添付したJIRAチケットを作ります。
↑こんなチケットを作るどうやるのか
JIRAには公開されているAPIが存在するのでこちらを使います。
ちなみに上記リンクはJIRA CloudのAPIで、JIRA ServerのAPIは別途こちらにあります。
差分はちゃんと見てないので宿題ということで…
作ってみた
事前準備
JIRAプロジェクトが無ければ始まらないので、プロジェクトを作成しておきます。
SwiftJiraSampleなどという名前にしたらプロジェクトキーが「SWIF」という残念な感じになりました。
チケットを切る
まずはシンプルにチケットを切るところです。
Issueを切るためのAPIはこちら。
チケットのタイトル(summary)、説明文(description)、報告者(reporter)などMustなものから、見積もり時間等(timetracking)、期限(duedate)など、あらゆる項目を設定できます。
ただしファイルを添付した状態でいきなりチケット作成はできないようなので、いったんシンプルにチケット作成だけ行います。
APIの準備
FeedbackManにはAPIプロトコルが用意されているので、JIRA用のAPIをenumで作ります。
チケット作成用のcreateIssueとファイル添付用のattachFileを用意しています。
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クライアントの準備
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))
}
})
}
チケットに画像を添付する
チケット自体は作成できたのですが、キャプチャ画像がないと何のこっちゃわかりません。というわけでキャプチャを貼りましょう。
やり方としては、チケット作成のコールバック内で、作成したチケットの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から画像ファイル付きのバグチケット作成ができました!
最後に
こうやって簡単にJIRAチケットが作れると便利ですね!JIRAチケットだけでなく、他にも様々なプロジェクト管理ツールにも対応すると業務用途でもどんどん導入できるかと思います。
まだまだ荒いコードで本家FeedbackManにPull Requestを送ったりはしていないですが、ソースコードをgithubに置いています。興味のある方は見てみて下さい!