Jira APIと戯れる 〜チケット情報取得編〜
eiryu
はじめに
エンジニアのeiryuと申します。
みなさんはJiraを使っていますか?
JiraはAtlassian社が提供しているプロジェクト・課題管理のWebサービスです。
JiraはAPIを提供しており、APIを扱えるようになると出来ることの幅が広がります。
私は今までAPIを利用して以下のようなことを行ってきました。
- チケット情報取得
- 特定条件のチケットを自動クローズ
- チケットの添付ファイルの一括ダウンロード
今回はチケット情報取得について書いてみたいと思います。
環境情報
この記事の内容は以下の環境にて確認しています。
- Jira v8.5.1(オンプレミス)
$ http --version
2.2.0
$ groovy -v
Groovy Version: 2.5.0 JVM: 1.8.0_181 Vendor: Azul Systems, Inc. OS: Mac OS X
実際にやってみる
今回やることを具体的に書くと「特定のプロジェクトのチケット情報を取得する」です。
実際の業務では、ユーザーからの問い合わせがあるとJiraが起票されるので、その件数の推移を見るために行っていました。結果をDBに保存して、日、週、月、四半期、半期、年について前のもの、前年のものとの比較を出力していました。(例: 前日比、前年同日比)
まずはドキュメントを見ながら軽くJira APIを触ってみましょう。今回はすごいcurlことHTTPieを利用しています。JSONのリクエストもしやすく、レスポンスのJSONも自動でフォーマット、色付けもされるためとても便利です。
ログインしてSearch APIを叩くと以下のようなレスポンスが返ってきます。
$ http --session=me https://$JIRA_HOST/rest/auth/1/session username=$JIRA_USERNAME password=$JIRA_PASSWORD
$ http --session=me https://$JIRA_HOST/rest/api/2/search?jql=project%20%3D%20{PROJECT_KEY}
...
{
"issues": [
...
],
"maxResults": 50,
"startAt": 0,
"total": 3541
}
Search APIはJQLをクエリストリングとしてパラメータに取るのですが、このJQLとはJira Query Languageの略で、SQLに近い構文で検索条件を指定することが出来ます。
JQLについては、基本的にはJiraのWebのチケット検索画面で実際に検索したときにURLに出ているものをそのまま使うことが出来ます。ですので、まずはWebで検索して出てきたJQLをコピーして使うのが楽です。
さて、レスポンスをよく見ると、Search APIで返ってくるissue情報は基本的なものしかないので、その後Issue APIにて完全な情報を取得する必要があることが分かりました。
今度はIssue APIを実際に叩いてみます。
$ http --session=me https://$JIRA_HOST/rest/api/2/issue/{ISSUE_KEY}
...
{
"expand": "renderedFields,names,schema,operations,editmeta,changelog,versionedRepresentations",
"fields": {
...
},
"id": "***",
"key": "{ISSUE_KEY}",
"self": "https://***"
}
ここまでで、特定のプロジェクトのチケット情報を取得しようと思うと、
- ログインして
- 特定のプロジェクトの全てのチケットのキー(issue key)を取得して
- チケットのキーを元にチケット情報を取得する
というような流れになることが分かりました。
2, 3については並列化出来るので、ここを工夫しつつコードに起こすと以下のようになります。例によってGroovyです。
@Grab('com.squareup.okhttp3:okhttp:3.14.9')
@Grab('com.squareup.okhttp3:logging-interceptor:3.14.9')
@Grab('com.squareup.okhttp3:okhttp-urlconnection:3.14.9')
import groovy.json.JsonBuilder
import groovy.json.JsonSlurper
import groovyx.gpars.GParsPool
import okhttp3.JavaNetCookieJar
import okhttp3.MediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody
import okhttp3.logging.HttpLoggingInterceptor
import java.util.concurrent.TimeUnit
class Config {
static PROXY_HOST = System.getenv()['PROXY_HOST']
static PROXY_PORT = System.getenv()['PROXY_PORT']
static JIRA_USERNAME = System.getenv()['JIRA_USERNAME']
static JIRA_PASSWORD = System.getenv()['JIRA_PASSWORD']
static JIRA_HOST = System.getenv()['JIRA_HOST']
static JIRA_JQL = System.getenv()['JIRA_JQL']
static MAX_RESULTS = 100
}
def httpLoggingInterceptor = new HttpLoggingInterceptor()
httpLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY)
OkHttpClient.Builder okHttpBuilder = new OkHttpClient().newBuilder()
// .addInterceptor(httpLoggingInterceptor) // 開発時のデバッグの際は設定する
if (Config.PROXY_HOST) {
Proxy proxy= new Proxy(Proxy.Type.HTTP, new InetSocketAddress(Config.PROXY_HOST, Config.PROXY_PORT.toInteger()))
okHttpBuilder.proxy(proxy)
}
def cookieManager = new CookieManager()
cookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ALL)
OkHttpClient client = okHttpBuilder
.readTimeout(30, TimeUnit.SECONDS)
.cookieJar(new JavaNetCookieJar(cookieManager))
.build()
def credentialJson = new JsonBuilder([username: Config.JIRA_USERNAME, password: Config.JIRA_PASSWORD]).toString()
// ログイン
RequestBody requestBodyOfLogin = RequestBody.create(MediaType.parse("application/json"), credentialJson)
Request requestOfLogin = new Request.Builder()
.url("https://${Config.JIRA_HOST}/rest/auth/1/session")
.post(requestBodyOfLogin)
.build()
client.newCall(requestOfLogin).execute().close()
// ページングのための情報を得る
def requestOfSearchIssues = new Request.Builder()
.url("https://${Config.JIRA_HOST}/rest/api/2/search?jql=${Config.JIRA_JQL}")
.build()
def responseOfSearchIssues = client.newCall(requestOfSearchIssues).execute()
def searchResult = new JsonSlurper().parseText(responseOfSearchIssues.body().string())
def basicIssues = []
def pageCount = (searchResult.total / Config.MAX_RESULTS).toInteger() + (searchResult.total % Config.MAX_RESULTS == 0 ? 0 : 1).toInteger()
println "total pageCount: ${pageCount}"
// ページング情報から並列で結果を取得
GParsPool.withPool {
if (pageCount > 0) {
basicIssues = (1..pageCount).collectManyParallel { pageNumber ->
println "[parallel] search pageNumber: ${pageNumber}"
def request = new Request.Builder()
.url("https://${Config.JIRA_HOST}/rest/api/2/search?startAt=${Config.MAX_RESULTS * (pageNumber - 1)}&maxResults=${Config.MAX_RESULTS}&jql=${Config.JIRA_JQL}")
.build()
client.newCall(request).execute().withCloseable {
def result = new JsonSlurper().parseText(it.body().string())
result.issues
}
}
println basicIssues.size()
}
basicIssues.collectParallel {
println "[parallel] getIssue issueKey: ${it.key}"
def request = new Request.Builder()
.url("https://${Config.JIRA_HOST}/rest/api/2/issue/${it.key}")
.build()
client.newCall(request).execute().withCloseable {
def issue = new JsonSlurper().parseText(it.body().string())
// データベース等に保存する処理を書く
}
}
}
おわりに
いかがでしたか?
今後も不定期ですがJira APIの情報を発信していきたいと思いますので、よろしくお願い申し上げます。
参考文献
- https://docs.atlassian.com/software/jira/docs/api/REST/7.6.1/
- https://ecosystem.atlassian.net/wiki/spaces/JRJC/overview
- /assets/rmp/techblog_bucket/server-side/happy-groovy-life/
- https://qiita.com/eiryu/items/eb10153408a21ce4fece
- https://stackoverflow.com/questions/24263921/how-to-implement-cookie-handling-on-android-using-okhttp