2021/05/12

Fastlaneを導入してリリース作業を自動化してみました

swiftfastlane

アプリのリリース関連の作業を自動化するためにfastlaneを導入しました。

iOS(Android)のビルド・テスト・リリース作業には毎度fastlaneを利用しているのですが、毎回毎回ハマって長時間作業するはめになるので、備忘録として作業メモを残しておきます。

具体的には、以下をコマンド1発で自動化するようにしました。(お金のあるプロジェクトだとCIサーバーを立てて、CIにコマンドを実行してもらう感じになります。)

  • fastlane beta というコマンドで、ビルドしてFirebase App Distributionにアプリをアップロード
  • fastlane deploy というコマンドで、ビルドしてApp Storeにアプリをアップロード

以下作業内容の詳細です。

fastlaneのインストール

事前にxcode commandlineツールをインストールしておきます。

% xcode-select --install

次にrbenv経由でrubyをインストールし、最終的にfastlaneをインストールしていきます。

以下注意が必要です。

  1. ruby versionをfastlaneの動作対象のものにする。(3.0 系で作業したらエラー頻発で動きませんでした。。。)
  2. fastlaneのversionは必ず最新のものにアップグレードして作業する。
# rubyをインストールします。
% rbenv install 2.7.1
% rbenv local 2.7.1
# bundlerをインストールします。
% rbenv exec gem install bundler
% rbenv exec bundler -v
Bundler version 2.2.17
# bundlerの設定ファイルを作成しておきます
% mkdir .bundle
% cat <<EOF > .bundle/config
---
BUNDLE_PATH: "vendor/bundle"
EOF
# Gemfileを作成してfastlaneをインストールします。
# (rexmlはインストール時にエラーを回避するために追加でインストールしました)
% cat <<EOF > Gemfile
source "https://rubygems.org"
gem 'rexml'
gem 'fastlane'
EOF
% rbenv exec bundle install
% rbenv exec bundle update fastlane
% rbenv exec bundle exec fastlane --version
-----------------------------
[✔] 🚀 
fastlane 2.182.0

fastlaneの初期化

% rbenv exec bundle exec fastlane init

match

署名関連の管理にfastlane matchを使います。

match用のgitリポジトリを作成

証明書管理用のGitリポジトリを作成します

https://github.com/your-company/your-app-certificatesみたいな名前で作成します。

fastlane matchの初期化

% rbenv exec bundle exec fastlane match init

fastlane/Matchfileにテンプレートができるので編集します。

git_url "[email protected]:your-company/your-app-certificates.git"
readonly false
type "development" # The default type, can be: appstore, adhoc or development
app_identifier ["com.your-company.your-app-id"]
username "[email protected]" # Your Apple Developer Portal username

firebase_app_distribution

アプリのテストにはFirebaseのApp Distributionを利用します。

fastlaneで利用するための設定を見ながら作業します。

firebase_app_distributionの設定

# pluginを追加します
% rbenv exec bundle exec fastlane actions | grep firebase
% rbenv exec bundle exec fastlane add_plugin firebase_app_distribution
% rbenv exec bundle exec fastlane actions | grep firebase
[✔] 🚀 
| fastlane-plugin-firebase_app_distribution | 0.2.7   | firebase_app_distribution_login firebase_app_distribution_get_udids firebase_app_distribution |
| firebase_app_distribution                   | Release your beta builds with Firebase App Distribution                             | Multiple                                                                                |
| firebase_app_distribution_get_udids         | Download the UDIDs of your Firebase App Distribution testers                        | Lee Kellogg                                                                             |
| firebase_app_distribution_login             | Authenticate with Firebase App Distribution using a Google account.                 | Manny Jimenez Github: mannyjimenez0810, Alonso Salas Infante Github: alonsosalasinfante |

# firebaseにログイン
% rbenv exec bundle exec fastlane run firebase_app_distribution_login
# gcpにログインして、サービスアカウトを作成し、jsonファイルの鍵を作成&ダウンロードして、GOOGLE_APPLICATION_CREDENTIALSにパスを設定する
% npm init
% npm install firebase-tools
% export PATH=${PATH}:`pwd`/node_modules/.bin
% firebase login

deliver

deliverの設定

% cat <<EOF > ./fastlane/Deliverfile
app_identifier "com.your-company.your-app-id"
username "[email protected]"
EOF

app storeにfastlaneから接続するためにapp_store_connect_api_keyを使います。

  • App Storeにログインして、App Store Connect APIのキーを作成して、以下の環境変数を予め設定しておきます。
name value
ASCAPI_KEY_ID key ID
ASCAPI_ISSUER_ID issuer ID
ASCAPI_KEY_CONTENT ダウンロードした鍵(p8ファイル)の内容をbase64でエンコードした文字列

register test device

  • fastlane/devices.txt にテストで登録したいデバイスのUDIDを予め以下のような内容で登録しておきます。
Device ID	Device Name
XXXXXXXX-XXXXXXXXXXXXXXXX	DeviceName

Fastfile

default_platform :ios

platform :ios do
  before_all do
    app_store_connect_api_key(
      key_id: ENV['ASCAPI_KEY_ID'],
      issuer_id: ENV['ASCAPI_ISSUER_ID'],
      key_content: ENV['ASCAPI_KEY_CONTENT'],
      is_key_content_base64: true,
      in_house: false,
    )
    register_devices(devices_file: './fastlane/devices.txt')
    # テスト環境ではapp idのsuffixを.devとかにしたいみたいな場合はここを変更します。
    # その場合はXCode側でScheme毎にapp idを変更したりする設定も必要です。
    match(type: "development", force_for_new_devices: true, app_identifier: "com.your-company.your-app-id")
    match(type: "appstore", force_for_new_devices: true, app_identifier: "com.your-company.your-app-id")
  end

  lane :beta do
    build(configuration: "Debug", export_method: "development")
    firebase_app_distribution(
      testers: "[email protected]",
      release_notes_file: "release-notes.txt"
    )
  end

  lane :deploy do
    # increment_build_number
    build(configuration: "Release", export_method: "app-store")
    version = get_version_number(xcodeproj: "YourApp.xcodeproj", target: "YourApp", configuration: "Release")
    deliver(
      app_identifier: "com.your-company.your-app-id",
      # app_icon: "./fastlane/metadata/app_icon.jpg",
      skip_screenshots: true,
      skip_metadata: true,
      skip_binary_upload: false,
      force: true,
      submit_for_review: false,
      app_version: version,
      languages: ["ja"],
      precheck_include_in_app_purchases: false,
    )
  end

  ############################# UTIL ##############################

  private_lane :build do |options|
    configuration = options[:configuration]
    exportMethod = options[:export_method]
    gym(
      scheme: "YourApp",
      export_method: exportMethod, # Method used to export the archive. Valid values are: app-store, ad-hoc, package, enterprise, development, developer-id
      configuration: configuration,
      clean: true,
      include_bitcode: false,
      output_directory: "../",
      output_name: "YourApp.ipa",
      xcargs: "ARCHIVE=YES" # Used to tell the Fabric run script to upload dSYM file
    )
  end

  after_all do |lane|
    File.delete("../../YourApp.ipa") if File.exist?("../../YourApp.ipa")
    File.delete("../../YourApp.app.dSYM.zip") if File.exist?("../../YourApp.app.dSYM.zip")
  end

  error do |lane, exception|
    # slack(
    #   message: exception.message,
    #   success: false
    # )
  end
end

References

以上です。