GitlabのIssueをSpreadSheetで集計しよう

この記事は [ユニークビジョン株式会社 Advent Calendar 2018](https://qiita.com/advent-calendar/2018/uniquevision) の 18 日目の記事です。

##はじめに
GitlabのIssueは増えてくると全体が見えにくいです。
そこで、GitlabのAPIとGASを使用して
>1.GitlabのIssueの一覧をSpreadSheetに出力する
2.SpreadSheetの作業一覧をまとめてIssue化して、作業リストを一元管理する

ということをやってみました。今回は上記の1についてご紹介します。


背景として、弊社のとあるプロジェクトでは開発を進めるに連れてIssueが増えてきたため、全体としてどのくらい作業があるのか見通しが悪くなっていました。Estimateを入力することで個々のIssueの作業量は管理できますが、**全体として誰がどれだけの作業を持っているのかということが見えづらい状況が発生**し、PMさんが困っておりました。
また、エンジニアとしてもSpreadSheet上に作成された開発タスクの一覧とGitlabのIssueに作業一覧が分散してしまい少し面倒な状況になっていました。


##GAS、SpreadSheetの良いところ
- 画面を作り込まなくて良い
- ブラウザから気軽に書きはじめて、少しずつ拡張していける
- プログラミングの知識がない人でも、マスターデータ（例えばIssue一覧）のシートからSpreadSheetの関数でいろいろできる
- 無料

既に進行中のプロジェクトであってもスピーディに必要なものを作れる、PM(プログラム書けない）でも関数でいろいろできるという点で今回はこの方法をやってみることにしました。

##準備
以下のものはあらかじめ用意しておいてください。

- Googleのアカウント
- Gitlabのアカウントとプロジェクト

### Gitlabでトークンを発行
GitlabのAPIを使用するため、認証する必要があります。方法はいくつかありますが、今回は[Personal access token](https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html)を使用します。

**※トークンの取扱にはご注意ください**

### GASプロジェクトを作成する
新しいSpreadSheetを開き、**ツール** ＞ **スクリプトエディタ** でエディタを開きます。
以下の通り無題のプロジェクトが作成され、`myFunction()`という空の関数が書かれた `コード.gs` というファイルが作られます。
<img width="444" alt="スクリーンショット 2018-12-15 午後2.48.47.png" src="https://qiita-image-store.s3.amazonaws.com/0/220881/38f1911b-8675-cf79-79f9-5550bd09f180.png">
プロジェクトの準備はこれでOKです。


##Issue一覧シートを作る
###GitlabからIssueの一覧を取得する
GitlabAPIの[issuesのエンドポイント](https://docs.gitlab.com/ee/api/issues.html)を使用します。
Issue取得したいプロジェクトのIDと、先ほど作成したトークンを使用して、URLはこんな感じ。

`https://gitlab.com/api/v4/projects/[プロジェクトのID]/issues?private_token=[トークン]`

GASから外部APIを叩く場合には[UrlFetchAppクラス](https://developers.google.com/apps-script/reference/url-fetch/url-fetch-app)を使用します。

```javascript
// GitlabからIssueをとってくる
var url = 'https://gitlab.com/api/v4/projects/[プロジェクトのID]/issues?private_token=[トークン]&per_page=100&page=1'
try {
  response = UrlFetchApp.fetch(url)
  var issues = JSON.parse(response.getContentText())
} catch(err) {
  Browser.msgBox(err)
  return
}  
```
デフォルトではIID降順で20件のIssueが取得されます。
それ以上ある場合はページを指定して続きを取得する必要があります。
（上記の例では100件（最大値）で1ページ目を取得しています。）

###取得したIssueをSpreadSheetに書き込む
SpreadSheetにデータを書き込む流れはおおよそ以下のとおりです。

1. データを2次元配列にする（表を作成するイメージ）
2. データを書き込む範囲を指定する
3. 書き込む

まず、Issueのオブジェクトを配列にします。JSONの中からSpreadSheetに出力したい値を適当に選んで、配列に詰め込みます。
1件のIssueにつき1つの配列にして、それをさらに配列にします。

```javascript
// Issueのオブジェクトを配列にする
issueArray = issues.map(function(issue){
  return [
    issue.iid,
    issue.title,
    issue.milestone ? issue.milestone.title : '',
    issue.labels ? issue.labels.join(',') : '',
    issue.assignee ? issue.assignee.name : '',
    issue.time_stats ? (issue.time_stats.time_estimate) / 60/60 : null,
    issue.due_date ? new Date(issue.due_date) : '',
    issue.state,
    new Date(issue.created_at),
    issue.closed_at ? new Date(issue.closed_at) : null,
    issue.web_url
  ]
})
```

配列ができたら、見出し表を追加してSpreadSheetに書き込みます。

```javascript
// 見出し行を作る
var headerRow = [
  'id', 'タイトル', 'マイルストーン', 'ラベル', '担当者', '工数（時間）', '期日', '状態', '作成日', '完了日', 'URL'
]
issueArray.unshift(headerRow)

// 書き込むSheetを用意する
var SHEET_NAME = 'Issue一覧'
var spreadSheet = SpreadsheetApp.getActiveSpreadsheet();
var sheet = spreadSheet.getSheetByName(SHEET_NAME)
if (sheet) {
  // - 全フェッチするので今あるデータは削除する
  sheet.clear()
} else {
  // - sheetがなければ作る
  sheet = spreadSheet.insertSheet(SHEET_NAME)
}
// - 左上のセルから範囲を指定して書き込む
var numRows = issueArray.length
var numColumns = headerRow.length
var range = sheet.getRange(1,1,numRows, numColumns)
range.setValues(issueArray)
```
詳細は割愛しますが、`getRange()`で範囲を指定して、`setValues()`で書き込みます。
この時、指定した範囲と書き込むデータの配列の大きさ（行数、列数）が合わないとエラーになります・・・・。

コードの全体は以下のとおりです。
`myFunction()`はそのままでも問題ありませんが、`makeIssueList()`にリネームしました。

```javascript:コード.gs
function makeIssueList() {
  // GitlabからIssueをとってくる
  var url = 'https://gitlab.com/api/v4/projects/[プロジェクトのID]/issues?private_token=[トークン]&per_page=100&page=1'
  
  try {
    response = UrlFetchApp.fetch(url)
    var issues = JSON.parse(response.getContentText())
  } catch(err) {
    Browser.msgBox(err)
    return
  }
  
  // Issueのオブジェクトを配列に変換する
  issueArray = issues.map(function(issue){
    return [
      issue.iid,
      issue.title,
      issue.milestone ? issue.milestone.title : '',
      issue.labels ? issue.labels.join(',') : '',
      issue.assignee ? issue.assignee.name : '',
      issue.time_stats ? (issue.time_stats.time_estimate) / 60/60 : 0,
      issue.due_date ? new Date(issue.due_date) : '',
      issue.state,
      new Date(issue.created_at),
      issue.closed_at ? new Date(issue.closed_at) : null,
      issue.web_url
    ]
  })  
  
  // 見出し行を作る
  var headerRow = [
    'id', 'タイトル', 'マイルストーン', 'ラベル', '担当者', '工数（時間）', '期日', '状態', '作成日', '完了日', 'URL'
  ]
  issueArray.unshift(headerRow)
  
  // Issueの配列をシートに書き込む
  var SHEET_NAME = 'Issue一覧'
  var spreadSheet = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = spreadSheet.getSheetByName(SHEET_NAME)
  if (sheet) {
    // - 全フェッチするので今あるデータは削除する
    sheet.clear()
  } else {
    // - sheetがなければ作る
    sheet = spreadSheet.insertSheet(SHEET_NAME)
  }

  // - 左上(1,1)のセルから範囲を指定
  var numRows = issueArray.length
  var numColumns = headerRow.length
  var range = sheet.getRange(1, 1, numRows, numColumns)

  // 書き込む
  range.setValues(issueArray)
}

```

スクリプトエディタの実行ボタンで`myFunction()`を実行するとIssue一覧というシートにGitlabのIssueの一覧が出力されます。
![スクリーンショット 2018-12-17 午後7.39.18.png](https://qiita-image-store.s3.amazonaws.com/0/220881/10755133-378e-3ccb-b829-048f5e27bc4f.png)

##集計シートを作る
Issue一覧のシートができたので、このシートのデータを集計してみます。
Issueのestimateをラベルごとに集計して、完了率を計算してみましょう。

```javascript:label.gs
function makeLabesSummary() {
  // 集計対象のラベルの一覧
  var labelNames = ['画面開発', 'API開発', 'TODO', 'バグ']
  
  labelArray = labelNames.map(function(name, index){
    var rowNum = index + 2
    return [
      name,
      '=COUNTIFS(\'Issue一覧\'!D:D,A' + rowNum + ',\'Issue一覧\'!H:H,"=opened")',
      '=COUNTIFS(\'Issue一覧\'!D:D,A' + rowNum + ',\'Issue一覧\'!H:H,"=closed")',
      '=SUMIFS(\'Issue一覧\'!F:F,\'Issue一覧\'!D:D,A' + rowNum + ',\'Issue一覧\'!H:H,"=opened")',
      '=SUMIFS(\'Issue一覧\'!F:F,\'Issue一覧\'!D:D,A' + rowNum + ',\'Issue一覧\'!H:H,"=closed")',
      '=E' + rowNum + '/(D' + rowNum + '+E' + rowNum + ')'
    ]
  })
  
  // 見出し行を作る
  var headerRow = [
    '名前', 'Opened件数', 'Closed件数', 'Opened工数（時間）', 'Closed工数（時間）', '完了率'
  ]
  labelArray.unshift(headerRow)
  
  // Labelの配列をシートに書き込む
  var SHEET_NAME = 'Label集計'
  var spreadSheet = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = spreadSheet.getSheetByName(SHEET_NAME)
  if (sheet) {
    // - 全フェッチするので今あるデータは削除する
    sheet.clear()
  } else {
    // - sheetがなければ作る
    sheet = spreadSheet.insertSheet(SHEET_NAME)
  }
  // - 左上のセルから範囲を指定して書き込む
  var numRows = labelArray.length
  var numColumns = headerRow.length
  var range = sheet.getRange(1,1,numRows, numColumns)
  range.setValues(labelArray)
}


```
上記のコードを実行すると「Label集計」シートが作成され、以下の内容が出力されます。
<img width="680" alt="スクリーンショット 2018-12-16 午後5.23.43.png" src="https://qiita-image-store.s3.amazonaws.com/0/220881/2b9436b6-753b-76af-9f6f-89cf5309c76b.png">


冒頭で触れた通り、この集計シートは**GUIから数式を入力することでも作成することができます**ね。
試しにOpened工数の列のセルにカーソルを合わせると、SpreadSheetの `SUMIFS` 関数が入力されているのがわかります。
ラベルが増えたらGUIから行をコピーするだけで集計対象を増やすことができます。
もちろん、GitlabAPIでラベル一覧を取得しても良いでしょう。実際、ラベルに紐付いたIssueの数はAPIでも取得することができます。

##自動化する
先ほど作成された集計シートは、参照先である「Issue一覧」のシートが更新されれば自動で内容が更新されます。
現状ですとスクリプトエディタからしかIssue一覧を更新できないので、トリガーを使用して自動化の設定を行います。
スクリプトエディタのメニューから　**編集**　＞　**現在のプロジェクトのトリガー**　＞　**トリガーを追加**
今回は下記のように、スプレッドシートを起動したときに`makeIssueList()`が実行されるようにします。
<img width="692" alt="スクリーンショット 2018-12-16 午後5.32.16.png" src="https://qiita-image-store.s3.amazonaws.com/0/220881/d21ca310-44c9-a8c3-9cde-af530ed579cb.png">
これで、いつでも最新の集計データを見ることができます。

##結び
SpreadSheetは今回とりあげたGitlabのIssueのような一覧性のあるものを取り扱うのに向いていると思います。とりあえずマスターデータのシートを作っておけば、プログラミングの知識がなくても備え付けの関数を使ってGUIからお手軽に集計シートやグラフなどを作成できます。BugラベルのIssueを集計して信頼度成長曲線を作成するなど、いろいろな集計シートが作れますね。