Edited at
iOSDay 9

趣味でつくるiOSアプリこそJenkinsでリリースを自動化

More than 3 years have passed since last update.

TestFlightへのアップロード部分の内容はAppleに買収されiTunes Connectに統合される前のTestFlightについての情報です、testflightapp.comは2015/2/26に終了です

コードを書き終わったがもう会社に行かなければならない、テストのために手持ちのデバイスにインストールしておきたい・・・。趣味でつくっているアプリこそ自動化して開発の時間を捻出すべきなのでは?そんなことを思っていたらMarvericksが無料でリリースされたので押入れで眠っていたMacBook AirにMarvericksとJenkinsを入れてリリース作業を自動化してみました。


前提として


  • Jenkinsのセットアップは割愛しています。かわりに別の記事をあげているので参考にしてください。



  • Xcode5, Jenkins 1.542, CocoaPodsを使ったプロジェクトを使います

  • Xcodeのプロジェクト名、ワークスペース名を MyProject としているので読み替えて使ってください

  • 今回はビルドと配布に焦点をあてています、静的解析やテストに関する話はありません

  • TestFlightへの自動アップロード、AppStoreに提出するipaファイルの自動ビルドまでを行います


ビルド環境のセットアップ

新しいマシンにセットアップする場合は以下を忘れないようにしておきます。


  • Xcode

  • Xcode Command Line Tools

  • iPhone Developer Program証明書

  • CocoaPods (使っている場合)

KeyChainへのアクセスなど初回に対話が必要な設定があるのでいきなりJenkinsでビルドするのではなく、一度プロジェクトを取得して、Xcodeでビルドができるか確認しておきましょう。


自動ビルドに必要なファイルのコミット


Provisioning Profile

Provisioning Profileを取得、更新しなくてよいようにリポジトリにコミットしておきます。Dropboxから参照するのもよいでしょう。ファイルは任意の場所にあってもビルド前に行うシェルスクリプトでプロジェクトディレクトリにコピーします。


スキーマ設定ファイル

xcodebuildでワークスペースファイルを使う際にスキーマファイルが必要になります。このファイルが Xcodeでワークスペースファイルを開いた際に生成される ものであるため、CocoaPodsで自動生成したワークスペースをxcodebuildでビルドしようとするとスキーマファイルが見つからずにビルドが失敗します。

MyProject.xcodeproj/xcuserdata/<UserName>.xcuserdatad/xcschemes/MyProject.xcscheme

MyProject.xcodeproj/xcuserdata/<UserName>.xcuserdatad/xcschemes/xcschememanagement.plist

この問題を回避するために事前にスキーマファイルをJenkinsを実行するユーザのファイルとしてコミットしておきます。

参考: JenkinsでiOSのビルドを行う時にハマるポイントとその解決法・1


使うJenkinsプラグイン

以下のものを使っています。

Jenkins CLIを使うなら以下のようにしてインストールできます。

wget http://localhost:8080/jnlpJars/jenkins-cli.jar

java -jar jenkins-cli.jar -s http://localhost:8080 install-plugin \
git git-client next-build-number run-condition conditional-buildstep flexible-publish \
xcode-plugin testflight
java -jar jenkins-cli.jar -s http://localhost:8080 safe-restart

rubyやbundlerで細かく制御するためCocoaPodsプラグインは使っておらずシェルで以下を実行します。(余談を参照)

pod install


ビルドの種類

Jenkinsからは1つのジョブで以下の3種類のビルドを行います。


  1. Dailyビルド

  2. TestFlight配布用ビルドとアップロード (テスト用)

  3. AppStore提出用ビルド

1が静的チェックやテストが目的のジョブです。SCMをポーリングさせればコミットしていない日はジョブを実行しないことも可能です。

2と3は配布のためのジョブです。自分のタイミングで行うためにパラメータ付きビルドで手動で行うジョブにします。この2つはTestFlight SDKを組み込むかどうかと、Provisioning Profileを変えているため別々のビルドで成果物を作成します。


使うパラメータ

