GitHub Projectsとは

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

Screen Shot 2018-02-13 at 21.59.39.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で取得してください。

GitHub Projects

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

サンプル

ここでは例として

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

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

このプログラムファイルを先ほどのhttps://example.com/ のドキュメントルートに置くことになります。名前はindex.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に対応するカードを発見するメソッド

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: カードをカラム間で移動させるメソッド

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: カードを削除するメソッド

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からカードを作成するメソッド

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);
}

参考

公式リファレンス

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.