GAS + GraphQLでGitHubのコミット数をSlackに通知する

@Panda_Program

はじめに

新しい技術を身につけるためには、実際に使うことが一番です。

最近流行りのGraphQLを学ぶために、実際にコードを書いてみました。

ついでに得意なSlack連携で、Githubのレポジトリのブランチごとのコミット数を習慣で集計するChatOps化します。

その週にコミットがあった全てのブランチを取得できるので、「知らない間に新しいブランチが切られている」ことや、「masterブランチへのマージ忘れ」を発見できるなど、思った以上に役に立ちました。

Slackに連携した状態

GithubのマスコットOctocatが毎週コミット数を教えてくれる形にしました。

準備するもの

  • Githubアカウント
  • Googleのアカウント
  • Slackのアカウントとワークスペース

手順

大まかな手順を列挙します。

  1. 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のトリガー設定画面

複数のレポジトリのコミットを取得する

このシステムは複数レポジトリがある前提で作成しました。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 branchconst totalはfor文を使う方がベター。

Happy Coding 🎉

パンダのイラスト
パンダ

記事が面白いと思ったらツイートやはてブをお願いします!皆さんの感想が執筆のモチベーションになります。最後まで読んでくれてありがとう。

  • Share on Hatena
  • Share on Twitter
  • Share on Line
  • Copy to clipboard