パラメータ付きビルドで使うパラメータは以下のとおりです。ジョブを作成したら登録してください。デフォルト値がオフなので毎日の自動ビルドでは配布に関する処理は何も行いません。

パラメータ名

デフォルト値
説明

TESTFLIGHT_UPLOAD
真偽値
false
TestFlightへアップロードするかどうか

BUILD_FOR_APPSTORE
真偽値
false
AppStore配布用のビルドを行うかどうか

パラメータ付きビルドで使ったパラメータの値はJenkinsのジョブの実行時に 環境変数 に設定されます。シェルなどから環境変数を参照すれば処理の分岐が可能になります。


ビルドスクリプトの抜粋


Jenkins Xcode Plugin の前処理

ビルド手順の追加 から シェルの実行 を追加し、以下のようなシェルの処理を Jenkins Xcode Plugin のビルドの に行っています。ここで行うのは


  • Provisioning Profileの切り替え

  • TestFlight SDK の有効・無効

  • CocoaPodsのインストール

です。

TestFlightでAdHocに配布する場合と、AppleStoreに配布する場合とでProvisioning Profileを分けているので配布方法によってファイルを切り替えています。Jenkins Xcode Plugin には MyProject.mobileprovision といった適当なファイル名を設定しておき、その名前に元ファイルからコピーしています。

cp MyProject_Ad_Hoc_Distribution.mobileprovision MyProject.mobileprovision

if $BUILD_FOR_APPSTORE = true ; then
rake configure_appstore
cp MyProject_AppStore_Distribution.mobileprovision MyProject.mobileprovision
fi
pod install

rake configure_appstore の処理ではTestFlight SDKを使わないようにソースファイルを変更しています。 Rakefileは以下のような内容になっています。


Rakefile

# encoding: utf-8

require 'rake'

task :configure_appstore do
build_content = File.read('MyProject/MyProjectBuild.h').gsub(/(USE_TESTFLIGHT_SDK) 1/, '\1 0')
open('MyProject/MyProjectBuild.h', 'w') do |f|
f.<< build_content
end
podfile_content = File.read('Podfile').gsub(/^pod 'TestFlightSDK'/, '#\0')
open('Podfile', 'w') do |f|
f.<< podfile_content
end
end


TestFlight SDKをCocoaPodsで利用している前提です。あるヘッダファイルとPodfileをそれぞれ以下のように置換しています。

#define USE_TESTFLIGHT_SDK 1 -> #define USE_TESTFLIGHT_SDK 0

pod 'TestFlightSDK' -> #pod 'TestFlightSDK'

リポジトリにコミットしてあるソースコードはTestFlight SDKを組み込んだ状態になっているので configure_appstore タスクを実行した場合に TestFlight SDKを使うコードをコンパイルしないように変更します。

USE_TESTFLIGHT_SDK で処理を分岐する話は iOSでTestFlight SDKを使うときのメモ を参考にしてください。


Jenkins Xcode Plugin の設定

Xcodeビルドの設定に入ります。 ビルド手順の追加 から Xcode を追加します。


ビルドの設定

General build settings を設定します。配布が目的なのでビルドの構成である ConfigrationRelease を設定し、成果物として使う ipaファイルの名前は MyProject-${VERSION}-${BUILD_DATE} としておきます。

jenkins-xcode-build_01.png

次に Advanced Xcode build options を設定します。Jenkins Xcode Pluginがわかりにくいのがこの部分です。Xcodeの場合、普通にプロジェクトを作ると、指定したプロジェクト名でプロジェクトディレクトリ、プロジェクトファイル、ワークスペースファイル、ターゲットがつくられるので何をどこで設定しているのがわからなくなります。

