GitHub Projectsとは
ソフトウェア開発に便利ないわゆる「カンバン」方式のタスク管理ツールです。カンバン方式のタスク管理ツールとしてはTrello(トレロ)などが有名かと思います。
ソフトウェアの共同開発においては、このようなタスク管理が開発スピードの肝となってきます。そして共同開発といえばGitHub。ならばGitHubでカンバン使えたら最強じゃね?そんなツールあったらな〜
...ありました。その名も「GitHub Projects」。直感的な使いやすさこそTrelloなどに劣りますが、GitHubのIssueやPull Requestとの連携の取りやすさを考えればこっちの方がいいんじゃないかと。
せっかくなので今回はIssue/Pull RequestのopenやcloseイベントをトリガーにしてGitHub Projectsを色々操作してみようと思います。
大体の流れ
- GitHub webhookでIssueのopenやassign, closeイベントを検知する
- 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としましょう。
<?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);
}