GAS + GraphQLでGitHubのコミット数をSlackに通知する
はじめに
新しい技術を身につけるためには、実際に使うことが一番です。
最近流行りのGraphQLを学ぶために、実際にコードを書いてみました。
ついでに得意なSlack連携で、Githubのレポジトリのブランチごとのコミット数を習慣で集計するChatOps化します。
その週にコミットがあった全てのブランチを取得できるので、「知らない間に新しいブランチが切られている」ことや、「masterブランチへのマージ忘れ」を発見できるなど、思った以上に役に立ちました。
Slackに連携した状態
GithubのマスコットOctocatが毎週コミット数を教えてくれる形にしました。
準備するもの
- Githubアカウント
- Googleのアカウント
- Slackのアカウントとワークスペース
手順
大まかな手順を列挙します。
- GraphQLのクエリを書く
- GASのダッシュボードからプロジェクトを作成する
- GASのコードを書く
- トリガーを設定し、送信時間を決める
GraphQLのクエリと返却されるJSON
query{
repo_name: repository(owner: "owner_name", name: "repo_name") {
...RepoFragment
}
}
fragment RepoFragment on Repository {
refs(first: 100, refPrefix:"refs/heads/") {
edges {
node {
name
}
}
nodes {
target {
... on Commit {
history(first: 0, since: "2018-08-01T09:00:00.000+09:00\" ) {
totalCount
}
}
}
}
}
}
repo_name
はレポジトリの名前、owner_name
はレポジトリの所有者を入れてください。
また、histroy
項目内のsince
はGASでトリガーの1週間前の日付が入るように調節します。
実際にテストしてみたい方は上記項目を変更した後、Github DeveloperのGraphQL API Explorerで上記クエリを実行してみてください。
Githubから下記のようなJSONが返却されます。
{
"data": {
"emma": {
"refs": {
"edges": [
{
"node": {
"name": "PHP_UNIT"
}
},
{
"node": {
"name": "develop"
}
},
{
"node": {
"name": "feature/SAMPLE_PROJECT"
}
},
{
"node": {
"name": "master"
}
},
{
"node": {
"name": "release_0820"
}
}
],
"nodes": [
{
"target": {
"history": {
"totalCount": 8
}
}
},
{
"target": {
"history": {
"totalCount": 44
}
}
},
{
"target": {
"history": {
"totalCount": 19
}
}
},
{
"target": {
"history": {
"totalCount": 2
}
}
},
{
"target": {
"history": {
"totalCount": 11
}
}
}
]
}
}
}
}
nodeのname:"ブランチ名"とtargetのtotalCount:"数字"が順番に対応しています。
これを整理すると、2018-08-01以降、2018-08-24 22:00(クエリを飛ばした時点)のコミット数がわかります。
|ブランチ名|コミット数| |:--|:--| | PHP_UNIT | 8 | | develop | 44 | | feature/SAMPLE_PROJECT | 19 | | master | 2 | | release_0820 | 11 |
ブランチのマージ先のコミット数は、その週にマージされたブランチのコミット数が加算さます。なので、developブランチはコミット数が多くなりがちです。
コード全体の紹介
repositoryのowner名をpanda_program、プロジェクト名をgasとします。
function createMessage() {
// GithubのAPIを叩く
const json = fetchCommitTotal();
const repos = [json.data.gas];
const branch = {
"gas" : repos[0].refs.edges
};
const total = {
"gas" : repos[0].refs.nodes
};
// JSONを整形し、プロジェクト毎のブランチとコミット数を取得
const gas = prepareInfo(branch.gas, total.gas);
const projectName = ['Gas'];
const project = [gas];
const today = formatDate(0);
const oneWeekBefore = formatDate(-7);
const time = new Date();
const hour = time.getHours();
const triggerTime = hour + ':00:00';
// メッセージの作成
var message = '今週もお疲れ様でした😊\n';
message += '今週のプロジェクト毎のコミット数を集計しました。\n';
message += '(集計期間 ' + oneWeekBefore + ' ' + triggerTime + ' ~ ' + today + ' ' + triggerTime + ')\n\n';
for (i = 0, len = project.length; i < len; ++i) {
message += 'プロジェクト名: *' + projectName[i];
message += '* \n ```' + project[i] + '```\n\n';
}
// Slackに送る
const to = PropertiesService.getScriptProperties().getProperty("TO");
sendToSlack(message, to);
}
function prepareInfo(branch, total) {
const branchName = [];
const commitTotal = [];
const data = [];
for (var i = 0, len = branch.length; i < len; ++i) {
// コミット数が0のブランチを除外
if(parseInt(total[i].target.history.totalCount) === 0) {
continue;
}
// 配列にオブジェクトを格納
data.push({
"branchName" : branch[i].node.name,
"commitTotal" : total[i].target.history.totalCount
});
}
var info = '';
var sum = 0;
for (var i = 0, len = data.length; i < len; ++i) {
info += data[i].branchName + ' のコミット数は ' + data[i].commitTotal + '件' + '\n';
sum += data[i].commitTotal;
}
info += '合計' + sum + '件です。';
return info;
}
function fetchCommitTotal() {
const url = 'https://api.github.com/graphql';
const token = PropertiesService.getScriptProperties().getProperty("TOKEN");
const oneWeekBefore = formatDate(-7);
const graphql = ' \
{ \
gas: repository(owner: "panda_program", name: "gas") {\
...RepoFragment\
}\
}\
fragment RepoFragment on Repository {\
refs(first: 100, refPrefix:"refs/heads/") {\
edges {\
node {\
name\
}\
}\
nodes {\
target {\
... on Commit {\
history(first: 0, since: "'
+ oneWeekBefore +
'T09:00:00.000+09:00\" ) {\
totalCount\
}\
}\
}\
}\
}\
}\
';
const options = {
'method' : 'post',
'contentType' : 'application/json',
'headers' : {
'Authorization' : 'Bearer ' + token
},
'payload' : JSON.stringify({ query : graphql })
};
const response = UrlFetchApp.fetch(url, options);
const json = JSON.parse(response.getContentText());
return json;
}
/** 日付をフォーマットする
* @param {int} days
*/ @return {string} YYYY-MM-DD
function formatDate(days) {
const now = new Date;
const oneWeekBefore = new Date(now.getFullYear(), now.getMonth(), now.getDate() + days);
const year = oneWeekBefore.getFullYear();
const month = ('0' + (oneWeekBefore.getMonth() + 1)).slice(-2);
const date = ('0' + oneWeekBefore.getDate()).slice(-2);
const format = year+ '-' + month + '-' + date;
return format;
}
function sendToSlack(body, channel) {
const url = PropertiesService.getScriptProperties().getProperty("WEBHOOK_URL");
// Slackに通知する際の名前、色、画像を決定する
const data = {
'channel' : channel,
'username' : 'Octocat',
'attachments': [{
'color': '#fc166a',
'text' : body,
}],
'icon_url' : 'https://assets-cdn.github.com/images/modules/logos_page/Octocat.png'
};
const payload = JSON.stringify(data);
const options = {
'method' : 'POST',
'contentType' : 'application/json',
'payload' : payload
};
UrlFetchApp.fetch(url, options);
}
createMessage
でSlackの本文を作成します。prepareInfo
でGraphQLから返却されたJSONの整形します。fetchCommitTotal
でGraphQLにクエリを飛ばす。formatDate
で日付をYYYY-MM-DDの形にフォーマットします。sendToSlack
でSlackに通知します。
1つの関数に1つの動作をさせることでコードをシンプルに保っています(KISS原則)。
プロパティとトリガーを設定する
プロパティの設定
下記の情報をGASの「スクリプトのプロパティ」に書き込む(方法は「GoogleAppsScript スクリプトのプロパティの超簡単な使い方」を参照)。
|プロパティ| 値 | |:--|:--| | Slackの通知先(チャンネル or ユーザー名) | TO(*1) | | SlackのWebhook URL | WEBHOOK_URL (*2) | | GithubのPersonal access tokens | TOKEN (*3) |
*1 チャンネルは「#チャンネル名」、ユーザーは「@ユーザー名」 *2 「SlackのWebhook URL取得手順」を参照 *3「GitHub「Personal access tokens」の設定方法」を参照
トリガーの設定
[編集 > 全てのトリガー]で関数を実行するタイミングを設定します。 GASはFaaSなので、関数ごとに実行を選択する事ができます。
定期実行する関数はcreateMessage
、一週間に一度に集計するため週タイマーを選択します。
複数のレポジトリのコミットを取得する
このシステムは複数レポジトリがある前提で作成しました。gas, nodejs, javascriptというレポジトリからコミット数を取得すると仮定します。
その場合、GraphQLのクエリとcreateMessage
を書き換えます。
GraphQLのクエリにレポジトリを追加する
下記のようにレポジトリを追加してください。
{
gas: repository(owner: "panda_program", name: "gas") {
...RepoFragment
}
nodejs: repository(owner: "panda_program", name: "nodejs") {
...RepoFragment
}
javascript: repository(owner: "panda_program", name: "javascript") {
...RepoFragment
}
}
JSONの整形箇所を変更する
createMessage
の一部を下記のように書き換えてください。
const json = fetchCommitTotal();
const repos = [json.data.gas, json.data.nodejs, json.data.javascript];
const branch = {
"gs" : repos[0].refs.edges,
"node" : repos[1].refs.edges,
"js" : repos[2].refs.edges
};
const total = {
"gs" : repos[0].refs.nodes,
"node" : repos[1].refs.nodes,
"js" : repos[2].refs.nodes
};
// JSONを整形し、プロジェクト毎のブランチとコミット数を取得
const gs = prepareInfo(branch.gs, total.gs);
const node = prepareInfo(branch.node, total.node);
const js = prepareInfo(branch.js, total.js);
const projectName = ['gas', 'nodejs', 'javascript'];
const project = [gs, node, js];
Slackの通知の表示方法を変更する
sendToSlack
のoptionを変更することで、表示名やアイコン画像を変更することができます。
実行環境としてGoogle Apps Scriptを選択した理由
- GASはファンクションを書くだけで利用できるFaaS(サーバレス)であるため管理コストが小さい
- 会社で使用しているGithubのトークンを掲載するため、セキュリティの観点から外部サーバーにファイルを置かず、会社用Google Driveに格納したかった
- Slackへの連携が容易だから
備忘
const graphql
の箇所で\
で改行をエスケープしないとエラーが出ます。何かいい方法はありそう。- レポジトリ数が増えると
const branch
、const total
はfor文を使う方がベター。
Happy Coding 🎉