こんにちは。インテグレーション・エンジニアの尾宇江(おうえ)です。
今回は、GoでJiraのチケットをエクスポートしてみた件について、テックブログに記載いたします。
はじめに
Yappliでは2週間に1度、エンジニアが自分たちが主体となってプロダクトや業務に関する改善に取り組む「YappdateDay*1」というイベントを実施しています。
2018年から開催してきたYappdateDayですが、今回100回目を迎えたので対応してきた成果を振り返ろうと考えました。
YappdateDayの成果を振り返ろうとして発生した問題
Yappli内部のチケット管理は、主にJiraを利用しており、YappdateDayで対応したチケットには、vol.1_180712
のように開催番号+開催年月日でラベルを付けておりました。
YappdateDayの全チケットを集計しようとしたのですが、Jiraのラベルは完全一致での検索しかできない*2ために、
labels in (vol.1_180712, vol.2_180726)
のように IN句で全部のラベルを繋いでいく必要があるということが判明しました。
解決方針
開催番号+開催年月日で作られているラベルを100個リストアップするのがかなり面倒だったために、いっそJiraのチケットをエクスポートしてしまおうと考えました。
今回はandygrunwald/go-jiraというGoでJiraを扱うパッケージを利用してチケット一覧をエクスポートしました。 github.com
コード
package main import ( "encoding/csv" "flag" "fmt" "os" "strconv" "strings" "time" "github.com/andygrunwald/go-jira" ) var ( baseURL = flag.String("baseURL", "", "baseUrl") username = flag.String("user", "", "user") token = flag.String("token", "", "API Token") jql = flag.String("jql", "", "JQL for search") yappdateDay = flag.Int("yappdateDay", 0, "count of YappdateDay") ) func init() { flag.Parse() } func main() { if err := Main(); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } } func Main() error { client, err := makeJiraClient() if err != nil { return err } return exportIssues(client) } // makeJiraClient は Jiraのクライアントを準備します func makeJiraClient() (*jira.Client, error) { tp := jira.BasicAuthTransport{ Username: *username, Password: *token, } return jira.NewClient(tp.Client(), *baseURL) } // exportIssues は 検索したチケット一覧をエクスポートします func exportIssues(client *jira.Client) error { // エクスポートするファイル名です fp, err := os.Create("./issues.csv") if err != nil { return err } defer fp.Close() // JQLで対象のチケット一覧を取得します issues, err := fetchIssues(client) if err != nil { return err } rows := [][]string{ titleRow(), } fmt.Println("exportIssues") for k, v := range issues { // エクスポートに結構時間がかかるので、進行状況を出力しておきます switch k % 100 { case 1: fmt.Printf("%6d\t", k) case 0: fmt.Println("") default: fmt.Printf(".") } // チケットの詳細を取得します i, err := fetchIssue(client, v.Key) if err != nil { return err } // チケットの内容をエクスポートしたいcsvに合わせて変換します row, err := toRowData(i) if err != nil { return fmt.Errorf("error occurred at %s. wrapped:%w", v.Key, err) } rows = append(rows, row) } w := csv.NewWriter(fp) return w.WriteAll(rows) } // titleRow は CSVのヘッダー行を準備します func titleRow() []string { ss := []string{"URL", "Summary", "Project,Status", "RelatedFeatures", "Created", "Updated", "Assignee", "Labels"} if *yappdateDay > 0 { ss = append(ss, "YappdateDay") } for i := 1; i <= *yappdateDay; i++ { ss = append(ss, fmt.Sprintf("vol.%d", i)) } return ss } // fetchIssues は JQLで対象のチケット一覧を取得します func fetchIssues(client *jira.Client) ([]jira.Issue, error) { fmt.Println("fetchIssues") last := 0 issues := []jira.Issue{} for { opt := &jira.SearchOptions{ MaxResults: 1000, StartAt: last, } chunk, resp, err := client.Issue.Search(*jql, opt) if err != nil { return nil, err } total := resp.Total if issues == nil { issues = make([]jira.Issue, 0, total) } issues = append(issues, chunk...) last = resp.StartAt + len(chunk) fmt.Printf("%6d/%6d\n", last, total) if last >= total { return issues, nil } } } // fetchIssue は チケットの詳細を取得します func fetchIssue(client *jira.Client, issueID string) (*jira.Issue, error) { i, _, err := client.Issue.Get(issueID, nil) if err != nil { return nil, err } return i, nil } // toRowData は チケットの内容をエクスポートしたいcsvに合わせて変換します func toRowData(i *jira.Issue) ([]string, error) { name := "Unassigned" if v := i.Fields.Assignee; v != nil { name = v.DisplayName } labels := strings.Join(i.Fields.Labels, ",") cf, _ := i.Fields.Unknowns.String("customfield_10948[0]value") // エクスポートする内容をチケット情報から抜き出す row := []string{} row = append(row, fmt.Sprintf("%sbrowse/%s", *baseURL, i.Key)) // チケットのURL row = append(row, strings.NewReplacer( "\r\n", "", "\r", "", "\n", "", ).Replace(i.Fields.Summary)) // チケットのタイトル 改行コードは除外 row = append(row, i.Fields.Project.Key) // プロジェクト row = append(row, i.Fields.Status.Name) // ステータス row = append(row, cf) // カスタムフィールド yappliの場合は関連機能 row = append(row, time.Time(i.Fields.Created).Format("2006-01-02")) // チケット作成日 row = append(row, time.Time(i.Fields.Updated).Format("2006-01-02")) // チケット更新日 row = append(row, strings.TrimSpace(name)) // 担当者名 row = append(row, labels) // ラベル一覧 row, err := appendYappdateDay(row, strings.Split(labels, ",")) // YappdateDayの対応状況 if err != nil { return nil, err } return row, nil } // appendYappdateDay は ラベル一覧の中からYappdateDayで対応したラベルをエクスポート用に取り出す func appendYappdateDay(row, labels []string) ([]string, error) { if *yappdateDay == 0 { return row, nil } ls := make([]string, *yappdateDay) i := 0 for _, v := range labels { s := strings.ToLower(v) if strings.Contains(s, "vol.") { tmp := strings.Split(strings.TrimPrefix(s, "vol."), "_") if len(tmp) > 1 { vol, err := strconv.Atoi(tmp[0]) if err != nil { return nil, err } if *yappdateDay >= vol { ls[vol-1] = "true" i++ } } } } row = append(row, fmt.Sprintf("%t", i > 0)) for _, v := range ls { row = append(row, v) } return row, nil }
実行方法
go run main.go -user={Jiraのユーザ名} -token={Jiraのトークン} -jql="order by created asc" -yappdateDay=100
解説
i.Fields.Unknowns.String("customfield_10948[0]value")
Jiraのカスタマフィールドを取得するコードです。
Yappliでは10948というIDには関連機能
というカスタムフィールドが設定されています。
カスタムフィールドのIDの調べ方は、JIRAカスタムフィールドIDの調べ方を参考にさせていただきましたIssueから取得できる情報について
もちろん今回のコードでは取得しなかった情報も取得できます。
取得する際の指定方法は、go-jiraのドキュメントのIssueFields が参考になります実行時に設定するtokenについて
https://id.atlassian.com/manage-profile/security/api-tokensから発行することができます
まとめ
あらためてYappdateDayのチケットを集計してみると、以下のような結果になっていました。
- YappdateDay全体で対応したチケット数は909枚
- プロダクトの機能改善関連のチケットに絞るとYappdateDayで対応したチケットは587枚
これはYappli全体の機能改善の20%以上!!
今回あらためて集計してみたことで、月に2回のイベントですが、継続することで大きな結果に繋がっていたと感じました。
また、YappdateDayを続けてきたことも一つの要因となって、エンジニアが自分たちが主体となって改善していく文化が根付き、色々なチャレンジも生まれていると感じています。
ちなみに、この記事で紹介したコードや、この記事自体もYappdateDayで取り組みました。
そんなYappliですが、一緒にYappliを成長させていてくれるエンジニアを募集中です!
「YappdateDayってちょっと気になった」といった方は、ぜひカジュアル面談でお話しましょう!