参考までに設定しているプロジェクトの階層を以下に示します。ここではソースコードと他のファイルを管理するために、リポジトリの1階層下にプロジェクトのファイルがあります(#1)、リポジトリ直下にファイルがない場合は Xcode Project Directory にプロジェクトのあるパスを設定します。

CocoaPodsが作成したWorkspaceファイル (#2) を使うので Xcode Workspace File にファイル名を指定しています。

SYMROOT${WORKSPACE}/build (#3) を設定しているのでここにipaファイルが出力されます。

.

├── MyProject # 1
│   ├── MyProject
│   ├── MyProject.xcodeproj
│   ├── MyProject.xcworkspace # 2
│   ├── Podfile
│   ├── Podfile.lock
│   └── Pods
├── build #3
│   └── Release-iphoneos
│   └── MyProject-NN-YYYY.mm.dd.ipa
└── README.md

jenkins-xcode-build_02.png


署名

署名は Code signing & OS X keychain options 項目で行います。 Embedded Profile はビルド前の処理でコピーしておいたファイルを設定します。

homebrewでインストールした場合ユーザ権限でJenkinsを動かせるので Unlock Keychain を有効にして Keychain path${HOME}/Library/Keychains/login.keychainKeychain password にパスワードを入力しておけばユーザの証明書を参照できます。

jenkins-xcode-sign.png


ビルド番号の設定

Jenkins Xcode Pluginでは Versioning 項目にチェックをいれるとバージョン番号を自動で設定することができます。

jenkins-xcode-version.png

Technical version${BUILD_NUMBER} を設定することでJenkinsのビルド番号を CFBundleVersion に設定できます。 Marketing version(CFBundleShortVersionString)はリポジトリで管理してCFBundleVersionはJenkinsに任せるのが良いと思います。

Jenkinsのビルド番号は 1 から始まりますが、Next Build Number Plugin を使えば番号を飛ばすことができます。過去のビルドを削除するなどしてより大きな数値のビルドが存在しないようにすれば番号を戻すことも可能です。


Jenkins Xcode Plugin の後処理

ビルド手順の追加 から シェルの実行 をもう一つ追加し、以下のようなシェルの処理を Jenkins Xcode Plugin のビルドの に行っています。

BUILD_DATE=`date '+%Y.%m.%d'`

cd build/Release-iphoneos

if $BUILD_FOR_APPSTORE = true ; then
mv MyProject-${BUILD_NUMBER}-${BUILD_DATE}.ipa MyProject-${BUILD_NUMBER}-${BUILD_DATE}_for_appstore.ipa
fi

BUILD_FOR_APPSTORE が有効のときにファイル名のsuffixに _for_appstore をつけています。これは間違えてTestFlightを組み込んだビルドをアップロードしてしまわないように人間がファイル名で判別しやすいようにするためです。


TestFlightへのアップロード設定

ここからはビルド後の処理の設定です。

TestFlightへのアップロードには Testflight Plugin を使います。Testflight Plugin をインストールするとビルド後の処理でTestFlightへのアップロード作業を追加できます。

アップロードにはTokenが必要になるので取得しておきます。

API Token: https://testflightapp.com/account/#api

TeamToken: https://testflightapp.com/dashboard/team/edit/

Tokenの設定はジョブではなく、 Jenkinsの管理 > システムの設定 > Test Flight で行います。チームごとに複数のTokenが設定できます。

jenkins-testflight-token.png

条件付きでアップロード処理を実行させるため、 ビルド後の処理の追加Flexible publish を使い Token を ${ENV,var="TESTFLIGHT_UPLOAD"} にし、 ActionUpload to TestFlight を選択します。

これにより手動によるパラメータ付きビルドで TESTFLIGHT_UPLOAD をチェックした場合にのみ TestFlight へのUploadが行えます。

jenkins_2013-11-11_1249.png

TestFlight Pluginでは高度な設定から配布時の設定が行えます。まずは Distribution Lists を使って自分宛に絞って送るようにしています。

Gitのコメントなどを Build Notes に設定することもできますが、コミットのコメントと知人などのテスターへ適切な説明を両立させるのはなかなか困難です。TestFlightではビルドの説明や配布するメンバーを後からでも設定できるのでまずは自分で試して問題ないことを確認してから、ビルドの説明を入力して、配布するメンバーを追加するという運用が良いと思います。

jenkins-testflight-detail.png


成果物の設定

ビルド後の処理の追加成果物を保存 を追加し、 build/**/*.ipa,build/**/*dSYM.zip を保存するようにしておきます。

jenkins-ipa.png

これでビルドごとのipaファイルがJenkinsからダウンロードできるようになります。


ジョブの実行

ジョブの設定ができたらSCMをポーリングするなどして毎日自動のビルドを走らせます。静的チェックやユニットテストはここで行いましょう。

配布するときはJenkinsにログインして手動でビルドを実行します。手動でジョブを実行しようとするとパラメータの入力を求められるので、TestFlightにアプロードするときは TESTFLIGHT_UPLOAD にチェックを入れ、AppStoreにリリースするときは BUILD_FOR_APPSTORE にチェックを入れてビルドを実行します。TestFlight SDKを組みこんだものをAppStoreに提出したくないのでこの2つのパラメータは排他的に使います。

jenkins-build-param.png


TestFlightへのアップロード

ジョブの設定が正しく動いていればTestFlightにアプロードは自動で行えます。実装が終わったらビルドボタンを押すだけでiOS端末でテストができるようになります。


AppStoreへの提出

パラメータ付きビルドで BUILD_FOR_APPSTORE を有効にしてビルドを行うと、ipa ファイルがビルドの成果物として作成されるのでこのファイルをAppStoreに提出します。

ipaファイルを指定してアップロードする場合は Xcode (Organizer) ではなく Application Loader を使います。Xcode のメニューの Xcode > Open Developer Tool > Application Loader から起動できます。

Organizerを使う場合と同じく、iTunesConnectのアプリのVersionで Ready to Upload Binary を押した状態になっていればアップロードが行えます。


余談


  • 本当は OS X Server + Bot に興味があったのだけど、CocoaPods周りが面倒そうだったので挫けました。でも諦めてはいません

  • どうもiOSプロジェクトでAnt, MavenやGradleを使うことに違和感を覚え、Macにはrubyが入っているし、CocoaPodsもrubyが必要なのでシェルやrakeで済ませることにしました

  • TestFlightでビルドを配布する上でTestFlightSDKは必須ではありません。今回はTestFlightSDKを使うビルド、使わないビルドをそれぞれ作っていますが、TestFlightSDKを使わないで同じビルドで署名だけをつけかえるだけという方法も有効です。



  • Jenkins上で署名できるようになったので署名に関する設定をプロジェクトファイルから削除しました(リポジトリにコミットしているのがなんか嫌だった)

  • いつもOrganizerを使ってAppStoreに提出していたので ipa ファイルを指定して提出する方法の調べ方がわからなかったのですが、Xcodeを使わないフレームワーク (AIR for iOSとか) 周辺のドキュメントを調べるとわかりました

  • 今回の内容とは関係ないですが Ruby の実行に rbenv を使っていて CocoaPods のバージョンをプロジェクトによって固定するために bundler でプロジェクトローカルにインストールしています。実際には pod install のコマンド部分は以下のような処理を実行しています。

export PATH=$HOME/bin:$HOME/.rbenv/bin:/usr/local/bin:$PATH

eval "$(rbenv init -)"
rbenv shell 1.9.3-p392
bundle install --path=vendor/bundle
bundle exec pod install


~/Library/LaunchAgents/homebrew.mxcl.jenkins.restart.plist

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>homebrew.mxcl.jenkins.restart</string>
<key>ProgramArguments</key>
<array>
<string>/usr/bin/java</string>
<string>-jar</string>
<string>/Users/Shared/jenkins-cli.jar</string>
<string>-s</string>
<string>http://localhost:8080</string>
<string>safe-restart</string>
</array>
<key>StartCalendarInterval</key>
<dict>
<key>Hour</key>
<integer>15</integer>
<key>Minute</key>
<integer>00</integer>
</dict>
</dict>
</plist>

wget http://localhost:8080/jnlpJars/jenkins-cli.jar /Users/Shared/jenkins-cli.jar

launchctl load ~/Library/LaunchAgents/homebrew.mxcl.jenkins.restart.plist