はじめに
修正に対するフィードバック(質問や指摘)はプルリクにコメントすることでレビュワーから開発者に伝えます。
各レビュワーがどれくらいコメントしているのかを集計することで、その人のレビュワーとしての活躍ぶりを測る一つの指標にできるのではないかと考えました。
各レビュワーのコメント数の集計を、タイトルの通り、GitHubAPIをGASで叩いてスプレッドシート上に結果を吐くことで実現しました。
GitHubの情報をスプレッドシートで確認・集計したいあなたにこの記事を捧げます。
利用するAPIを確認する
GitHub APIのドキュメントにアクセスします。
今回はプルリクエストのレビューコメントを取得したいです。
ちょうどいいのがありますね。
Pull request レビュー コメント用 REST API エンドポイント
これを利用するために必要な権限を確認しておきます。
粒度の細かいトークンには次のアクセス許可セットが設定されている必要があります:
"Pull requests" repository permissions (read)
どうやらリポジトリでプルリクエストの読み取り権限が必要そうです。
集計先のスプレッドシートを作る
GitHubから取得した情報を出力するスプレッドシートを作りましょう。
上部のメニューから、「拡張機能」「Apps Script」を選択すると、GASの新しいプロジェクトが開きます。
スプレッドシート側の準備は一旦完了です。
アクセストークンを作る
APIを叩くのに不可欠なトークンを作ります。
個人開発ならPATで十分かもしれません。
参考:
【Git】personal access tokenを使用してGitHubへアクセスする
GitHubで個人アクセストークンを発行する方法
今回はGitHub Appsを用いたトークン発行をやってみます。
GitHub Appsを用いることで、個人アカウントに依存しないトークンが発行できるので、組織での開発に向いています。
参考:
GitHubと連携する新しいアプリの形:GitHub Appsの作り方
PATとGitHub Appsトークンの違いと使い分けのポイント
GitHub Appのインストールアクセストークンを試してみる
GitHub Appsの詳しい説明については偉大なる先人たちの記事を参照いただくとして、ここでは自分が実施した手順をさっと記載しておきます。
GitHub Appsを作る
GitHub Appsの作成ページにアクセスして、作成します。
ここではPermissionについて説明します。
今回使いたいAPIに必要な権限は以下でした。
"Pull requests" repository permissions (read)
Permissionsから「Repository permissions」を開いて

「Pull requests」を探して、read権限をつけましょう。

リポジトリにインストールする
作成したGitHub Appsの設定画面から、「Install App」メニューを開くと、インストール可能なアカウントが表示されています。
アカウントを選択すると、それに紐づくリポジトリが選択できるようになりますので、プルリクの情報を取得したいリポジトリを選択して、Install&Request を押します。
GitHub Appsの情報を環境変数として保管する
GitHub Apps経由でトークンを発行するには、以下の3つの情報が必要です。
- App ID
- Private Key
- Installation ID
App IDとPrivate KeyはAppsの設定画面の「General」から確認できます。
Installation IDは「Install App」からインストール先のアカウントの歯車を押すと、権限の更新などができるページが開きます。このページのURLの末尾の数字がInstalltion IDです。
3つの情報を入手したら、GASの環境変数として保存します。
Apps Scriptの「プロジェクトの設定」を開き、スクリプトプロパティとして、3つの値を保存します。

