GitHubで自動更新のカンバン作ってみたい プルリクとプロジェクトのカードを連動させる編

# はじめに
諸事情により前回から9ヶ月以上間が空いてすみません。
前回はPull requestがどんなアクションに反応してどんなデータを送信するかについて確認してざっとまとめました。
[GitHubで自動更新のカンバン作ってみたい Pull requestイベント確認編 - Qiita](http://qiita.com/KanaSakaguchi/items/09a330cd361382b86c70)
今回はいよいよプルリク作られたらプロジェクトのカードを作成し、ラベルが変更されたらカードを移動するようにしたいと思います。

## この記事でやること
プルリク作られたらプロジェクトのカードを作成し、ラベルが変更されたりマージされたりしたらカードを移動するようにします。
最後にコード掲載＆動作サンプル用のリポジトリを用意しています。
(GitHubの仕様上そこで試せるのは一部のみです。)

# 準備
実装の前に仕様決めとプロジェクトの作成を行います。
(ここに書いている仕様はあくまでサンプルです。 実際に運用しているルールとは異なります。)

## ラベルで進捗整理
今回は以下のようなラベルでプルリクの進捗を管理している場合を考えます。

|ラベル|進捗|
|:--:|:--:|
|(なし)|[WIP]で実装中など|
|レビュー待ち|実装したので見てほしい|
|レビュー後改修中|レビューで指摘された点改修中|
|セルフテスト待ち|セルフテスト中|
|テスト待ち|テスト待ち|
|テスト後改修中|テストで指摘された点改修中|
|テスト済み|リリース待ち|

## プロジェクトとカラムを作成する
カンバンを意識して以下の7カラムを作成しました。

- 未分類
- 実装中
- レビュー待ち
- セルフテスト待ち
- テスト待ち
- リリース待ち
- リリース完了

## プルリクがどうなったらカードをどうするか考える
今回はプルリクに対して次のような操作があればカードを自動操作することにします。
(サンプルでは複数ラベルが貼られた場合やプルリクがクローズされた場合などは考えないこととします。)

|プルリク操作|カード操作|
|:--:|:--:|
|プルリク作成|[未分類]カラムにプルリクのカードを作成|
|タイトルに「[WIP]」をつけてでプルリク作成|[実装中]カラムにプルリクのカードを作成|
|[レビュー待ち]ラベルを貼る|[レビュー待ち]カラムにカードを移動|
|[レビュー後改修中]ラベルを貼る|[実装中]カラムにカードを移動|
|[セルフテスト待ち]ラベルを貼る|[セルフテスト]カラムにカードを移動|
|[テスト待ち]ラベルを貼る|[テスト待ち]カラムにカードを移動|
|[テスト済み]ラベルを貼る|[レビュー待ち]カラムにカードを移動|
|プルリクマージ|[完了]カラムにカードを移動|

# 実装
仕様が決まったので実装していきます。
以下のようにアクションごとに関数を呼び出して操作することにします。
(ここで紹介するコードは突っ込みどころがあるかと思いますが、一旦スルーでお願いします。)

```js:doPost.gs
function doPost(data) {
  data = JSON.parse(data.postData.contents);
  switch (data.action) {
    case 'opened':
      onOpened(data);
      break;
    case 'labeled':
      onLabeled(data);
      break;
    case 'closed':
      onClosed(data);
      break;
  }
}
```

## APIのトークンを取得してカラムのIDを調べる
以下の記事などをもとにAPIのトークンを取得してカラムのIDを調べておいてください。
[GitHubのProjects用API(試験運用中)を使ってみた - Qiita](http://qiita.com/KanaSakaguchi/items/fbd244af006dd746ef7e#github-api%E3%82%92%E4%BD%BF%E3%81%86%E3%81%9F%E3%82%81%E3%81%AE%E6%BA%96%E5%82%99)
そのままだとどれがどれだかわからなくなりやすいので、変数に入れておくといいでしょう。

```js:定数.gs
var UNCATEGORIZED; //「未分類」カラムのID;
var IN_IMPLEMENTATION; //「実装中」カラムのID;
var REVIEW_PENDING; //「レビュー待ち」カラムのID;
var SELF_TEST_PENDING; //「セルフテスト待ち」カラムのID;
var TEST_PENDING; //「テスト待ち」カラムのID;
var RELEASE_PENDING; //「リリース待ち」カラムのID;
var RELEASE_COMPLETE; //「リリース完了」カラムのID;
```

## プルリク作成でカード作成
プルリクが作成されたらプロジェクトにカードを作成します。
titleに`[WIP]`が含まれているかどうかは正規表現を使って調べます。
大文字小文字は問わないことにします。

```js:onOpened.gs
function onOpened(data) {
  if (data.pull_request.title.match(/\[WIP]/i)) {
    createCard(data.pull_request.id, IN_IMPLEMENTATION);
  } else {
    createCard(data.pull_request.id, UNCATEGORIZED);
  }
}
```

```js:createCard.gs
function createCard(pullRequestId, columnId) {
  var headers = {
    "Accept" : "application/vnd.github.inertia-preview+json"
  };
  var payload = {
    "content_id" : pullRequestId,
    "content_type" : "PullRequest"
  };
  var options = {
    "method" : "POST",
    "headers" : headers,
    "payload" : JSON.stringify(payload)
  };

  var url = "https://api.github.com/projects/columns/" + columnId + "/cards?access_token=取得したトークン";

  return JSON.parse(UrlFetchApp.fetch(url, options));
}
```

### やってみた
こんな感じで[WIP]でプルリクを作成すると実装中カラムにカードが作成されます。
![[WIP]でプルリク作る.gif](https://qiita-image-store.s3.amazonaws.com/0/76523/9e0f4205-eab3-2f57-25dd-aaa715a924ca.gif)

## プルリクIDからカードIDを調べる
他の場合はカードを移動するので、プルリクのIDからカードを特定する必要があります。
ですが、ProjectにプルリクIDからカードIDを調べるAPIは用意されていないので、カラムのカードを取得するAPIを使って自前で実装する必要があります。

```js:getCardId.gs
function getCardId(pullRequestId) {
  var columnIds = [UNCATEGORIZED, IN_IMPLEMENTATION, REVIEW_PENDING, SELF_TEST_PENDING, TEST_PENDING, RELEASE_PENDING, RELEASE_COMPLETE];
  for (var n = 0; n < columnIds.length; n++) {
    var cards = getCardList(columnIds[n]);
    for (var m = 0;m < cards.length; m ++) {
      if (cards[m].content_url.indexOf(pullRequestId) !== -1) {
        return cards[m].id;
      }
    }
  }
}

function getCardList(columnId) {
  var headers = {
    "Accept" : "application/vnd.github.inertia-preview+json"
  };
  var options = {
    "method" : "GET",
    "headers" : headers
  };
  var url = "https://api.github.com/projects/columns/" + columnId + "/cards?access_token=取得したトークン";
  return JSON.parse(UrlFetchApp.fetch(url, options));
}
```

## ラベル追加でカード移動
いよいよ自動更新です。
プルリクIDと移動先カラムのIDを引数で渡してプルリクのカードを移動させるようにします。

```js:onLabeled.gs
function onLabeled(data) {
  switch (data.label.name) {
    case "レビュー待ち":
      moveCard(data.number, REVIEW_PENDING);
      break;
    case "レビュー後改修中":
      moveCard(data.number, IN_IMPLEMENTATION);
      break;
    case "セルフテスト待ち":
      moveCard(data.number, SELF_TEST_PENDING);
      break;
    case "テスト待ち":
      moveCard(data.number, TEST_PENDING);
      break;
    case "テスト後改修中":
      moveCard(data.number, IN_IMPLEMENTATION);
      break;
    case "テスト済み":
      moveCard(data.number, REVIEW_PENDING);
      break;
  }
}
```
```js:moveCard.gs
function moveCard(pullRequestId, columnId) {
  var headers = {
    "Accept" : "application/vnd.github.inertia-preview+json"
  };
  var payload = JSON.stringify({
    "position" : "top",
    "column_id" : columnId
  });
  var options = {
    "method" : "POST",
    "headers" : headers,
    "payload" : payload
  };
  var cardId = getCardId(pullRequestId);
  var url = "https://api.github.com/projects/columns/cards/" + cardId + "/moves?access_token=取得したトークン";
  UrlFetchApp.fetch(url, options);
}
```

### やってみた
こんな感じで[セルフテスト待ち]ラベルを貼るとセルフテスト待ちカラムにカードが移動します。
![セルフテスト待ちラベルを貼る.gif](https://qiita-image-store.s3.amazonaws.com/0/76523/aa298543-5291-eac3-5854-95b3e66fe103.gif)

## プルリクマージでカード移動
最後にマージした時にリリース完了カラムにカードを移動します。
マージせずクローズした分に関しては今回は何もしないことにしていますが、運用によってはそれ用のカラムを用意してそこに移動する・カードを削除するといった実装をするといいでしょう。

```js:onClosed.gs
function onClosed(data) {
  if (!data.pull_request.merged) {
    return;
  }
  moveCard(data.number, RELEASE_COMPLETE);
}
```

### やってみた
こんな感じでプルリクをマージするとリリース完了カラムにカードが移動・・・しませんでした。
リファクタリングした時にコードミスってました。
修正後のコードであれば問題なく移動しますし、記事に載せたコードでも動くはずです。
(正しく動いている様子は[動作サンプル](https://qiita.com/KanaSakaguchi/items/2ba20632d66fe7a3587e0#%E3%82%B3%E3%83%BC%E3%83%89%E6%8E%B2%E8%BC%89%EF%BC%86%E5%8B%95%E4%BD%9C%E3%82%B5%E3%83%B3%E3%83%97%E3%83%AB)見てください。)
![マージしてカード移動に失敗.gif](https://qiita-image-store.s3.amazonaws.com/0/76523/f67c0526-8e7c-3263-31f2-5ab8cfc1679b.gif)

# 導入
[Webhook受信編](https://qiita.com/KanaSakaguchi/items/ccb3432a262cb723327a)ですでにWebhookの設定は済んでいるので、プロジェクトバージョンを更新するだけです。
別プロジェクトで作った人やそもそも初めての人はその記事の通りWebhookを設定してください。
<img width="336" alt="プロジェクト更新.png" src="https://qiita-image-store.s3.amazonaws.com/0/76523/1c49bf6e-80c6-685b-dfdb-71c32d2be7a8.png">

# コード掲載＆動作サンプル
コード掲載＆動作サンプルとして[KanaSakaguchi/AutoUpdateKanbanForGitHub](https://github.com/KanaSakaguchi/AutoUpdateKanbanForGitHub)というリポジトリを用意しました。
ぜひ実際にプルリク立てたりして試してみてください。
と言いたいところですが、リポジトリ関係者以外で作成後にラベルを操作したりすることはできないようです。
なので、プルリクのラベル操作等でカードが自動操作される様子を自分で試すには各自スクリプトを組んでみてください。

すでに[私がここのコードをリファクタしてみたプルリク](https://github.com/KanaSakaguchi/AutoUpdateKanbanForGitHub/pull/1)を作成からマージまでやっているので、ラベルを変更した後すぐにカートが移動されている様子を確認できます。

ですが、[上記リファクタはミスがあったので修正](https://github.com/KanaSakaguchi/AutoUpdateKanbanForGitHub/pull/2)しました。
合わせてそのプルリクを作ってマージするまでを録画しました。
これでプルリクの状況に合わせてプロジェクトのカードが移動している様子がわかると思います。
![一通り動作確認.gif](https://qiita-image-store.s3.amazonaws.com/0/76523/8dc1737c-b584-fb30-2478-2f32d3710493.gif)

# 運用してみて
実際2017年6月終わりから(勝手に)運用して2018年4月現在で9ヶ月ちょっと経っています。
「知らないところで[マージ済み]カラムと[マージせずクローズ]カラムを消されて思ったように動かないことに4日後に気づいた」なんてこともありしましたが、ほかに大きなトラブルもなく運用できています。
(ほとんど自分でマージすることがないので発見が遅れました。)
複数人が関わっているリポジトリで運用する場合は、あらかじめ説明しておいたほうがいいですね。

自分のトークンを使っているので、自分が関係ないプルリクも自分が操作したという記録が残るのがちょっと気になってます、
BOTアカウントを作って運用できるならこの問題は解消されますね。

# 次回予告
GitHubがプルリクエストの状態を元にProjectに反映させる機能を作っていたので使って見たいと思います。
(今度はあまり間を開けずに投稿したい。)

<details><summary>[余談] 画像アップロードに困った件</summary>
やっとこの記事を書き上げるための準備ができたのが四月中旬。
録画したMOVをリサイズなしのフレームレート20でGIFアニメにしたもの(18.8MB)を貼ろうとしたらエラーになりました。
ですが、`![error]()`としか出なくて不親切でした。
容量オーバーを疑って投稿のコメント欄を見ると、9.xxMB/100MBでした。
(具体的な数値は覚えてないです。)
この10MB弱を使用可能残量だと思い、5月になるまで投稿を延期することにしました。
(そんなに画像をあげた覚えなかったのでなんでだろう？でした。)
　
で、5月になって再トライするも、結果は同じ。
ここで、1ファイル10MBまでだと気づきました。
DocBase使っていなければ1ファイル10MB制限に気づけませんでした。
(もっと早く気づけていればもっと早く投稿できたのに。)
[Qiitaのヘルプ](https://help.qiita.com/)を確認しましたが、1ファイルの容量制限のことは書かれていませんでした。
(DocBaseの場合は[ヘルプに書かれています](https://help.docbase.io/posts/74580))
Qiitaには1ファイル10MBまでとわかるようにして欲しいと思いました。
(そもそも27インチの画面録画してGIFアニメにしたら大きいですね。)
</details>