GASでTravis CIのビルドを自動化する 

GASを使ってTravis CIのビルドを定期的に実行するタスクを自動化したので、その方法について説明します。

GASを使った理由としては手軽にスケジューラが使えるという点が一番大きいです。無料で、かつサーバーを用意する手間もないのは良いです[^1]。
[^1]: 実はTravis CIにもビルドを定期実行する機能(Cron Jobs)はあるのですが、ビルド対象のブランチや実行時間を柔軟に設定できないので今回は利用しませんでした。

# ビルドを自動化する方法

TravisのAPI経由でビルドを実行することが可能なので、APIを叩くGASの関数を定義してスケジューラに登録し、定期実行します。
やることは以下です。それぞれ簡単に解説していきます。

- Travisのトークンを取得する
- GASでTravisのビルドを実行するAPIを叩く関数を作成する
- GASのスケジューラに関数を登録して定期実行させる

## Travisのトークンを取得する

[ドキュメント](https://developer.travis-ci.org/authentication)に記載されている通り、Travisのコマンドラインクライアントをインストールしてトークンを生成します。

### Travisコマンドラインクライアントのインストール

rubyがインストールされている場合は以下のようにします。
詳しくは[ドキュメント](https://github.com/travis-ci/travis.rb#installation)を参照してください。

```sh
$ gem install travis -v 1.8.8 --no-rdoc --no-ri
```

### APIトークンの生成

```sh
# privateリポジトリの場合は両方のコマンドに --pro オプションをつけること
$ travis login
$ travis token
```

## GASでTravisのビルドを実行するAPIを叩く関数を作成する

`entryPoint` という関数を作成して、ビルドを実行するようにしました。
コードは以下のようになりました。一番上にある設定用の変数に値をいれて使います。

処理の流れは、リポジトリからGithub上に存在しているブランチを全て取得し、ブランチ名が `REPOSITORY_NAME_REGEXP` とマッチするブランチを対象にビルドを実行するといった感じです。
エラー処理のせいで若干長くなってしまいましたが、特に複雑なことはやっていません。

```javascript
/////////////////////////////////////////////////////////////////////////
// 設定
/////////////////////////////////////////////////////////////////////////
var AUTHORIZATION_TOKEN = 'xxxxx'; // TravisのAPIトークン
var ORGANIZATION = 'username'; // Githubのユーザー名
var REPOSITORIES = ['repo1']; // リポジトリ名、複数設定可能
var REPOSITORY_NAME_REGEXP = /master/ // ビルド対象ブランチの正規表現
/////////////////////////////////////////////////////////////////////////

function entryPoint() {
  buildRepositories();
}

function buildRepositories() {
  for (var i = 0; i < REPOSITORIES.length; i++) {
    Logger.log(Utilities.formatString('target repository: %s', REPOSITORIES[i]));
    buildBranches(REPOSITORIES[i]);
  }
}

function buildBranches(repository) {
  var slug = Utilities.formatString('%s%%2F%s', ORGANIZATION, repository); // {oraganization}%2F{repository}
  var targetBranches = fetchTargetBranches(slug);
  Logger.log(Utilities.formatString('target branches: %s', targetBranches));
  for (var i = 0; i < targetBranches.length; i++) {
    build(slug, targetBranches[i]);
  }
}

function fetchTargetBranches(slug) {
  var url = Utilities.formatString('https://api.travis-ci.com/repo/%s/branches?exists_on_github=true', slug);
  var response = UrlFetchApp.fetch(url, getApiOptions());

  if (!isStatusSucceed(response.getResponseCode())) {
    Logger.log(Utilities.formatString(
      'Error - fail to fetch branches %s - responseCode is　%s',
      slug,
      response.getResponseCode()
    ));
    return [];
  }

  var data = response.getContentText();
  var result = parseJson(data);
  if (result === false) {
    Logger.log(Utilities.formatString('Error - JSON parse error - %s', data));
    return [];
  }

  return selectTargetBranches(result['branches']);
}

function selectTargetBranches(branches) {
  var targetBranches = [];
  for (var i = 0; i < branches.length; i++) {
    var name = branches[i]['name'];
    if (name.match(REPOSITORY_NAME_REGEXP) !== null) {
      targetBranches.push(name);
    }
  }
  return targetBranches;
}

function build(slug, branch) {
  var url = Utilities.formatString('https://api.travis-ci.com/repo/%s/requests', slug);
  var response = UrlFetchApp.fetch(url, getBuildApiOptions(branch));

  if (isStatusSucceed(response.getResponseCode())) {
    Logger.log(Utilities.formatString('Successfully finished build %s:%s', slug, branch));
  } else {
    Logger.log(Utilities.formatString(
      'Error - build api error %s:%s - responseCode is %s',
      slug,
      branch,
      response.getResponseCode()
    ));
  }
}

function parseJson(data) {
  var json;
  try {
    json = JSON.parse(data);
  } catch (e) {
    return false;
  }
  return json;
}

function isStatusSucceed(code) {
  strCode = String(code);
  return (strCode.match(/^2\d\d$/) !== null ? true : false);
}

function getBuildApiOptions(branch) {
  var options =  getApiOptions();
  options.method = 'post';
  var today = Utilities.formatDate(new Date(), 'JST', 'yyyy/MM/dd');
  var data = {
    'request': {
      'branch': branch,
      'message': 'build ' + today
    }
  };
  options.payload = JSON.stringify(data);
  return options;
}

function getApiOptions() {
  var headers = {
    'Travis-API-Version': '3',
    'Authorization': Utilities.formatString('token %s', AUTHORIZATION_TOKEN)  
  };
  return {
    'contentType': 'application/json',
    'muteHttpExceptions': true, // ステータスコードのチェックは自力でやるため
    'headers': headers
  };
}
```

## GASのスケジューラに関数を登録して定期実行させる

あとは作成した `entryPoint` 関数をスケジューラに登録して完了です。
スケジューラの登録はGUIからも可能ですが、コードを書いておくと登録や編集が楽にできます。

以下のようなコードを用意しました。
`setupCronJobs` を実行することで、月曜から金曜の夜23:00にスケジューリングできます。
ちなみにApps Scriptの仕様上、15分は起動時間が前後するので注意が必要です。

```javascript
function setupCronJobs() {
  targetDays = [
    ScriptApp.WeekDay.MONDAY,
    ScriptApp.WeekDay.TUESDAY,
    ScriptApp.WeekDay.WEDNESDAY,
    ScriptApp.WeekDay.THURSDAY,
    ScriptApp.WeekDay.FRIDAY
  ];
  for (var i = 0; i < targetDays.length; i++) {
    createClockTriggerBuilder(targetDays[i]);
  }
}

function createClockTriggerBuilder(day) {
  ScriptApp.newTrigger('entryPoint')
    .timeBased()
    .atHour(23)
    .onWeekDay(day)
    .inTimezone('Asia/Tokyo')
    .create();
}
```

# 振り返り

改善点としてはログ出力があります。上記のコードでは `Logger.log` を使っていますが、これだとスケジューラ上で実行したもののログが残りません。
ちゃんとログを取るなら[こちらの記事](https://qiita.com/howdy39/items/4dc11af7b1c74c17f7f7)にあるように、Google DocumentなりStackdriverなりに出力した方が良いです。

また、GASのメリットとして手軽にスケジューラが作れる点をあげましたが、逆に手軽すぎてどこでスケジューラが動いているのかわからなくなってしまうこともあるようです。そのため、スケジューラの説明やリンクをドキュメントに残しておきましょう。タスク管理系の機能が乏しいのでタスクが増えたり、複雑になってきた時には別のサービスに乗り換える方が良さそうです。

# 参考情報

- https://developer.travis-ci.org/
- https://developers.google.com/apps-script/reference/script/script-app
- https://www.higebu.com/blog/2015/12/01/travis-ci-with-niftycloud-timer/#.WjZw-VT1Uo8
- https://qiita.com/howdy39/items/4dc11af7b1c74c17f7f7