GoogleAppsScript
gas
TravisCI

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

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

GASを使った理由としては手軽にスケジューラが使えるという点が一番大きいです。無料で、かつサーバーを用意する手間もないのは良いです1

ビルドを自動化する方法

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

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

Travisのトークンを取得する

ドキュメントに記載されている通り、Travisのコマンドラインクライアントをインストールしてトークンを生成します。

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

rubyがインストールされている場合は以下のようにします。
詳しくはドキュメントを参照してください。

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

APIトークンの生成

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

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

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

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

/////////////////////////////////////////////////////////////////////////
// 設定
/////////////////////////////////////////////////////////////////////////
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分は起動時間が前後するので注意が必要です。

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 を使っていますが、これだとスケジューラ上で実行したもののログが残りません。
ちゃんとログを取るならこちらの記事にあるように、Google DocumentなりStackdriverなりに出力した方が良いです。

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

参考情報


  1. 実はTravis CIにもビルドを定期実行する機能(Cron Jobs)はあるのですが、ビルド対象のブランチや実行時間を柔軟に設定できないので今回は利用しませんでした。