Jenkins Pipeline iOSプロジェクトの自動配信化をする

## この記事の内容
jenkinsのpipelineで
- 開始と終了のSlackの通知
- Gitでのソースの取得
- CocoaPodsの更新
- ipaの作成
- ipaの成果物の保存
- AppleStore（TestFlight） or FireBaseDistributionでの配信

を自動化してやってしまおうという内容です。

## 前提条件
- Xcodeコマンドラインツールをインストール済み
- WORKSPACE直下にXcodeProjectファイルがある状態
- XcodeBuildでipa書き出し用にExportOptions.plistが必要
    - 他の方の記事になりますが、[こちら](https://qiita.com/taisuke_h/items/e37d96c96a811b630c0c)を参考にして作成するとわかりやすいです。 
    - Organizerから一度DistributionAppをすればExportOptions.plistがExportされるのでそれを使うのも可
- 使用するXcodeでAppleDeveloperアカウントのログイン済み
- AppleDeveloperアカウントにてAppPWを発行しておく※「AppleStore」に送信する場合のみ
- FireBaseCLIを導入し、配信を行うのID,PWにてログイン済み※「Firebase Distribution」をする場合のみ


## PipeLine
各変数を修正すれば環境を整えれるようにしております。
まずはPipeLineの文をすべて載せ、下記にて一つづつ解説を載せていきます。

```
node("NodeName") {
    // Slack Settings
    def strSlackChannel = "#sample_channel"
    def strSlackMessageJob = "Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]' (${env.BUILD_URL})"
    def strSlackCredentialId = "" 

    // Git Settings
    def strGitRepo = "https://hoge.com/git/fugo.git"
    def strBranch = "*/branch"
    def strCredentialsId = "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
  
    // Xcode Settings
    def strXcodeVersion = "13.4.1"
    def strXcodePath = "/Applications/xcode/Xcode_${xcodeVersion}.app/Contents/Developer"

    // Project Settings
    def strConfiguration = "Release"
    def strScheme = "Scheme"
    def strWorkSpacePath = "${WORKSPACE}/WorkSpace.xcworkspace"
    def strExportPath = "${WORKSPACE}/build"
    def strArchivePath = "${WORKSPACE}/build/${strScheme}.xcarchive"
    def strIpaPath = "${strExportPath}/${strScheme}.ipa"
    def strExportOptionsPath = "${WORKSPACE}/build_plist/ExportOptions.plist"

    // TestFlight Settings
    def strAppleID = "sample@sample.co.jp"
    def strAppleAppPW = "XXXX-XXXX-XXXX-XXXX"

    // Firebase Settings
    def strFirebaseAppID = "1:1234567890:ios:1234567890"
    def strFirebaseDistributionGroup = "GroupName"
            
    try {
        // Slackへの開始通知
        slackSend channel: "${strSlackChannel}", color: 'FFFF00', message: "START JOB : ${strSlackMessageJob}", tokenCredentialId: "${strSlackCredentialId}"
        
        stage ("Checkout") {
            def scm = checkout(
                [$class: 'GitSCM', branches: [[name: strBranch]],
                  doGenerateSubmoduleConfigurations: false, 
                  extensions: [[$class: 'CloneOption', depth: 0, noTags: false, reference: '', shallow: false, timeout: 60]],
                  submoduleCfg: [],
                  userRemoteConfigs: [[credentialsId: strCredentialsId, url: strGitRepo]]])
        }
        
        stage("Pod Install"){
            sh "cd ${WORKSPACE} &&" +
            "LANG=en_US.UTF-8 pod install"
        }


        stage ("Build iOS") {
            sh "export DEVELOPER_DIR=${strXcodePath} &&" +
            "cd ${WORKSPACE} &&" +
            "agvtool new-version -all ${env.BUILD_NUMBER} &&" +
            "xcodebuild -sdk iphoneos -workspace ${strWorkSpacePath} -scheme ${strScheme} -configuration ${strConfiguration} -archivePath ${strArchivePath} clean archive &&"+
            "xcodebuild -exportArchive -archivePath ${strArchivePath} -exportPath ${strExportPath} -exportOptionsPlist ${strExportOptionsPath}"
        }
        
        stage ("Archive Artifacts") {
            archiveArtifacts allowEmptyArchive: true, artifacts: '**/*.ipa'
        }
        
        stage ("Upload Apple Store") {
            sh "xcrun altool --upload-app -f ${strIpaPath} -t ios -u ${strAppleID} -p ${strAppleAppPW}"
        }
    
        stage ("Firebase Distribution") {
            sh "firebase appdistribution:distribute ${strIpaPath} --app ${strFirebaseAppID} --groups ${strFirebaseDistributionGroup} --debug"
        }
        
        // Slackへの成功通知など
        slackSend channel: "${strSlackChannel}", color: '0000FF', message: "SUCCESS JOB : ${strSlackMessageJob}", tokenCredentialId: "${strSlackCredentialId}"

    }catch (e) {
        // Slackへの失敗通知など
        slackSend channel: "${strSlackChannel}", color: 'FF0000', message: "FAILED JOB : ${strSlackMessageJob}", tokenCredentialId: "${strSlackCredentialId}"
        throw e
    }
}
```

## 各種解説
### Node
コマンドを実行するNode名を設定する
```
node("NodeName")
```

### 定数の解説
#### Slack Settings
Slack通知を行う場合のみ、不要なら消してください。
またjenkinsにSlack通知用のプラグイン(Slack Notification)を追加する必要があります。
```
    // Slack Settings
    def strSlackChannel = "#sample_channel"
    def strSlackMessageJob = "Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]' (${env.BUILD_URL})"
    def strSlackCredentialId = "" 
```
- strSlackChannel : 通知を行うチャンネル
- strSlackMessageJob : Slackに送るJobについての情報メッセージ
- strSlackCredentialId : Jenkinsの設定画面より登録をしたJenkinsのSlackの送信用ID

#### Git Settings
Gitの設定
strCredentialsIdのみJenkinsにて事前に登録をしておく必要あり
```
   // Git Settings
    def strGitRepo = "https://hoge.com/git/fugo.git"
    def strBranch = "*/branch"
    def strCredentialsId = "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
```
- strGitRepo : GitのURL
- strBranch : ビルドを行うGitのBranch名
- strCredentialsId : Jenkinsの設定画面より登録をしたJenkinsのGitの接続用ID

#### Xcode Settings
Xcodeの設定
Xcodeバージョンを指定する場合のみ必要
下記のPathをそのまま使う場合は「/Applications/xcode」のフォルダ内にXcode14.1の場合は「Xcode_14.1.app」に名前を変更してインストールする必要あり
```
    // Xcode Settings
    def strXcodeVersion = "13.4.1"
    def strXcodePath = "/Applications/xcode/Xcode_${strXcodeVersion}.app/Contents/Developer"
```
- strXcodeVersion : 指定のXcodeのバージョン
- strXcodePath : 使用する「Xcode.app」の「/Contents/Developer」を設定する

#### Project Settings
XcodeProjectの設定
今回はWorkSpaceでの設定で説明をしていく。
必用があれば「.xcodeproj」に書き換え、「xcodebuild -workspace」の部分を書き換えると良い　
```
    // Project Settings
    def strConfiguration = "Release"
    def strScheme = "Scheme"
    def strWorkSpacePath = "${WORKSPACE}/WorkSpace.xcworkspace"
    def strExportPath = "${WORKSPACE}/build"
    def strArchivePath = "${WORKSPACE}/build/${strScheme}.xcarchive"
    def strIpaPath = "${strExportPath}/${strScheme}.ipa"
    def strExportOptionsPath = "${WORKSPACE}/build_plist/ExportOptions.plist"
```
- strConfiguration : XcodeProjectのConfiguration、初期であれば Debug or Release
- strScheme : ManageSchemesにて指定のtargetに指定したScheme名。Show+Shardをしておく必要あり
- strWorkSpacePath : XcodeProjectのxcworkspaceのパスを設定する。
- strExportPath : ipaファイルがExportされる用のフォルダパスを設定する。
- strArchivePath : xcarchiveファイルがExportされるパスを設定する。
- strIpaPath : ipaファイルのExportされた先のパスを設定する。
- strExportOptionsPath : 「xcodebuild -exportArchive」用に設定するPlistファイル

#### 配信関連の設定
TestFlightの設定とFirebaseDistributionの設定
必要があればお使いください。
```
    // TestFlight Settings
    def strAppleID = "sample@sample.co.jp"
    def strAppleAppPW = "XXXX-XXXX-XXXX-XXXX"

    // Firebase Settings
    def strFirebaseAppID = "1:1234567890:ios:1234567890"
    def strFirebaseDistributionGroup = "GroupName"
```
- strAppleID : 配信を行うAppleアカウントのIDを設定する。
- strAppleAppPW : 配信を行うAppleアカウントのAppパスワードを設定する。 ※通常ログインで使用するPWではない
    - [こちら](https://support.apple.com/ja-jp/HT204397)を参考に「App用パスワード」を作成する。

- strFirebaseAppID : FirebaseプロジェクトのAppIDを設定する。
- strFirebaseDistributionGroup : Firebaseプロジェクト内に作成をした配信グループIDを設定する。

### 各ステージの解説
#### Slack関連
SCMをポーリングなど、ビルドトリガを設定している場合に
jenkinsが走っているのかの確認が取りづらいためSlackで通知をするようにしている。
開始時は黄色、成功時は緑、失敗時は赤と見た目でもわかりやすいようにしている。
slackSendの設定で指定のメッセージにスタンプを付けるような動作もできるため、開始のメッセージに成功か失敗かでスタンプを付けるような仕様にするのも良いとは思います。
```
slackSend channel: "${strSlackChannel}", color: 'FFFF00', message: "START JOB : ${strSlackMessageJob}", tokenCredentialId: "${strSlackCredentialId}"

slackSend channel: "${strSlackChannel}", color: '0000FF', message: "SUCCESS JOB : ${strSlackMessageJob}", tokenCredentialId: "${strSlackCredentialId}"

slackSend channel: "${strSlackChannel}", color: 'FF0000', message: "FAILED JOB : ${strSlackMessageJob}", tokenCredentialId: "${strSlackCredentialId}"
```

#### Checkout
GitのCheckoutを行う
「SCMをポーリング」などを使うことが出来るのでこちらの方法でGitのCheckOutをすると、Gitのコミットをキャッチしてビルドをするなどが楽に実行できます。
```
    stage ("Checkout") {
        def scm = checkout(
            [$class: 'GitSCM', branches: [[name: strBranch]],
              doGenerateSubmoduleConfigurations: false, 
              extensions: [[$class: 'CloneOption', depth: 0, noTags: false, reference: '', shallow: false, timeout: 60]],
              submoduleCfg: [],
              userRemoteConfigs: [[credentialsId: strCredentialsId, url: strGitRepo]]])
    }
```
Pipeline Syntaxのcheckoutで設定できる項目を一部変数化してカスタムしたもの

#### Pod Install
cocoaPodsプロジェクトのため`pod install`を使っていますが、carthageやxcodeproj単体の場合は基本不要
```
    stage("Pod Install"){
        sh "cd ${WORKSPACE} &&" +
        "LANG=en_US.UTF-8 pod install"
    }
```

#### Build iOS
```
    stage ("Build iOS") {
        sh "export DEVELOPER_DIR=${strXcodePath} &&" +
        "cd ${WORKSPACE} &&" +
        "agvtool new-version -all ${env.BUILD_NUMBER} &&" +
        "xcodebuild -sdk iphoneos -workspace ${strWorkSpacePath} -scheme ${strScheme} -configuration ${strConfiguration} -archivePath ${strArchivePath} clean archive &&"+
        "xcodebuild -exportArchive -archivePath ${strArchivePath} -exportPath ${strExportPath} -exportOptionsPlist ${strExportOptionsPath}"
    }
```
- 「export DEVELOPER_DIR=${strXcodePath}」 : ビルドを実行をするXcodeの指定
    - `xcode-select --switch`でも可能だが、一時の使用のためexportを使用している。
    - Xcodeのバージョンの指定をする必要がない場合には省略可
- `agvtool new-version -all ${env.BUILD_NUMBER} ` : XcodeのBuildNumberをjenkinsで使用しているBuildNumberに設定する。
- `xcodebuild -sdk iphoneos -workspace ${strWorkSpacePath} -scheme ${strScheme} -configuration ${strConfiguration} -archivePath ${strArchivePath} clean archive`: xcarchiveを作成する。
- `xcodebuild -exportArchive -archivePath ${strArchivePath} -exportPath ${strExportPath} -exportOptionsPlist ${strExportOptionsPath}` : xcarchiveからipaを作成する。Distortion Appの設定などはExportOptions.plistに記載をする必要が有る。

#### Archive Artifacts
成果物の保存用
主にipaファイルを保存しておく用として使用している。
```
    stage ("Archive Artifacts") {
        archiveArtifacts allowEmptyArchive: true, artifacts: '**/*.ipa'
    }
```

#### Apple Store
作成をしたipaファイルをAppStoreにuploadをする。
```
    stage ("Upload Apple Store") {
        sh "xcrun altool --upload-app -f ${strIpaPath} -t ios -u ${strAppleID} -p ${strAppleAppPW}"
    }
```

#### FireBase
FireBaseのdistributionにuploadをする。
```
    stage ("Firebase Distribution") {
        sh "firebase appdistribution:distribute ${strIpaPath} --app ${strFirebaseAppID} --groups ${strFirebaseDistributionGroup} --debug"
    }
```
FirebaseCLIの導入については[こちら](https://firebase.google.com/docs/cli)を参考に導入をしてください。

### 参考URL
- [xcodebuildの-exportOptionsPlist の設定項目を一通り調べてみました](https://qiita.com/taisuke_h/items/e37d96c96a811b630c0c)
- [App 用パスワードを使って Apple ID で App にサインインする](https://support.apple.com/ja-jp/HT204397)
- [FirebaseCLIリファレンス](https://firebase.google.com/docs/cli)