読者です 読者をやめる 読者になる 読者になる

TravisCIでiOSの依存ライブラリの更新を自動化する

これはSpeee Advent Calendarの1日目の記事です。

iOSアプリエンジニアの@hiragramです。

私がいるチームでは、依存ライブラリ管理にCarthage、CIにTravisCIを使っています。

また、ライブラリのバージョンアップに積極的についていくために、TravisCIのCron Jobshubコマンドを使い、定期的に依存ライブラリの最新バージョンをチェックして、アップデートがあればGitHubにPRを投げるような仕組みを構築しています。

実際に投げられるPRはこんな感じです。僕のトークンを使ってるので僕が作ったみたいになってますが、CIのタスクから作られたものです。

スクリーンショット 2016-10-21 16.31.23.png

パッチやマイナーアップデートの優先度は下がりがちだと思います。しかし、Swiftは言語のバージョンが比較的速いスピードで上がっていくこともあり、ライブラリにバグが残る可能性は大いにあるのでこまめに更新しておきたいところです。

やってみましょう

  • TravisCIサポートに連絡してcronを開放してもらう
  • cronの設定をする
  • 環境変数の設定をする
  • cron用のスクリプトを用意する
  • PRの本文を作るスクリプトを用意する

このようなステップで準備していきます。

TravisCIでcronを有効にする

Cron Jobsのページにあるように、cron機能はデフォルトでは設定できなくなっているので、サポートに連絡して有効にしてもらう必要があります。

TravisCIのダッシュボード左上の Help -> Email Support から Please enable cron jobs configuration on "Hogehoge" organization. とか送っておけば、翌営業日には対応してくれます。

cronの設定をする

リポジトリを選択して、設定画面を開きます。

スクリーンショット 2016-10-21 17.10.08 のコピー.png

サポートから返事が来ていれば、設定項目の中に"Cron Jobs"があるはずです。

スクリーンショット 2016-10-21 17.17.41.png

ここで定期的に実行するブランチと周期と実行条件を設定します。

私たちは、週に1回masterブランチで実行するようにしています。

ちなみにtipsですが、毎週のタスクを任意の曜日や時間に設定する項目はありませんが、cronの設定をAddした時(金曜17時に設定したなら毎週金曜17時)に実行されるようになります。

タスク内でcarthage updateを実行するため非常に時間がかかります。私たちは月曜の朝7時にしています。

TravisCIに環境変数を設定する

Personal access tokensのページで、TravisCIからPRを作るのに使うトークンを発行します。

TravisCIの設定画面で、GH_TOKENに発行したトークンを、GH_USERにGitHubのユーザー名を書きます。

スクリーンショット 2016-10-24 10.30.40.png

cronから起動された時用のビルドタスクを書く

各種タスクは.travis.ymlでシェルスクリプトを指定してその中に書くようにしています。

# .travis.yml

language: objective-c
osx_image: xcode8
script:
  - ./.travis_scripts/build.sh
notifications:
  slack: **********
before_install:
  - ./.travis_scripts/before_install.sh
before_script:
  - sudo systemsetup -settimezone  Asia/Tokyo

cronによって実行されている時、環境変数$TRAVIS_EVENT_TYPEcronという値になっているので、それをみて処理を切り替えるようにしています。

#!/bin/bash

# build.sh

if [ $TRAVIS_EVENT_TYPE = cron ] ; then
    if carthage outdated | grep "All dependencies are up to date." ; then
        exit 0
    else
        carthage update --platform ios --configuration Debug
        createPR
    fi
else
    carthage bootstrap --platform ios --configuration Debug
    runTest
fi

$TRAVIS_EVENT_TYPEがcronだった場合、まずcarthage outdatedでアップデートがあるライブラリを探します。 もしもアップデートがあるようならcarthage updateで更新します。 最後に、自分で定義したcreatePRを実行します。

createPRの中身はこんな感じです。

