GitHub リポジトリの訪問数を kintone に記録しよう

著者名:竹内 能彦(サイボウズ株式会社)

目次

caution
警告

Moment.js はメンテナンスモードになり、 日付処理できる代替ライブラリへの移行 (External link) が推奨されています。
代替ライブラリのひとつ Luxon (External link) については、 kintone カスタマイズでの導入方法の紹介記事 があります。

はじめに

cybozu developer network でも GitHub の活用シーンが増えてきました。

リポジトリを公開すると気になるのは訪問数です。
GitHub ではリポジトリの訪問数を取得できますが、直近 2 週間しか取得できません。
そこで kintone と AWS Lambda を使って、GitHub リポジトリの訪問数を kintone に定期的に記録するサンプルを作りました。

定期的に実行するために AWS Lambda を利用します。
また、Lambda 関数の作成に Node.js 環境が必要ですのでご注意ください。

大きな処理の流れは以下のとおりです。

ではさっそく準備に取り掛かりましょう。

1. kintone 設定

kintone アプリの作成

フィールド名 フィールドタイプ フィールドコード
プロジェクト 文字列(1行) project
年月日 日付 date
訪問数 数値 count
ユニーク訪問数 数値 uniques

API トークンの発行

「アプリの設定 > API トークン」で、レコード閲覧、レコード追加が可能な API トークンを発行します。
kintone に追加するデータが重複しないようにレコード閲覧権限も必要です。

2. GitHub 設定

GitHub アカウントの作成

github.com (External link) から、GitHub アカウントを作成します。
GitHub アカウントを取得済みの方はログインしてください。

Personal access token の作成

AWS Lambda から GitHub API を利用するために必要な作業です。
Personal access tokens (External link) にアクセスして新しい Personal access token を作成します。

「Token description」には適当な値を、「Select scopes」には「public_repo」を設定します。
Token となるキーは作成時にしか確認できませんので忘れずにメモしてください。

3. Lambda 関数の実行ファイル作成

Node.js をインストールした環境での作業になります。

以下のサンプルコードをファイル名「index.js」で保存します。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
/*
 * kinone x GitHub sample program
 * Copyright (c) 2017 Cybozu
 *
 * Licensed under the MIT License
 * https://opensource.org/license/mit/
 */

const GitHubApi = require('github');
const moment = require('moment');
const https = require('https');
const querystring = require('querystring');
const bluebird = require('bluebird');
const async = require('async');

// kintone 接続設定
const DOMAIN = 'subdomain.cybozu.com';
const APP_ID = 1;
const API_TOKEN = 'your_api_token';

// GitHub 接続設定
const OWNER = 'owner_name';
const REPO = 'repository_name';
const PERSONAL_ACCESS_TOKEN = 'your_personal_access_token';


const github = new GitHubApi({
  protocol: 'https',
  host: 'api.github.com',
  Promise: bluebird
});

github.authenticate({
  type: 'token',
  token: PERSONAL_ACCESS_TOKEN
});

const getOptions = (apiPath, method) => {
  'use strict';

  return {
    hostname: DOMAIN,
    port: 443,
    path: apiPath,
    method: method,
    headers: {
      'X-Cybozu-API-Token': API_TOKEN
    }
  };
};

// kintone から一番直近に登録したデータの年月日を取得
const getRecord = (project, callback) => {
  'use strict';
  console.log('[START] get kintone record');

  const params = {
    app: APP_ID,
    query: `project = "${project}" order by date desc limit 1 offset 0`,
    fields: ['date']
  };
  const query = querystring.stringify(params);
  const options = getOptions('/k/v1/records.json?' + query, 'GET');

  const req = https.request(options, (res) => {
    res.setEncoding('utf-8');
    res.on('data', (chunk) => {
      if (res.statusCode === 200) {
        callback(null, JSON.parse(chunk).records);
      } else {
        callback(res.statusMessage);
      }
    });
  });

  req.on('error', (err) => {
    callback(err.message);
  });

  req.end();
};

// kintone に訪問数などを登録
const postRecord = (project, date, count, uniques, callback) => {
  'use strict';
  console.log(`[START] post kintone record: ${date}`);

  const params = {
    app: APP_ID,
    record: {
      project: {
        value: project
      },
      date: {
        value: date
      },
      count: {
        value: count
      },
      uniques: {
        value: uniques
      }
    }
  };

  const options = getOptions('/k/v1/record.json', 'POST');
  options.headers['Content-Type'] = 'application/json';

  const req = https.request(options, (res) => {
    res.setEncoding('utf-8');
    res.on('data', (chunk) => {
      if (res.statusCode === 200) {
        const body = JSON.parse(chunk);
        callback(null, body.id);
      } else {
        callback(res.statusMessage);
      }
    });
  });

  req.on('error', (err) => {
    callback(err.message);
  });

  req.write(JSON.stringify(params));
  req.end();
};

