fastlaneを使ったTestFlight・App Store ConnectおよびPlayStoreへの申請用のコードuploadの自動化

# 概要

appstoreおよびplay storeにデプロイを自動化するコマンドを定義できます。
弊社環境では同じプロジェクトにios,android両方のアプリを設定しているため、同時に設定しています。

# fastlaneの環境変数設定

fastlane/.env または、 fastlane/.env.defaultに設定するとfastlane実行時に読まれます。
以下例。

```
FASTLANE_USER=
FASTLANE_PASSWORD=
FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD=
SLACK_URL=
MATCH_PASSWORD=
FASTLANE_SESSION=
ENV=
```

上記はAppleの二段階認証などに使われるパラメータなどを設定しています。


fastlaneを使うと証明書発行とbuildおよび、TestFlightおよび、App Store Connectのコードuploadまでの処理を自動化できます。
手っ取り早く設定のtemplateは以下のように作成しています。
以下の例ではデプロイ失敗に備えて、increment_version_numberとcommitの順番を制御できるように設定していますが書き方は自由です。
build versionはアプリがappstore connectにアップロードされている場合は同じbuild versionでコードをあげられなくるため、incrementし続けるようにしていますが、versionの更新と同時に１に戻すようにしたりもできます。


```ruby:Fastfile
fastlane_version '2.129.0'

default_platform :ios

before_all do |lane|
  ensure_git_branch(branch: '(develop|release|\d{0,4}\.\d{0,4}\.\d{0,4})')
  ensure_git_status_clean
  git_pull
  if lane == :release or lane == :beta
    ENV["ENV"] = 'production'
  elsif lane == :appcenter
    ENV["ENV"] = 'staging'
  else
    ENV["ENV"] = 'development'
  end
end

platform :ios do
 
  # iOS Lanes
  desc 'Fetch certificates and provisioning profiles'
  lane :certificates do |options|
    if ENV["ENV"] == 'production'
      match(app_identifier: 'jp.sample.app', type: 'appstore', readonly: true)
    elsif ENV["ENV"] == 'staging'
      match(app_identifier: 'jp.sample.staging.app', type: 'adhoc',)
    else
      match(app_identifier: 'jp.sample.app', type: 'development')
    end
  end

  desc 'Build each Scheme'
  lane :build do |options|
    Dir.chdir("../") do
      # code here runs in the parent directory
      sh('./appcenter-pre-build.sh')
    end
    cocoapods(
      podfile: "./ios/Podfile"
    )
    certificates
    if ENV["ENV"] == 'production'
      gym(scheme:'sample-production', workspace:'./ios/sample.xcworkspace',export_method: "app-store")
    elsif ENV["ENV"] == 'staging'
      gym(scheme:'sample-staging', workspace:'./ios/sample.xcworkspace',export_method: "ad-hoc")
    else
      gym(scheme:'sample-development', workspace:'./ios/sample.xcworkspace',export_method: "development")
    end
    increment_build_number(xcodeproj: './ios/sample.xcodeproj')
  end

  desc 'deploy to TestFlight'
  lane :beta do |options|
    build
    commit_version_bump(message: 'success test flight commit', xcodeproj: './ios/sample.xcodeproj')
    pilot
    push_to_git_remote
  end

  desc 'deploy to app store connect'
  lane :release do |options|
    build
    unless options[:no_version_up]
      if options[:version]
        increment_version_number(
          version_number: options[:version],
          xcodeproj: './ios/sample.xcodeproj'
        )
      else
        bump_type =  options[:bump_type] || 'minor'
        increment_version_number(bump_type: bump_type, xcodeproj: './ios/sample.xcodeproj')
      end
      commit_version_bump(message: 'release commit', xcodeproj: './ios/sample.xcodeproj')
    end
    deliver
  end
end

platform :android do
  # Android Lanes
# Android Lanes
  desc "Deploy a new version to the Google Play"
  lane :release do |options|
    gradle(task: 'clean', project_dir: 'android/')
    increment_version(options)
    gradle(task: 'bundle', build_type: 'Release', project_dir: 'android/')
    supply(package_name: 'jp.sample.app',track: 'beta', track_promote_to: 'beta', skip_upload_apk: true)
    git_commit(path: './android/*', message: "upload aab google play")
    push_to_git_remote
  end

  desc "version increment"
  lane :increment_version do |options|
    # bump_type: major,minor,patch
    gradle(task: 'incrementVersion', properties: { version_number: options[:version_number], bump_type: options[:bump_type] }, project_dir: 'android/')
  end
end


after_all do |lane|
  slack(
    success: true,
    channel: 'fastlane',
    message: "#{lane} has suceeded."
  )
end

error do |lane, exception, options|
  if options[:debug]
    puts "some output"
  else
    slack(
      success: false,
      channel: 'fastlane',
      message: "#{lane}:#{exception.to_s} :\n#{exception.backtrace}"
    )
  end
end
```

