Jenkins Pluginに頼らずシェルだけでJobを構築する（ios開発アプリ転送編）

# 概要

Jenkins Pluginを使うとJenkinsのバージョンを上げた時に動作しなくなったり、そもそもGUI設定が面倒だったりで、実はシェルで書いた方が楽じゃん。と思っているのでそうします。
（全く使わないわけではなく、Jenkins2でのお勧めPluginをそのまま入れとくぐらいはやります）

時が経つのは早いもので、Hudson兄さんでしたが、いつの間にやらJenkinsおじさんです。
Jenkinsもバージョン2が出て、Jenkinsfileなどがあり、便利になりつつあります。が、それでもシェルでやります。
そもそも、いまどきはfastlaneでビルドしたり、Webサービス使ったりと思います。が、Jenkins + シェルでやります。

シェルで書いておくメリットは、コマンドレベルで何をしているのかが理解できることでしょう。

# インストール

homebrewで入れてます。

```
$ brew update
$ brew cask install java
$ brew install jenkins
```

設定はネットで探せばすぐ出てくるので割愛。

# 想定運用

jenkinsのパラメーター付きビルドを使い、以下の2点を指定してビルドできるようにします。

* gitのブランチ
* 接続先サーバー名(develop,staging,productionなど)

こんな運用を想定しています。

1. githubでpushしたことを検知するJobがこのJobが呼びだします。（別の記事で書きます）
2. ステージングと本番環境接続用アプリを共存できるように二つのプロビジニングファイル(別々のBundle Identifier)を用意しておきます。
3. ビルド時にCFBundleShortVersionStringにどのbranch・接続先の組合せのアプリなのかがわかるようにsuffixを入れます。   
(例えば、reviewブランチ+stagingサーバ=```.alpha```, releaseブランチ+stagingサーバ=```.beta```など)
4. ビルド後は、Fabricを利用して自動でテスト端末に配信します。
5. Fabricで見たときにバージョンを見れば、2で設定したsuffixがあるので、接続先がわかるようになります。

# Job

## JenkinsのGUI設定

まずは、GUI上で記述します

