PHPからGitHub Projectsを操作する

# GitHub Projectsとは
ソフトウェア開発に便利ないわゆる「カンバン」方式のタスク管理ツールです。カンバン方式のタスク管理ツールとしては[Trello(トレロ)](https://trello.com/)などが有名かと思います。

![Screen Shot 2018-02-13 at 21.59.39.png](https://qiita-image-store.s3.amazonaws.com/0/203213/b3c5f27c-fef8-4ce4-815b-6ad0a8e07ffd.png)

ソフトウェアの共同開発においては、このようなタスク管理が開発スピードの肝となってきます。そして共同開発といえばGitHub。ならばGitHubでカンバン使えたら最強じゃね？そんなツールあったらな〜

...ありました。その名も**「GitHub Projects」**。直感的な使いやすさこそTrelloなどに劣りますが、GitHubのIssueやPull Requestとの連携の取りやすさを考えればこっちの方がいいんじゃないかと。

せっかくなので今回はIssue/Pull RequestのopenやcloseイベントをトリガーにしてGitHub Projectsを色々操作してみようと思います。

# 大体の流れ
1. GitHub webhookでIssueのopenやassign, closeイベントを検知する
2. GitHub APIを叩いてGitHub Projects上のカードに操作を加える

# 準備
## イベントをリッスンするサーバー
ドキュメントルートなどにプログラムファイルを置く。ここではhttps://example.com/ のドキュメントルートにプログラムファイルを置くと仮定する。

## GitHub webhook
https://github.com/{組織名}/{レポジトリ名}/settings/hooks から[Add webhook]をクリックし作成する。

| Payload URL                                          | https://example.com/                             |
|------------------------------------------------------|--------------------------------------------------|
| Content type                                         | application/json                                 |
| Secret                                               | {パスワードを設定}                               |
| Which events would you like to trigger this webhook? | こだわりがなければとりあえずSend me everythingで |
| Active                                               | check                                            |

これでhttps://example.com/ に対してこのレポジトリにおける全てのイベントを送りつける設定ができたことになります。

## GitHub API
[Personal access tokens](https://github.com/settings/tokens)で取得してください。

## GitHub Projects
Projectsを作成後カラムをいくつか作っておき、そのうち二つのIDを取得しておいてください。カラム右上から```Copy column URL```をクリックするとコピーされたURLの末尾に出てきます。

# サンプル
ここでは例として

- IssueがオープンしたらProjectsのカラム１にカード追加
- 誰かがIssueにアサインされたら対応するカードをカラム２に移動
- Issueがクローズしたら対応するカードをカラム２から削除

を実装します。ただしこの操作以外に手動でカードを動かしたりすることがないという前提に基づきます。でないとget_cardメソッドが破綻します。

このプログラムファイルを先ほどのhttps://example.com/ のドキュメントルートに置くことになります。名前はindex.phpとしましょう。

```php:index.php
<?php

define('SECRET_KEY', '上で決めたパスワード');
define('COLUMN_1', {ProjectsのカラムID});
define('COLUMN_2', {Projectsの別のカラムID});

$header = getallheaders();
$hmac = hash_hmac('sha1', $HTTP_RAW_POST_DATA, SECRET_KEY);
if(isset($header['X-Hub-Signature']) && $header['X-Hub-Signature'] == 'sha1='.$hmac){
    $payload = json_decode($HTTP_RAW_POST_DATA, true);

    if(isset($payload['action'])
        && $payload['action'] == 'assigned'
        && isset($payload['issue'])
    ){ // Issueが誰かにアサインされたとき

        $number = $payload['issue']['number'];
        $card_id = get_card($number,COLUMN_1);
        move_card($card_id, COLUMN_2);

    }
    elseif(isset($payload['action'])
        && $payload['action'] == 'opened'
        && isset($payload['issue'])
    ){ // Issueが開いた時

        $id = $payload['issue']['id']; // 注意 !!!
        create_card($id, COLUMN_1);

    }
    elseif(isset($payload['action'])
        && $payload['action'] == 'closed'
        && isset($payload['issue'])
    ){ // Issueが閉じた時

        $number = $payload['issue']['number'];
        $card_id = get_card($number,COLUMN_2);
        delete_card($card_id);

    }
}
```
ここでIssueに対応するカードを作成する時、必要なのが```number```ではなく```id```であることに注意してください。前者はuniqueな値であるのに対し、idは特定のレポジトリについてインクリメントする値です。

## get_card: Issueに対応するカードを発見するメソッド
```php
function get_card($issue, $at)
{
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, 'https://api.github.com/projects/columns/'.$at.'/cards');
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
        'Authorization: token {APIトークン}',
        'Acccept: application/vnd.github.inertia-preview+json',
        'User-Agent:Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:26.0) Gecko/20100101 Firefox/26.0'
    ]);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);

    $resp = curl_exec($ch);
    curl_close($ch);

    $resp = json_decode($resp,true);
    foreach ($resp as $v) {
        $issue_id = substr($v['content_url'] ,strrpos($v['content_url'],'/')+1);
        if($issue_id == $issue){
            return $v['id'];
        }
    }
}
```

## move_card: カードをカラム間で移動させるメソッド
```php
function move_card($card, $to)
{
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, 'https://api.github.com/projects/columns/cards/'.(string)$card.'/moves');
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
        'Authorization: token {APIトークン}',
        'Acccept: application/vnd.github.inertia-preview+json',
        'User-Agent:Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:26.0) Gecko/20100101 Firefox/26.0'
    ]);
    curl_setopt($ch, CURLOPT_POST, 1);
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(['position' => 'top', 'column_id' => $to]));
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);

    curl_exec($ch);
    curl_close($ch);
}
```

## delete_card: カードを削除するメソッド
```php
function delete_card($card)
{
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, 'https://api.github.com/projects/columns/cards/'.(string)$card);
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
        'Authorization: token {APIトークン}',
        'Acccept: application/vnd.github.inertia-preview+json',
        'User-Agent:Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:26.0) Gecko/20100101 Firefox/26.0'
    ]);
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);

    curl_exec($ch);
    curl_close($ch);
}
```

## create_card: Issueからカードを作成するメソッド
```php
function create_card($issue, $at)
{
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, 'https://api.github.com/projects/columns/'.$at.'/cards');
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
        'Authorization: token {APIトークン}',
        'Acccept: application/vnd.github.inertia-preview+json',
        'User-Agent:Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:26.0) Gecko/20100101 Firefox/26.0'
    ]);
    curl_setopt($ch, CURLOPT_POST, 1);
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(['content_id' => $issue, 'content_type' => 'Issue']));
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);

    curl_exec($ch);
    curl_close($ch);
}
```

# 参考
[公式リファレンス](https://developer.github.com/v3/projects/)