appcenter-pre-build.shはAppcenterでbuildの初期設定を行なっている、自前のスクリプトを使いまわして実行しています。このように任意の設定processを組み込めます。

次に、Appfileで、アプリの情報とアプリをビルドするApple IDアカウントの情報を設定しておきます。

```ruby:Appfile
app_identifier "jp.sample.app" # The bundle identifier of your app
apple_id "hoge@example.com" # Your Apple email address

team_id "fugfuga"  # Developer Portal Team ID

for_platform :ios do
  app_identifier "jp.sample.debug.app"
  for_lane :release do
    app_identifier "jp.sample.app"
  end
  for_lane :beta do
    app_identifier "jp.sample.app"
  end
end
```

matchファイルはコマンド実行時に自動で生成されます。
既存の証明書を使いたい場合やエフェメラルに使いたくない場合は、certとsighを使うと、mathの代用ができます。


```ruby:Matchfile
git_url("git@github.com:hoge/samplerepo.git")

type("development") # The default type, can be: appstore, adhoc, enterprise or development

# app_identifier(["tools.fastlane.app", "tools.fastlane.app2"])
# username("user@fastlane.tools") # Your Apple Developer Portal username

# For all available options run `fastlane match --help`
# Remove the # in the beginning of the line to enable the other options
```

## Android 

ぶっちゃけgradleのビルドタスクが同じ役割を果たすので、fastlaneで設定しなくても良いのですが、デプロイのメンテ性の向上やgitへのコミットpushをiOSと管理を共通化する程度の利用目的で使っています。

### service accoutの登録

以下の手順を実行して、service accoutを取得したのちにAppfileに適応します。

https://docs.fastlane.tools/getting-started/android/setup/

```Appfile
json_key_file "./fastlane/example.service-account.json"
```


###

androidはincrement versionのapiが存在しないので、自前で実装する。
以下はgradle.propertiesからversionNameとversionCodeを取得し、global変数にexportするコード。

```app/incrementVersion.gradle

def setVersion(bump_type) {
    def versionNums = VERSION_NAME.split('\\.')
    def result = VERSION_NAME
    println(versionNums.length)
    println(VERSION_NAME)
    switch (bump_type) {
        case 'patch':
            versionNums[2] =  (Integer.parseInt(versionNums[2]) + 1).toString()
            result = versionNums.join('.')
            break
        case 'minor':
            versionNums[1] = (Integer.parseInt(versionNums[1]) + 1).toString()
            result = versionNums.join('.')
            break
        case 'major':
            versionNums[0] = (Integer.parseInt(versionNums[0]) + 1).toString()
            result = versionNums.join('.')
            break
        default:
            break
    }
    result
}


task('incrementVersion') {
    description= "Increments Version"
    doLast {
        def versionCode = Integer.parseInt(VERSION_CODE) + 1
        def versionName = version_number ?: setVersion(bump_type)
        ant.propertyfile(file: "../gradle.properties") {
            entry(key: "VERSION_CODE", value: versionCode)
            entry(key: "VERSION_NAME", value: versionName)
        }
    }
}
```

以上のコードをapp/build.gradleから読み込む。

```app/build.gradle
// ...略
apply from: './incrementVersion.gradle'
// ...略
defaultConfig {
   //...
   versionCode Integer.parseInt(VERSION_CODE)
   versionName VERSION_NAME
}
```

これでbuildのgradleタスクを設定して呼び出すと、バージョン更新が適用されます。
supplyのパラメータを変えるとリリースプロセスを選べます。

## その他補足

### laneについて

fastlaneにおけるプロセスの単位。rubyのProc形式で拡張定義できる。

### 既存のlane

- cocoapods: pod install
- match: 証明書の取得及びsigning
- gym: ios appのビルド設定
- pilot: TestFlightへのuploade
- deliver: App Connect Storeへのuploade
- increment_build_number: xcode projectのビルド回数を変更する
- increment_version_number: Bundle versions string, shortのバージョン番号を変更する
- commit_version_bump: fastlaneの実行中に発生したdiffをコミットメッセージを指定してコミットしてくれる。