TECH BLOG
【TECH BLOG #5】GitHub AppsとGraphQLの話
技術戦略チームのTAです。
サーバーサイドのエンジニアをやっています。
何について書こうか迷っていたのですが、「せっかくTECH BLOGなので技術的なことで」というようなリクエストがどこかから聞こえてきました。
ということで、多少技術が絡んだようなことを書こうと思います。
GitHub Apps
弊社でソースコードのバージョン管理として使用しているのはGitHubです。
運用の仕方についてはチームにより違いがありますが、私が担当しているチームではGitHub Projectsでタスク確認をしています。
Issueとしてやるべきことや課題を溜めていき、Pull request(PR)の状態と合わせて、今どうなっているかを見ます。IssueやPRをProjectsと紐付けることで、カンバンのように見られるようになるわけですね。オートメーション機能により、レビューやマージに合わせて自動でカラム移動するところが素敵です。
さて、この「Projectsでタスク確認する」ということを実現する上で大事なことは、先に書いた通りですが「IssueやPRをProjectと紐付ける」ところです。IssueやPRの作成時には忘れずにProjectを選択する必要があります。そうしないとタスクを見落としてしまいますからね。
「そんなもの選ぶだけだし、大した手間ではない」と言われそうで、実際その通りだったりしますが……。なんだかそれは面倒くさいというか、「決まりきった動作なのになんで自分でやらなきゃいけないんだ」と思ってしまいました。Issue等作成時にデフォルトで選択された状態にする設定とかあるかな、と探して見ましたが、どうやらなさそうです。
ということで前置きが長くなりましたが、ないならそういう仕組を作ればいいじゃないかということですね。
もしかしたらすでにあるのかもしれないですが、単純に勉強の意味と諸事情により自分で作ることにしました。ここで目をつけたのがGitHub Appsです。Webhook+OAuthと違い、ユーザーに紐付かない動作となるのがいいところですね。
詳細な実装については置いておくとして、以下のように動作させるようにしました。
1. IssueやPR作成をトリガーとして、GitHub AppsがWebhook通知
2. Webhookを受けたサーバーで通知内容を判断
3. 該当のIssueやPRにProjectsを紐付けるAPIを実行
WebhookのリクエストヘッダにはX-GitHub-Eventとしてイベントの種類(pull_request等)が設定されており、トリガーとなったイベントを判断できます。また、ボディ(Payload)はJSON形式になっており、詳細な内容を確認できます。
イベントによりPayloadの内容は異なるため、処理を変える必要はあります。PHPの例でいうと、状態取得でもPRなら $json[“pull_request”][“state”] で、Issueなら $json[“issue”][“state”] という感じです。PRにもIssueにもstateが存在するので、この例では”pull_request”と”issue”の違いだけですが、もちろん片方にしかないものも存在します。また、それ以外のイベントでは全然違う構成になっている場合もあります。実現したいことはイベントごとに異なることが多いと思うので、ここは素直に分岐処理したほうがいいですね。
サーバーからAPIを実行する上での前準備として、アクセストークンを取得する必要があります。
GitHub Appsの設定からprivate keyを発行し、その鍵とJWTの仕組みでアクセストークンを要求すれば取得できます。アクセストークンには期限があるので、API実行までの実装としては以下のようになります。
1. Webhookを受ける
2. キャッシュしているアクセストークンが期限切れでなければ使い回す
3. 期限切れならJWTでトークン発行要求を作成し、GitHubへ送信
4. アクセストークンが取得できたら、それを使ってAPI実行
GraphQL
無事にトークンが取れたらProjectsを紐付けるAPIを実行しますが、ここでもこれまでと違うことをしようと思ってGraphQLを使うことにしました。
GitHubでは以前からあるREST APIだけでなく、GraphQLを使用することができます。RESTと違って欲しい情報を部分的に取ってきたり、IssueとPRの両方の情報を1リクエストで取ってきたりできるのがGraphQLのいいところですね。
GraphQLのクエリは欲しいデータ構造をそのまま書くというか……文章で上手く言えないのですが、そんな感じですね。
query { organization(login: "funplex-organization") { projects(first:1, search: "funplex-project") { nodes { columns(first:10) { nodes { name id } } } } } }
適当な例ですが、これでfunplex-organizationにあるfunplex-projectの中のカラムの名前とIDを最大10個取得します。レスポンスはJSONで、ほぼこのクエリの構造通りで返ってきます。
{ "data":{ "organization":{ "projects":{ "nodes":[ { "name":"カラム名", "id":"ZnVucGxleCBibG9n" } ] } } } }
見やすくインデントや改行を入れて加工してありますが、概ねこんな感じです。
更新処理(例えばIssueに対してProjectを紐付ける)を行う場合は
mutation { updateIssue(input:{id: "ZnVucGxleCBpc3N1ZQ==", projectIds: ["ZnVucGxleCBwcm9qZWN0cw=="]}) { issue { id number title } } }
こんな感じになります。
ポイントとしては、指定するのはそれぞれidであるというところですね。Issue番号やProject名などは指定できないので、事前にqueryなどを使用して取得しておく必要があります。
今回やりたいことの場合、IssueのidについてはWebhookのPayloadに含まれているので問題ありません。Projectのidは不明であるため、先にqueryでProjectの情報を取得しておきProject名が一致するnodeのidを検索するようにしました。
さらにPRに関してですが……なんと実装時点ではupdatePullRequestのinputにprojectIdsがなかったので、PRのProjectを変更できませんでした(執筆時点の現在ではあるようです)。
これについては上手い方法がなかったので、PRにProjectを紐付けるのではなくProjectのIn progressカラムにPRのカードを追加するという手法を取りました。一応動作したのですがPR作成から更新API実行までに当然ながら短い遅延があるので、間髪入れずにマージしたりクローズしたりすると意図しないカラム移動をしてしまう現象が……(笑)。いつか直します。
最後に
自分が触れたことのないものに触れてみるのは刺激があっていいですね。
実は大したことをしていない上に効果が微妙なものですが、得られるものはあったように思います。
今回の実装では自分のチームに特化した作りになっていますが、手を加えることで適用範囲は広がりそうです。
サーバーで受けているところをサーバーレス化してみるとか、Organizationごとに設定を持っておいて複数Organizationで同じGitHub Appsを使えるようにしてみるとか……。仕組みを作ってしまえば機能拡張もコストをかけずにできるようになるかもしれません。
GitHub Appsに限らずですが、「こんなことをできる仕組みがある」というのを知っておくのが大事ではないかと思います。浅くでもいいので色々なアプローチを知っておくと、何らかの課題に直面したときの対応時間や質を高められるのではないかと。
ということで、今後も色々なものに触れてみてネタとして投稿できればいいなと思います。
※2021年7月1日よりファンプレックスはグリーエンターテインメントへ商号変更いたしました。
※2021年6月30日以前の記事においてファンプレックスという表記がある場合がございますが、ご了承ください。
information
本件に関するお問い合わせ先
グリーエンターテインメント株式会社 広報担当
東京都港区六本木6-10-1 六本木ヒルズ森タワー
E-mail:info-ent@ml.gree.net