exports.handler = (event, context, callback) => {
  'use strict';

  const project = `${OWNER}/${REPO}`;
  getRecord(project, (err, records) => {
    'use strict';

    if (err !== null) {
      callback(err);
      return false;
    }

    const currentDate = moment();
    let lastDate = '';
    try {
      lastDate = moment(records[0].date.value);
    } catch (_) {
      lastDate = moment('2000-01-01');
    }

    github.repos.getViews({
      owner: OWNER,
      repo: REPO
    }).then((res, err2) => {
      if (err2 !== undefined) {
        callback(err2);
        return false;
      }

      const views = res.data.views;
      const postRecordHandlers = [];
      for (let i = 0; i < views.length; i++) {
        const view = views[i];
        const timestamp = view.timestamp;
        const count = view.count;
        const uniques = view.uniques;

        const date = moment(timestamp);

        // 一番直近に登録したデータの年月日以前の場合はスキップ
        if (date.isSameOrBefore(lastDate, 'day')) {
          continue;
        }
        // 実行日と同じ年月日もスキップ
        if (date.isSame(currentDate, 'day')) {
          continue;
        }
        postRecordHandlers.push(postRecord.bind(this, project, date.format('YYYY-MM-DD'), count, uniques));
      }

      if (postRecordHandlers.length === 0) {
        console.log('[COMPLETE] nothing to do');
        callback(null, '[COMPLETE] nothing to do');
        return false;
      }

      async.series(postRecordHandlers, (err3, rid) => {
        if (err3 !== null) {
          callback(err3);
          return false;
        }
        console.log('[COMPLETE] record id: ' + rid.join(', '));
        callback(null, '[COMPLETE] record id: ' + rid.join(', '));
      });
    });
  });
};

17~19 行目に kintone のドメイン、アプリ ID、API トークンを設定します。

16
17
18
19
// kintone 接続設定
const DOMAIN = 'subdomain.cybozu.com';
const APP_ID = 1;
const API_TOKEN = 'your_api_token';

訪問数を取得するにはそのリポジトリの管理者権限が必要になります。
22~24 行目に訪問数を取得したいリポジトリのオーナー名、リポジトリ名、先ほど作成した Personal access token を設定します。
リポジトリの URL が https://github.com/kintone-labs/kintoneUtility (External link) の場合、オーナー名は「kintone-labs」、リポジトリ名は「kintoneUtility」になります。

21
22
23
24
// GitHub 接続設定
const OWNER = 'owner_name';
const REPO = 'repository_name';
const PERSONAL_ACCESS_TOKEN = 'your_personal_access_token';

以下のコマンドを実行して、モジュールのインストール、ZIP ファイル(Lambda 関数の実行ファイル)を作成します。
zip コマンドでエラーが発生した場合は、階層にファイル「index.js」とディレクトリー「node_modules」が存在するか確認してください。

1
2
npm install github moment https querystring bluebird async
zip -rq github-to-kintone.zip index.js node_modules

4. AWS 設定

AWS アカウントの作成

Lambda の開始方法 (External link) を参考に AWS アカウントと管理者ユーザーを作成します。
AWS アカウントを取得済みの方はログインしてください。

実行ロールの作成

Lambda 実行ロール (External link) を参考に Lambda を実行するロールを作成します。

例として、今回はロール名を「AWSLambdaExecute」で作成しました。

Lambda の設定

新規関数を作成します。
設計図は「ブランク関数」を選択します。

トリガーには CloudWatch を設定します。
「ルール名」、「ルールの説明」は適当な値を、「スケジュール式」には「rate(1 day)」を設定します。

「名前」は適当な値を設定します。
「ランタイム」は「Node.js 6.10」を選択します。
「関数パッケージ」には先ほど作成した「GitHub-to-kintone.zip(Lambda 関数の実行ファイル)」をアップロードします。

「ハンドラー」は「index.handler」を、「ロール」は先ほど作成したロールを設定します。
詳細設定の「タイムアウト」には少し余裕をみて 20 秒を設定します。

実行

実行すると kintone にデータが登録されました。
また、AWS Lambda の機能で毎日データが登録されるので過去の訪問数も確認できますね。

おわりに

GitHub 非常に楽しいですね。
訪問数の増加が確認できればコード更新のモチベーションにもつながりそうです。