* ビルドのパラメータ化にチェックを入れ、以下のようにします。

 ![ビルドのパラメーター化.png](https://qiita-image-store.s3.amazonaws.com/0/10494/777b2593-71bb-02e6-a581-adff4bcb9ac9.png "ビルドのパラメーター化.png")


* ソースコード管理の	Branch Specifierで上記のパラメータで設定した変数を記述します。

 ![git.png](https://qiita-image-store.s3.amazonaws.com/0/10494/b935d06c-0325-32ca-1aa7-bf012a291150.png "git.png")


* ビルドのシェルの実行を記述します。   
~~~PROVISIONING_PROFILEは端末を登録するたびに変わるので、gitで管理した値を使うとtagsから過去バージョンがビルドできなくなるのでGUIから渡すようにします。
接続する環境によって利用するPROVISIONING_PROFILEが違うので分岐しています。
あとは、~~~
gitで管理しているシェルスクリプトを呼び出すだけです。

```
#!/bin/bash -l
export WORKSPACE
sh -x ${WORKSPACE}/jenkins/main.sh $branch $server
```

_**追記**_
_cocoapodsで、```use_frameworks!```のようにフレームワークとしてライブラリを取り込むとxcodebuild時にPROVISIONING_PROFILEの指定をするとできません。そもそも```-exportOptionsPlist```を使っていればPROVISIONING_PROFILEの指定は必要ないので削除しました。どのPROVISIONING_PROFILEを利用するかはXcodeでのBuild Settings内のCode Signing Identitiyで設定しておけば良いです。



## Jenkins外のシェル

gitで管理します。それぞれ動く単位で別のシェルにします。 
```$shellFilePath``` はシェルファイルのパスです。
main.shは、それらシェルを呼び出すシェルになります。

```main.sh
#!/bin/bash -l
set -eux
: usage
: sh -x ~/jenkins/build.sh develop staging

: $1 $2
branch=$1
server=$2

shellFilePath="${WORKSPACE}/jenkins"

# クリーン処理
. ${shellFilePath}/clean.sh
clean
install

. ${shellFilePath}/setupVariable.sh
setupVar $branch $server
# suffix,buildType,fileName,groupAliasが設定される
. ${shellFilePath}/setupVariable.sh

. ${shellFilePath}/replaceVersion.sh
replaceVersion $suffix
# CFBundleShortVersionString,CFBundleVersionが設定される
. ${shellFilePath}/replaceVersion.sh

buildPath="${WORKSPACE}/build/${buildType}-iphoneos"

. ${shellFilePath}/archive.sh
archive $buildType "$buildPath"

. ${shellFilePath}/exportIpa.sh
exportIpa "$buildPath" "$fileName"
renameIpa "$buildPath" $fileName $CFBundleShortVersionString $CFBundleVersion
# newFileNameが設定される
. ${shellFilePath}/exportIpa.sh
newPathName="${buildPath}/$newFileName"
zipDSYM $fileName "$newPathName"

# crashlytics send
. ${shellFilePath}/sendFabric.sh
send "$newPathName" $groupAlias
```

### クリーンとCocoapodsの処理

以下を行います。

* ```xcodebuild clean```　と buildディレクトリ以下の削除
* ```pod install```

```clean.sh
#!/bin/bash -l
set -eux

function clean() {
    xcodebuild clean -workspace ${WORKSPACE}/＜プロジェクト名＞.xcworkspace \
    -scheme ＜プロジェクト名＞
    rm -rf ${WORKSPACE}/build/*
}

function install() {
    pushd ＜プロジェクト名＞
    pod install
    popd
}
```

### 変数設定処理

Job内の処理で利用する変数を設定します。
```$groupAlias``` はFabric Betaで配信するグループ名です。
```$suffix``` は表示されるバージョンのsuffixです。例：1.0.0.betaのbeta部分
```$buildType``` は、archive時の ```-configuration``` に指定する値。

ちなみにブランチ運用は通常developブランチで開発を行い、スプリントレビューの度にreviewブランチにPushして、総合テストが始まったらreleaseブランチにPushしています。

```setupVariable.sh
#!/bin/bash -l
set -ux

setupVar() {
    : usage
    : '$1=branch name, $2=server name'
    : $1 $2

    local branch=$1
    local server=$2

    # 変数設定
    if [ $server = "staging" ]; then
      buildType="Staging"
      fileName="ステージング"
      if [ $branch = "review" ]; then
        suffix="alpha"
        groupAlias='appteam,serverteam'
      elif [ $branch = "release" ]; then
        suffix="beta"
        groupAlias='pmteam,appteam,serverteam,planningteam'
      fi
    elif [ $server = "production" ]; then
      buildType="Release"
      fileName="本番環境"
      if [ $branch = "release" ]; then
        suffix="rc"
        groupAlias='pmteam,appteam,serverteam,planningteam'
      elif [ $branch = "master" ]; then
        suffix="gm"
        groupAlias='pmteam,appteam,serverteam,planningteam,manager'
      fi
    fi

    # 上記で設定されなかった場合(最初の「suffix=」は頭に:があればなくても良いがdebug用に記述)
    suffix=${suffix:-"develop"}
    buildType=${buildType:-"Debug"}
    fileName=${fileName:-"開発環境"}
    groupAlias=${groupAlias:-"appteam,serverteam"}
}
```

### 表示バージョン変更

Fabricで見たときにどの環境なのかがわかるようにバージョンにsuffixをつける処理です。
ビルド番号もJenkinsのビルド番号にします。

```replaceVersion.sh
#!/bin/bash -l
set -ux

replaceVersion(){
    : usage
    : 'arg $1=suffix'
    : $1

    local suffix=$1
    CFBundleShortVersionString=`/usr/libexec/PlistBuddy -c "Print CFBundleShortVersionString" ${WORKSPACE}/＜プロジェクト名＞/Supporting\ Files/＜プロジェクト名＞-Info.plist`
    CFBundleVersion=`/usr/libexec/PlistBuddy -c "Print CFBundleVersion" ${WORKSPACE}/＜プロジェクト名＞/Supporting\ Files/＜プロジェクト名＞-Info.plist`
    /usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString ${CFBundleShortVersionString}.${suffix}" ${WORKSPACE}/＜プロジェクト名＞/Supporting\ Files/＜プロジェクト名＞-Info.plist
    /usr/libexec/PlistBuddy -c "Set :CFBundleVersion ${BUILD_NUMBER}" ${WORKSPACE}/＜プロジェクト名＞/Supporting\ Files/＜プロジェクト名＞-Info.plist

    CFBundleShortVersionString=`/usr/libexec/PlistBuddy -c "Print CFBundleShortVersionString" ${WORKSPACE}/＜プロジェクト名＞/＜プロジェクト名＞/Supporting\ Files/＜プロジェクト名＞-Info.plist`
    CFBundleVersion=`/usr/libexec/PlistBuddy -c "Print CFBundleVersion" ${WORKSPACE}/＜プロジェクト名＞/Supporting\ Files/＜プロジェクト名＞-Info.plist`
}

```

### アーカイブ処理

2番目の引数の ```$buildPath`` にはビルドパスが入っています。（例: ${WORKSPACE}/build/Debug-iphoneos）
そこで指定した場所に xcarchive が作成されます。

```
#!/bin/bash -l
set -ux

archive() {
    : usage
    : 'arg $1=buildType $2=buildPath $3=fileName

    : $1 $2 $3
    local buildType=$1
    local buildPath=$2
    local fileName=$3

    # arhive処理
    xcodebuild -workspace "${WORKSPACE}/＜プロジェクト名＞/＜プロジェクト名＞.xcworkspace" \
    -scheme ＜プロジェクト名＞ \
    -configuration "${buildType}" \
    archive -archivePath "${buildPath}/${fileName}.xcarchive"
}
```

### IPA作成処理

xcarchiveからipaとそのdSYMを作成します。

```exportIpa.sh
#!/bin/bash -l
set -ux

exportIpa() {
    : usage
    : 'arg $1=buildPath $2=fileName $3=buildType
    : $1 $2 $3

    local buildPath=$1
    local fileName=$2
    local buildType=$3
    xcodebuild \
        -exportArchive -archivePath "${buildPath}/${fileName}.xcarchive" \
        -exportPath "${buildPath}" \
        -exportOptionsPlist "${WORKSPACE}/jenkins/exportOptions_${buildType}.plist"
}

renameIpa() {
    : usage
    : 'arg $1=buildPath $2=fileName $3=CFBundleShortVersionString $4=CFBundleVersion'
    : $1 $2 $3 $4

    local buildPath=$1
    local fileName=$2
    local CFBundleShortVersionString=$3
    local CFBundleVersion=$4
    local now=`date +"%Y.%m.%d"`

    mv "${buildPath}/<プロダクト名>.ipa" "${buildPath}/${fileName}-v${CFBundleShortVersionString}-${CFBundleVersion}-${now}.ipa"
    newFileName=${fileName}-v${CFBundleShortVersionString}-${CFBundleVersion}-${now}
}

zipDSYM() {
    : usage
    : 'arg $1=fileName $2=newPathName'
    : $1 $2

    local fileName=$1
    local newPathName=$2

    pushd $buildPath
    zip -r "${newPathName}.app.dSYM.zip" "${fileName}.xcarchive"
    popd
}
```

```exportOptions_Debug.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>method</key>
    <string>development</string>
</dict>
</plist>
```

```exportOptions_Staging.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>method</key>
    <string>ad-hoc</string>
</dict>
</plist>
```


### Fabricで転送

作成したIPAをあらかじめ登録していたグループ充に転送します。

```
#!/bin/bash -l
set -ux

send() {
    : usage
    : 'arg $1=newPathName $2=groupAlias'
    : $1 $2
    newPathName=$1
    groupAlias=$2

    $WORKSPACE/Pods/Crashlytics/submit <API_KEY> <BUILD_SECRET> -debug YES -groupAliases "${groupAlias}" -ipaPath "${newPathName}.ipa"
}
```