Google Apps Scriptで複数リポジトリのissue情報を取得しシートと連携

# はじめに
昨年@hitomi_氏が書いていた[GoogleAppScript(GAS)でGithubとWBS スプレッドシートを同期](http://qiita.com/hitomi_/items/96564c070d523a04321b)を読んで、関わりのある他職種メンバーの予定も含めたスケジュールの管理がしやすそうだったため、僕の担当しているチームでも採用する流れになりました。

ただ、@hitomi_氏の記事では1リポジトリからしかissueが取得できません。
僕のチームは複数サービスを同時に運営している関係上、複数リポジトリからissue情報を取得するようコードに変更を加える必要がありその対応を行ってみました。

# 変更点
参考記事からの変更点は大まかに下記4点。

### configシートにカンマ区切りで複数リポジトリ名を登録
`org/hoge,org/fuga,org/piyo` のような形。
上記文字列を`split`で配列にし、forEachで処理してやるようにした。

```diff
- updateSeet(REPOSITORY);
+ REPOSITORYS.split(",").forEach(function(repo) {
+   updateSheet(repo);
+ });
```

### close済issueの行は削除させずカラムにcloseの旨を記入
削除すると、後でシートを見返した時に「あの件どうなったっけ？」となるような気がしました。issue見ればいいっちゃいいんですが、シートから移動して探してってのはちょっと面倒です。
そこで、ステータスの列を利用し「完了」の文字列を入れることで、フィルタかけたり条件付き書式設定ルールで色を変えてやったり、ソートができるようにしました。
非表示にすることも考えましたが、非表示にしたら開くの面倒だろうなぁ…

```diff
}else if(github_issue["state"] == "closed"){
- var raw = findRow(sheet, issue_num, ISSUE_NUMBERS_COL);
- if(raw > 0){ sheet.deleteRow(raw); }
+ updateClosedIssueStatus(UPDATE_SHEET, repo, issue_num, ISSUE_NUMBERS_COL);  
}
```

```javascript:updateClosedIssueStatus
function updateClosedIssueStatus (sheet, repo, issue_num, col) {
  var sheet_data  = sheet.getDataRange().getValues();
  var serviceName = getServiceNameByRepo(repo);
  var serviceNameColumn = col - 5;
  var issueNumColumn = col - 1;

  for(var i=1; i < sheet_data.length; i++) {
    if(sheet_data[i][serviceNameColumn] == serviceName && sheet_data[i][issueNumColumn] == issue_num) {
      var updatingRow = i + 1;
      var updatingStatusColumn = 'F' + updatingRow;
      sheet.getRange(updatingStatusColumn).setValue('完了');
      return;
    }
  }
  return;
}
```

### issue番号だけで判定を行わない
1つのシートで複数リポジトリの内容を管理すると、issue番号が被ってしまうことがあります。
サービス名+issue番号の条件であればユニークになるため、そのあたりのifの処理を変更しました。

### シートの行と列の構成が参考記事のものとは違うためそれに合わせ微妙にセットする値を調整
`ISSUE_NUMBERS_COL`や`START_ROW`の値など変えています。

# 実行後のシート
実行するとこんな感じのシートになります。
誰が作業するフェーズなのかが週次で見えやすくなりますし、自由に色々できます。
(どんなデータがほしいかはマーケターに整理してもらいました)
![スケジュールシートキャプチャ.png](https://qiita-image-store.s3.amazonaws.com/0/37036/e5a93e91-d64a-8094-a45c-b5515271f5ca.png)

# 実際のコード
下記が変更を加えた実際のコード。

```javascript
function updateIssues() {

  // configシートでtokenなどを管理
  var CONFIG_SHEET = getSheet('schedule_config');
  var GITHUB_OWNER = CONFIG_SHEET.getRange("B1").getValue();
  var REPOSITORYS  = CONFIG_SHEET.getRange("B2").getValue();
  var GITHUB_ACCESS_TOKEN = CONFIG_SHEET.getRange("B3").getValue();
  var NEW_ISSUE_NUMBER = CONFIG_SHEET.getRange("B4").getValue();
  var UPDATE_SHEET = getSheet('schedule_sheet');
  var ISSUE_NUMBERS_COL = 7; //issue番号を記載する列番号
  var START_ROW = 4;
  
  // リポジトリごとに処理
  REPOSITORYS.split(",").forEach(function(repo) {
    updateSheet(repo);
  });

  // スプレッドシート取得
  function getSheet(sheetName) {
    var spreadSheet = SpreadsheetApp.getActive()
    var sheet = spreadSheet.getSheetByName(sheetName);

    if (sheet == null) {
      spreadSheet.insertSheet(sheetName);
      sheet = spreadSheet.getSheetByName(sheetName);
    }
    return sheet;
  }

  // Github API issue情報取得
  function getGithubIssues(repo) {
    //取得したいisuueページ数※3は適当。そのPJTのissue更新頻度に合わせて
    var page_count = NEW_ISSUE_NUMBER ? Math.ceil(NEW_ISSUE_NUMBER / 30) : 3;
    
    var data = [];
    for (i = 1; i <= page_count; i++) {
      var base = 'https://api.github.com/repos/' + GITHUB_OWNER + '/' + repo + '/issues';
      var url = base + '?page=' + i +'&state=all&sort=created&direction=desc&access_token=' + GITHUB_ACCESS_TOKEN;

      var response = UrlFetchApp.fetch(url);
      var json = response.getContentText();
      Array.prototype.push.apply(data,JSON.parse(json));
    }
    return data;
  }

  // リポジトリ名からシートに記載するサービス名に変換
  function getServiceNameByRepo(repo) {
    if (repo === 'hoge') {
      return 'hoge is hoge';
    } else if (repo === 'fuga') {
      return 'fuga is fuga';
    } else if (repo === 'piyo') {
      return 'piyo is piyopiyo';
    }  
  }

  // 既にシートに記載があるissue番号の配列を返す
  function getSheetIssueNumbersArray(repo) {
    var needColumns = '5';
    var serviceNameColumn = '3';

    var numbers = []; 
    if (UPDATE_SHEET) {
      var lastRow = UPDATE_SHEET.getLastRow();
      var data = UPDATE_SHEET.getRange(START_ROW, serviceNameColumn, lastRow, needColumns);
      numData = data.getValues();

      // 該当リポジトリのissueのみ返す
      numData.forEach(function(val, i){
        if (val[0] == getServiceNameByRepo(repo)) {
          numbers.push(val[4])
        }        
      });
    }
    return numbers;
  }


  // ラベルから優先度の判断する文字のみ抽出
  function propertyFormatTxt(label_txt) {
    if (label_txt.match('優先度:高')) {
      return '高';
    } else if (label_txt.match('優先度:中')) {
      return '中';
    } else if (label_txt.match('優先度:低')) {
      return '低';
    } else {
      return '低';
    }
  };

  // issue情報を 行にあてはめるarrayに整形
  function issueRowFormatData(repo, issue) {

    var type = "";
    if(issue["html_url"]) {
      type = issue["html_url"].match('issues');
    }

    // issue以外は空でreturn
    if(type != 'issues'){
      return;
    }

    var createdUser = "";
    if (issue["user"]['login']) {
       createdUser = issue["user"]["login"];
    }

    var opend_at = "";
    if (issue["created_at"]) {
      opend_at = issue["created_at"].substring(5, 10);
      opend_at = opend_at.replace(/-/g, '/');
    }

    var number_link = '=HYPERLINK("' + issue["html_url"] + '","' + issue["number"] + '")';

    var serviceName = getServiceNameByRepo(repo);
    
    var labels = "";
    if (issue["labels"]) {
      labels = issue["labels"].map(function(label){
        return label["name"]
      }).join(",");
    }

    // 
    // チーム 担当者 サイト タスク概要 優先度 status URL
    return [
      '', // none
      createdUser, // 担当者
      serviceName, // サイト
      issue["title"], // issueタイトル
      propertyFormatTxt(labels), // 優先度
      '', // status
      number_link // URL
    ]
  }

  // close済issueの行のstatusを「完了」に変更
  function updateClosedIssueStatus (sheet, repo, issue_num, col) {
    var sheet_data = sheet.getDataRange().getValues();
    var serviceName = getServiceNameByRepo(repo);
    var serviceNameColumn = col - 5;
    var issueNumColumn = col - 1;
    
    for(var i=1; i < sheet_data.length; i++) {
      if(sheet_data[i][serviceNameColumn] == serviceName && sheet_data[i][issueNumColumn] == issue_num) {
        var updatingRow = i + 1;
        var updatingStatusColumn = 'F' + updatingRow;
        sheet.getRange(updatingStatusColumn).setValue('完了');
        return;
      }
    }
    return;
  }

  // 最新を最終行に追加&closeを削除
  function updateSheet(repo) {
    var sheet_issue_numbers = getSheetIssueNumbersArray(repo);

    var new_open_issues = getGithubIssues(repo).map(function(github_issue) {
      var issue_num = String(github_issue["number"]);
      var num_index = sheet_issue_numbers.indexOf(issue_num)

      // シートになかった時だけ追加用のdataを返す
      if(num_index == -1 && github_issue["state"] == "open") {
        return issueRowFormatData(repo, github_issue);

      // closeしていたら該当行のステータスを完了にする
      } else if (github_issue["state"] == "closed") {
        updateClosedIssueStatus(UPDATE_SHEET, repo, issue_num, ISSUE_NUMBERS_COL);
      }
    });

    // 追加データを最終行に追記
    new_open_issues.forEach(function(github_issue) {
      if (github_issue) {
        UPDATE_SHEET.appendRow(github_issue);
      }
    });
  }

}

// スプレッドシートのメニューに関数実行 を追加
function onOpen() {
  var spreadSheet = SpreadsheetApp.getActive();
  var items = [{name: 'sync_issues', functionName: 'updateIssues'}];
  spreadSheet.addMenu('MyScripts', items);
}
```

数年ぶりにGAS触ったけれど便利ですね。