createPR() {
    CREDENTIAL_FILE="$HOME/.config/git-credential"
    mkdir -p $HOME/.config

    echo "https://${GH_TOKEN}:@github.com" > $CREDENTIAL_FILE
    echo "github.com:
- oauth_token: $GH_TOKEN
  user: $GH_USER" > $HOME/.config/hub

    which hub
    if [ $? != 0 ] ; then
        brew install hub
    fi

    git config --global user.name "TravisCIたん"
    git config --global user.email "hiragram@users.noreply.github.com"
    git config --global hub.protocol "https"
    git config --global credential.helper "store --file=$CREDENTIAL_FILE"

    BRANCH_NAME="dependencies_autoupdate_"`date "+%Y-%m-%d_%H-%M-%S"`
    git checkout -b $BRANCH_NAME

    touch pr.txt
    echo "Detected outdated dependencies." > pr.txt
    echo "# Outdated dependencies" >> pr.txt;
    git diff Cartfile.resolved | perl .travis_scripts/check_version.pl >> pr.txt # 後述
    git add Cartfile.resolved
    if git commit -m "[Auto generated] Update dependencies" ; then
        git push origin $BRANCH_NAME
        hub pull-request -F pr.txt
    fi
}

処理の流れとしては、

  • 環境変数に入れたトークンとユーザーをhubの設定ファイルに書き出す
  • hubが入ってなければ入れる
  • gitの設定
  • ブランチ切る
  • PRのタイトル/本文をファイルに書き出す
  • リモートにpushする
  • PRを作る

という感じです。

PRの本文を作るPerlスクリプトを書く

先程のシェルスクリプトの中に

git diff Cartfile.resolved | perl .travis_scripts/check_version.pl >> pr.txt

とありましたが、これの解説をします。 PRの本文には

## Quick/Nimble
v5.0.0 -> [v5.1.0](https://github.com/Quick/Nimble/releases/tag/v5.1.0)
[compare](https://github.com/Quick/Nimble/compare/v5.0.0...v5.1.0)
  • 導入済みのバージョン
  • 最新バージョンとリリースノートへのリンク
  • そのバージョン間のdiffへのリンク

が記載されています。 これを作る為にPerlでこのようなスクリプトを書きました。

#!/usr/bin/perl

use strict;
use warnings;

my $input = "";
while(my $line = <STDIN>) {
  $input .= $line;
}

while ($input =~ /\-github "(.*?)" "(.*?)"/gms) {
  my $repository = $1;
  my $oldVersion = $2;
  if ($input =~ /\+github "$repository" "(.*?)"/ms) {
    my $newVersion = $1;
    my $oldDispVer = $oldVersion;
    my $newDispVer = $newVersion;
    if (length($oldVersion) > 25 && length($newVersion) > 25) {
      $oldDispVer = substr($oldVersion, 0, 6);
      $newDispVer = substr($newVersion, 0, 6);
    }
    print("## $repository\n");
    print("$oldDispVer -> [$newDispVer](https://github.com/$repository/releases/tag/$newVersion)\n");
    print("[compare](https://github.com/$repository/compare/$oldVersion...$newVersion)\n");
  }
}

carthage updateを実行すると、アップデートがあった場合にCartfile.resolvedが更新されます。 git diff Cartfile.resolvedの結果を標準入力に渡してやると、先程のPRの本文を生成するスクリプトです。

試しに動かしてみる

準備の手順は以上です。 通常のpushで動くタスクはTRAVIS_EVENT_TYPEcronでは無いので、テストの段階ではシェルスクリプトの中でTRAVIS_EVENT_TYPE=cronとしてやるといいでしょう。

まとめ

従来は依存パッケージ7個のビルドだけで20分ほどかかっていましたが、そこが短縮されたのは非常に大きかったです。

細かいアップデートは、放置すればするほど後回しになってしまい、健全さが失われていくと思います。 とはいえ、人間が定期的にアップデートをチェックして更新するのも手間なので、自動化してしまえということで今回の仕組みを構築しました。

この仕組みを導入する前は、気づいたときにGitHubをみて新しいリリースがないかを確認していましたが、導入後はある程度ほったらかしでもアップデートに追随できるようになりました。

今のところは破壊的な変更のないマイナーアップデートまでしか自動化出来ていないので、メジャーアップデートも同じような仕組みで追随できるようにしたいと思っています。

参考

Travis CIからGitHubにPull Requestを送る - GeekFactroy