PrivateKeyについてはそのままだと使えないので変換します。
以下が参考になると思います。
スクリプトプロパティとして保存した変数は、以下のように取得できます。
const props = PropertiesService.getScriptProperties()
const appId = props.getProperty("GITHUB_APP_ID")
const installationId = props.getProperty("GITHUB_INSTALLATION_ID")
const privateKey = props.getProperty("GITHUB_PRIVATE_KEY").replaceAll("\\n", "\n")
トークンを発行する
GitHub Appsのトークンは、以下のJWTをhttps://api.github.com/app/installations/{installationId}/access_tokensに送り付けることで入手できます。
headerとpayloadを用意
- header:おまじない
{
alg: "RS256",
typ: "JWT"
}
- payload:nowを現在時刻として、iatは60秒前、expは10分後(今から10分間有効になる)、issにGitHub AppsのApp IDを指定する
{
iat: now - 60,
exp: now + 10 * 60,
iss: appId
}
headerとpayloadをつなげる
それぞれBase64エンコードしたものを「.」でつなげて一つの文字列にします。
const src = Utilities.base64Encode(JSON.stringify(header)) + "." + Utilities.base64Encode(JSON.stringify(payload))
秘密鍵で署名する
上記の文字列と秘密鍵で署名を作ります。
const signed = Utilities.computeRsaSha256Signature(src, privateKey)
文字列と署名をつなげる
上記の文字列とBase64エンコードした署名を「.」でつなげます。
const jwt = src + "." + Utilities.base64Encode(signed)
以上まとめると、トークンの発行のコードは以下のように書けます。
function generateGitHubAppsToken() {
const props = PropertiesService.getScriptProperties()
const appId = props.getProperty("GITHUB_APP_ID")
const installationId = props.getProperty("GITHUB_INSTALLATION_ID")
const privateKey = props.getProperty("GITHUB_PRIVATE_KEY").replaceAll("\\n", "\n")
const now = Math.floor(Date.now() / 1000)
const header = {
alg: "RS256",
typ: "JWT"
}
const payload = {
iat: now - 60,
exp: now + 10 * 60,
iss: appId
}
const src = Utilities.base64Encode(JSON.stringify(header)) + "." + Utilities.base64Encode(JSON.stringify(payload))
const signed = Utilities.computeRsaSha256Signature(src, privateKey)
const jwt = src + "." + Utilities.base64Encode(signed)
const url = "https://api.github.com/app/installations/" + installationId + "/access_tokens"
const response = UrlFetchApp.fetch(url, {
method: "post",
headers: {
Accept: "application/vnd.github.machine-man-preview+json",
Authorization: "Bearer " + jwt
}
})
return response.getContentText()
}
お目当てのAPIを叩く
取得したトークンを使って、プルリクエストのコメント一覧を取得するAPIを叩きます。
ドキュメントによると、/repos/{owner}/{repo}/pulls/commentsにGetリクエストを送ればよさそうですね。
const url = "https://api.github.com/repos/{owner}/{repo}/pulls/comments" // 取得対象のリポジトリの情報(owner, repo)を入れてください
const response = UrlFetchApp.fetch(url, {
headers: {
Accept: "application/vnd.github.v3+json",
Authorization: "Bearer " + generateGitHubAppsToken()
},
})
やってみると分かるのですが、上記の書き方だと30件しか取得できません。
これは、per_page(一度に取得する件数)のデフォルト値が30だからです。クエリパラメータを変更することで100件まで取得できるようです。
per_page integer
The number of results per page (max 100). For more information, see "Using pagination in the REST API."Default: 30
const url = "https://api.github.com/repos/{owner}/{repo}/pulls/comments&per_page=100" // 取得対象のリポジトリの情報(owner, repo)を入れてください
100件以上を取得するには、レスポンスヘッダーに存在する「次のページ」のURLを叩く必要があります。
{
'Content-Encoding': 'gzip',
'Access-Control-Allow-Origin': '*',
...
Link: '<https://api.github.com/repositories/{repo}/pulls/comments?per_page=100&page=1>; rel="prev",
<https://api.github.com/repositories/{repo}/pulls/comments?per_page=100&page=3>; rel="next",
<https://api.github.com/repositories/{repo}/pulls/comments?page=5>; rel="last",
<https://api.github.com/repositories/{repo}/pulls/comments?per_page=100&page=1>; rel="first"',
...
}
上記の例ではhttps://api.github.com/repositories/{repo}/pulls/comments?per_page=100&page=3が「次のページ」です。
100件を取得した後、次はこのURLにGetリクエストを送ることで、次の100件が取得できます。
全ての情報を取得したい場合は、ヘッダーに「次のページ」のリンクが出なくなるまでリクエストをくりかえすことで達成できます。
以下のように実装できます。
function doAPI() {
const url = "https://api.github.com/repos/{owner}/{repo}/pulls/comments&per_page=100" // 取得対象のリポジトリの情報(owner, repo)を入れてください
const token = generateGitHubAppsToken() // 何度も発行するのは無駄なので最初に取っておきます
let result = []; // 結果はここに貯めます
while (url) {
const response = UrlFetchApp.fetch(url, {
headers: {
Accept: "application/vnd.github.v3+json",
Authorization: "Bearer " + token
},
})
result = result.concat(JSON.parse(response.getContentText()))
const links = response.getHeaders()['Link']
if (links && links.includes(`rel=\"next\"`)) {
url = links.match(/(?<=<)([\S]*)(?=>; rel="Next")/i)[0]
} else {
url = null;
}
}
return result
}
結果をシートに出力する
取得した結果をスプレッドシートに出力します。
各コメント1行として、コメントを一覧で確認できるようにします。
シートの取得
出力先のシート名を決めて、シートの名称を変更しておきましょう。ここでは「一覧」とします。
Apps Scriptからは、以下のようにシートを取得できます。
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('一覧');
最終行の取得
取得した結果を書きこむ行を取得します。
for文で結果を回しながら、1行ずつ書き込むことを考えます。
何も入力されていない最終行の行番号は以下のように取得できます。
const newRow = sheet.getLastRow() + 1
セルの取得と書き込み
Apps Scriptでは getRange(行番号, 列番号)でセルを指定できます。
また、指定したセルに対してsetValue(値)を呼ぶことで、セルに値を書き込めます。
取得した結果から、コメントのリンク、コメントしたユーザ、コメントの内容、コメント日時をスプレッドシートに出力してみます。
for(let comment of result) {
sheet.getRange(newRow, 1).setValue(comment.html_url)
sheet.getRange(newRow, 2).setValue(comment.user.login)
sheet.getRange(newRow, 3).setValue(comment.body)
sheet.getRange(newRow, 4).setValue(comment.created_at)
}
スプレッドシートへの書き込みをまとめると以下のようになります。
function main() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('一覧');
const result = doAPI();
for(let comment of result) {
sheet.getRange(newRow, 1).setValue(comment.html_url);
sheet.getRange(newRow, 2).setValue(comment.user.login);
sheet.getRange(newRow, 3).setValue(comment.body);
sheet.getRange(newRow, 4).setValue(comment.created_at);
}
}
GitHubから情報を取得してスプレッドシートへ書き込む、という一連の処理は、最後のmain関数を実行することで実現できます。
この関数を定期的に実行することで、だれがどんなコメントをしたのか、定期観察をすることもできます。
おわりに
これでGitHub上のあらゆる情報をスプレッドシートで好きに集計・分析することができますね。
今後私は、どんなコメントがされているのかを分析して「よくされている指摘」を抽出したいと思っています。「よくされている指摘」は仕組みやルールで防いで、「誰もミスをしないから2度とされない指摘」に変えていきたいですね。