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


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

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

## 前提として

* Jenkinsのセットアップは割愛しています。かわりに別の記事をあげているので参考にしてください。
 * [OS XにJenkinsをHomebrewでセットアップする](http://qiita.com/makoto_kw/items/cbe93d4ebbc35f3b43fd)
* 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のビルドを行う時にハマるポイントとその解決法・１](http://qiita.com/mokemokechicken/items/5ea39c87512a515071f5)

## 使うJenkinsプラグイン

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

* [Git Plugin](http://wiki.jenkins-ci.org/display/JENKINS/Git+Plugin)
* [Git Client Plugin](http://wiki.jenkins-ci.org/display/JENKINS/Git+Client+Plugin)
* [Xcode Plugin](https://wiki.jenkins-ci.org/display/JENKINS/Xcode+Plugin)
* [Testflight Plugin](http://wiki.jenkins-ci.org/display/JENKINS/Testflight+Plugin)
* [Run Condition Plugin](http://wiki.jenkins-ci.org/display/JENKINS/Run+Condition+Plugin)
* [Conditional BuildStep Plugin](https://wiki.jenkins-ci.org/display/JENKINS/Conditional+BuildStep+Plugin)
* [Flexible Publish Plugin](http://wiki.jenkins-ci.org/display/JENKINS/Flexible+Publish+Plugin)
* [Next Build Number Plugin](https://wiki.jenkins-ci.org/display/JENKINS/Next+Build+Number+Plugin)

[Jenkins CLI](https://wiki.jenkins-ci.org/display/JA/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. Dailyビルド
1. TestFlight配布用ビルドとアップロード (テスト用) 
1. AppStore提出用ビルド

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

2と3は配布のためのジョブです。自分のタイミングで行うためにパラメータ付きビルドで手動で行うジョブにします。この２つは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`` といった適当なファイル名を設定しておき、その名前に元ファイルからコピーしています。

```bash
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は以下のような内容になっています。

```ruby: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を使うときのメモ](http://qiita.com/makoto_kw/items/75e684435e650a4442a4) を参考にしてください。

### Jenkins Xcode Plugin の設定

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

#### ビルドの設定

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

![jenkins-xcode-build_01.png](https://qiita-image-store.s3.amazonaws.com/0/2435/5264a754-d74e-a8a0-c833-51981871a504.png "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](https://qiita-image-store.s3.amazonaws.com/0/2435/d0c57640-2e4b-090a-e857-25e967a46e89.png "jenkins-xcode-build_02.png")


#### 署名

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

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

![jenkins-xcode-sign.png](https://qiita-image-store.s3.amazonaws.com/0/2435/6940fe47-f098-b18c-92f7-0cd61acc1ce3.png "jenkins-xcode-sign.png")


#### ビルド番号の設定

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

![jenkins-xcode-version.png](https://qiita-image-store.s3.amazonaws.com/0/2435/c3da4ab7-bae5-ad47-fcef-a2e10e562f26.png "jenkins-xcode-version.png")

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

Jenkinsのビルド番号は **1** から始まりますが、[Next Build Number Plugin](https://wiki.jenkins-ci.org/display/JENKINS/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](https://qiita-image-store.s3.amazonaws.com/0/2435/95e8484d-244d-2145-f990-0f1a2b0c7720.png "jenkins-testflight-token.png")


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

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

![jenkins_2013-11-11_1249.png](https://qiita-image-store.s3.amazonaws.com/0/2435/8e36383a-c8b2-51e7-433b-bf92efeda79c.png "jenkins_2013-11-11_1249.png")

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

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

![jenkins-testflight-detail.png](https://qiita-image-store.s3.amazonaws.com/0/2435/dd10be8f-2d1d-dfb7-eb70-77f57edabe2a.png "jenkins-testflight-detail.png")


## 成果物の設定

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

![jenkins-ipa.png](https://qiita-image-store.s3.amazonaws.com/0/2435/13db5df5-b6e7-5036-f11d-b136387645cc.png "jenkins-ipa.png")

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

## ジョブの実行

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

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

![jenkins-build-param.png](https://qiita-image-store.s3.amazonaws.com/0/2435/3abdce3e-6b4a-72b7-3642-f8bb0137056d.png "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を使わないで同じビルドで署名だけをつけかえるだけという方法も有効です。 
 * 参考: 
iOSアプリの署名を付け替える http://qiita.com/beakmark/items/33c0b73603e491f08a33
* 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
```

* Jenkinsを長時間起動していると状態がおかしくなる問題に遭遇していて定期的に再起動させています。 https://issues.jenkins-ci.org/browse/JENKINS-17526

```xml:~/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
```