fastlaneを用いたCircleCIによるReactNativeアプリのビルドを自動化した話
こんにちは、harashun11です。
この記事は、airCloset Advent Calendar 2021 の6日目に寄稿させていただいてます。
フリーランスエンジニアとして活動している中での、株式会社エアークローゼット様(以下、エアクロ)の開発の一部を紹介させていただきます。
概要
エアクロでは、ベトナムオフショアを活用させていただいたり、テスターを外部にアウトソーシングしているので人の出入りが多く、PLを含めた非エンジニアの皆さんがプロジェクト単位でアサインされるため、実機で動作確認するまでの連携が結構面倒になってました。
なので、
誰がコミットしても、誰でもすぐ実機で確認できる
を要件とし、
fastlaneを用いたCircleCIによるReactNativeアプリビルド自動化
の対応をしたので、その内容を紹介します。
開発環境
ReactNativeでios/android向けのアプリを提供していて、CI環境としては、CircleCIを使ってます。
また、動作確認方法としては、ステージング環境を向き先としたアプリをDeployGateに展開してプロジェクトに関わる人に共有してます。
対応方針
fastlaneの利用
fastlaneを使います。
Ruby製のツールで、ドキュメントがしっかり整っているのと、iOSアプリビルド時のかゆいところにも手が届くので、fastlaneを採用しました。そのかゆいところについては、以下がわかりやすかったです。
特に、エアクロでは前述通りオフショアを活用させていただいているので、エンジニアの出入りが多くあると想定され、iOS開発用証明書の発行からセットアップは結構面倒になるなと考えられました。
なので、Github Teamsに依存したread/write権限付与と、Github Repositoryに暗号化したProvisioning Profileを置くことで必要な証明書は各自取得できるようになることはかなり作業効率が上がるので、メリットと感じました。
CircleCIでmacOS VMが必要
iosアプリビルドをする際にxcodeが必要なので、maxOS VMを利用します。
その際、CircleCIのプランをupgradeする必要があります。
エアクロのワークフローに適応する
Staging向けアプリをDeployGateにデプロイして、社内メンバーなどが動作確認してます。
そのため、DeployGateへのアップロードまでをCircleCI上で実現します。
また、Slack上でコミュニケーションを取っているので、アップロード成功やエラーの通知はSlackへ通知します。
ReactNativeにfastlane導入
Gemfile
source "https://rubygems.org"
# setup_circle_ci action was released in 2.107.0
gem 'fastlane', '>= 2.191'
できる限り共通環境とするためGemfileで導入してます。
ReactNativeアプリのroot dirに置きました。
fastlane
fastlaneでは、インタラクティブモードで指定しながら動かすことができますが、CircleCI上で動かしたいので、ある程度指定する必要があるのと、簡単に動くようにtaskをios/androidで別々に表現します。
android/fastlane
androidは簡単です。
ビルドには、fastlane上で、gradleが用意されているので、利用するだけです。
Fastfile全部見たい人はこちらをクリック
fastlane_version "2.191.0"
default_platform :android
platform :android do
before_all do
# https://docs.fastlane.tools/best-practices/continuous-integration/circle-ci/
setup_circle_ci
end
desc "Build a new version for release"
lane :build_release do
gradle(
task: "assemble",
build_type: "Release"
)
end
desc "Build a new version to upload to deploygate"
lane :build_for_deploygate do
commit = last_git_commit
build_release
deploygate(
api_token: ENV["DEPLOYGATE_API_TOKEN"],
user: ENV["DEPLOYGATE_USER"],
apk: "./app/build/outputs/apk/release/app-release.apk",
message: "uploaded with fastlane, branch: #{git_branch}, last commit message: #{commit[:message]}, last commit hash: #{commit[:abbreviated_commit_hash]}"
)
slack(
message: "assembleReleaseなandroidアプリビルドとdeploygateへのアップロードに成功しました",
payload: {
"App Name" => "{YOUR_APP_NAME}"",
"Circle Build Url" => ENV["CIRCLE_BUILD_URL"]
}
)
end
desc "Build a new version to upload to beta"
lane :build_for_beta do
build_release
# 省略
end
# error block is executed when a error occurs
error do |lane, exception|
puts exception
slack(
# message with short human friendly message
message: exception.to_s,
success: false,
# Output containing extended log output
payload: {
"Output" => exception.to_s,
"Circle Build Url" => ENV["CIRCLE_BUILD_URL"]
}
)
end
end
ios/fastlane
iosは結構面倒です。
ビルド時に、Provisioning Profileが必要となってきます。
Fastfile全部見たい人はこちらをクリック
fastlane_version "2.191.0"
default_platform :ios
platform :ios do
before_all do
# https://docs.fastlane.tools/best-practices/continuous-integration/circle-ci/
setup_circle_ci
end
desc "Build a new version for adhoc"
lane :build_adhoc do |options|
sync_code_signing(type: "adhoc", readonly: true)
update_code_signing_settings(
path: "{YOUR_APP_NAME}.xcodeproj",
code_sign_identity: ENV["IOS_CODE_SIGN_ID"],
profile_name: ENV["IOS_ADHOC_PROFILE_NAME"],
use_automatic_signing: false,
)
build_ios_app(
export_method: "ad-hoc",
output_directory: "./fastlane/build/adhoc/",
export_options: {
provisioningProfiles: {
"{YOUR_APP_BUNDLE_ID}" => ENV["IOS_ADHOC_PROFILE_NAME"]
}
}
)
update_code_signing_settings(
path: "{YOUR_APP_NAME}.xcodeproj",
code_sign_identity: ENV["IOS_CODE_SIGN_ID"],
profile_name: ENV["IOS_ADHOC_PROFILE_NAME"],
use_automatic_signing: true,
)
end
desc "Build a new version to upload to deploygate"
lane :build_for_deploygate do
commit = last_git_commit
build_adhoc
deploygate(
api_token: ENV["DEPLOYGATE_API_TOKEN"],
user: ENV["DEPLOYGATE_USER"],
ipa: "./fastlane/build/adhoc/aircloset.ipa",
message: "uploaded with fastlane, branch: #{git_branch}, last commit message: #{commit[:message]}, last commit hash: #{commit[:abbreviated_commit_hash]}"
)
slack(
message: "adhoc用のiosアプリビルドとdeploygateへのアップロードに成功しました",
payload: {
"App Name" => "{YOUR_APP_NAME}",
"Circle Build Url" => ENV["CIRCLE_BUILD_URL"]
}
)
end
desc "Build a new version for appstore"
lane :build_appstore do
# 省略
end
desc "Build a new version to upload to testflight"
lane :build_for_testflight do
# 省略
end
# error block is executed when a error occurs
error do |lane, exception|
slack(
# message with short human friendly message
message: exception.to_s,
success: false,
# Output containing extended log output
payload: {
"Output" => exception.to_s,
"Circle Build Url" => ENV["CIRCLE_BUILD_URL"]
}
)
end
end
sync_code_signing(type: "adhoc", readonly: true)
Github Repository上に存在するProvisioning Profileにアクセスします。readonly
とすることで、write権限を必要とせずread権限を持つアカウントがProvisioning Profileを取得することができます。
なので、初めてローカルでiosアプリビルドするために、Provisioning Profileを必要とする際には、githubアカウントでread権限を持っていれば、
bundle exec fastlane match { development|adhoc|appstore } --readonly
CLI上で取得できます。便利。
ちなみに、matchはsync_code_signing
のエイリアスです。
update_code_signing_settings(
path: "{YOUR_APP_NAME}.xcodeproj",
code_sign_identity: ENV["IOS_CODE_SIGN_ID"],
profile_name: ENV["IOS_ADHOC_PROFILE_NAME"],
use_automatic_signing: false,
)
ビルド時にProvisioning Profileを指定するため、update_code_signing_settingsのuse_automatic_signing: false
にしてからbuildする必要があります。(use_automatic_signing: true
に戻しているのは、ローカルでも動かせるようにしているためです。)
このあたりは参考になりました。
以上、fastlane側の設定でした。
ちなみに、ENV指定することで、CircleCI > Project Settings > Environment Variables に設定して呼び出せます。ローカルで使うときはdotenv&direnvでも入れると良いと思います。
Matchfileをいろいろ指定してたりしますが、そんなに重要じゃないので、割愛させていただきます。
CircleCI上でfastlane動かす
circleciの設定全部見た人はこちらをクリック
version: 2.1
orbs:
node: circleci/node@4.7.0
android: circleci/android@1.0.3
executors:
ios-build-machine:
macos:
xcode: "12.5.1"
commands:
setup_to_build:
description: "アプリビルドに必要な共通セットアップ処理"
steps:
## 省略 - node_modulesのinstallなどが必要です。##
## install gems ##
- restore_cache:
keys:
- gem-v1-{{ arch }}-{{ .Branch }}-{{ checksum "Gemfile.lock" }}
- run: bundle check || bundle install --path vendor/bundle
- save_cache:
key: gem-v1-{{ arch }}-{{ .Branch }}-{{ checksum "Gemfile.lock" }}
paths:
- vendor/bundle
setup_to_build_ios:
description: "iosビルドに必要な共通セットアップ処理"
steps:
- run:
name: jsbundle init
command: yarn ios-bundle # npx react-native bundle --entry-file index.js --platform ios --dev false --bundle-output ios/main.jsbundle --assets-dest ios
## install cocoapods ##
- restore_cache:
keys:
- cocoapods-v1-{{ checksum "ios/Podfile.lock" }}
- cocoapods-v1
- run:
name: pod install
command: cd ios && bundle exec pod install
- save_cache:
key: cocoapods-v1-{{ checksum "ios/Podfile.lock" }}
paths:
- ios/Pods
setup_to_build_android:
description: "androidビルドに必要な共通セットアップ処理"
steps:
## build ##
- run:
name: yarn android-bundle
command: yarn android-bundle # npx react-native bundle --platform android --dev false --entry-file index.js --bundle-output android /app/src/main/assets/index.android.bundle
no_output_timeout: 30m
setup_env_to_stg:
description: "stgの環境変数をセットする"
steps:
- run:
name: set env stg
command: grep -v '^\s*#' env.staging | grep -v '^\s*$' | while read line; do echo "export ${line}" >> $BASH_ENV; done
- run:
name: reload BASH_ENV
command: |
source $BASH_ENV
echo $AC_API_URL
## setup_env_to_prod: 省略 ##
jobs:
## npm_dependencies: 省略 ##
## test: 省略 ##
build_ios_stg:
executor: ios-build-machine
working_directory: /Users/distiller/project
steps:
- checkout
- setup_env_to_stg
- setup_to_build
- run:
name: set env to staging
command: yarn switch-config:stg
- setup_to_build_ios
## build ##
- run:
name: fastlane ios for staging
command: cd ios && bundle exec fastlane build_for_deploygate
- run:
name: zip
command: cd ios/fastlane/build && zip -r ipa.zip adhoc
when: always
- store_artifacts:
path: /Users/distiller/project/ios/fastlane/build
build_android_stg:
executor:
name: android/android-machine
resource-class: medium
environment:
JVM_OPTS: -Xmx3200m
GRADLE_OPTS: '-Dorg.gradle.daemon=false -Dorg.gradle.jvmargs="-Xmx3200m -XX:+HeapDumpOnOutOfMemoryError"'
working_directory: ~/project
steps:
- checkout
- setup_env_to_stg
- setup_to_build
- run:
name: set env to staging
command: yarn switch-config:stg
- setup_to_build_android
- run:
name: fastlane android for staging
command: cd android && bundle exec fastlane build_for_deploygate
- run:
name: zip
command: cd android/app/build/outputs && zip -r apk.zip apk
when: always
- store_artifacts:
path: /home/circleci/project/android/app/build/outputs
## build_ios_prod: 省略 ##
## build_android_prod: 省略 ##
workflows:
version: 2
build-test:
jobs:
- test:
requires:
- npm_dependencies
- build_ios_stg:
requires:
- test
filters:
branches:
only:
- /feature.*/
- master
- build_ios_prod:
filters:
tags:
only: /.*/
branches:
ignore: /.*/
- build_android_stg:
requires:
- test
filters:
branches:
only:
- /feature.*/
- master
- build_android_prod:
filters:
tags:
only: /.*/
branches:
ignore: /.*/
version: 2.1
orbs:
node: circleci/node@4.7.0
android: circleci/android@1.0.3
versionを2.1にしてorbs使います。
gradleもcircleci/android使えば、defaultでinstall済みっぽいです。
commands:
setup_to_build_ios:
description: "iosビルドに必要な共通セットアップ処理"
steps:
- run:
name: jsbundle init
command: yarn ios-bundle # npx react-native bundle --entry-file index.js --platform ios --dev false --bundle-output ios/main.jsbundle --assets-dest ios
## install cocoapods ##
- restore_cache:
keys:
- cocoapods-v1-{{ checksum "ios/Podfile.lock" }}
- cocoapods-v1
- run:
name: pod install
command: cd ios && bundle exec pod install
- save_cache:
key: cocoapods-v1-{{ checksum "ios/Podfile.lock" }}
paths:
- ios/Pods
最終的にappstoreへのアップロードもするので、iosビルドの共通セットアップ処理として切り出してます。
ReactNativeでiosビルドするため、npx react-native bundle
をする必要がありました。
android側も同様です。
job:
build_ios_stg:
executor: ios-build-machine
working_directory: /Users/distiller/project
steps:
- checkout
- setup_env_to_stg
- setup_to_build
- run:
name: set env to staging
command: yarn switch-config:stg
- setup_to_build_ios
## build ##
- run:
name: fastlane ios for staging
command: cd ios && bundle exec fastlane build_for_deploygate
- run:
name: zip
command: cd ios/fastlane/build && zip -r ipa.zip adhoc
when: always
- store_artifacts:
path: /Users/distiller/project/ios/fastlane/build
iosディレクトリ下で、bundle exec fastlane build_for_deploygate
することで、先に示したfastlane/Fastlaneを見に行って動いてくれます。
また、ipaファイルをARTIFACTSに吐き出すことで、DeployGateにアカウント招待せずとも、ベトナムオフショア側もDeployGateで共有しているアプリを簡単に手に入れることが可能となってます。(これが一番みんなに喜ばれてます。)
job:
(省略)
build_android_stg:
executor:
name: android/android-machine
resource-class: medium
environment:
JVM_OPTS: -Xmx3200m
GRADLE_OPTS: '-Dorg.gradle.daemon=false -Dorg.gradle.jvmargs="-Xmx3200m -XX:+HeapDumpOnOutOfMemoryError"'
working_directory: ~/project
steps:
(省略)
android側で注意すべきなのは、JVM_OPTS
とGRADLE_OPTS
の指定です。
メモリ確保しないと普通にこけます。ひとまず上記のように設定すればいけました。
workflows:
version: 2
build-test:
jobs:
- test:
requires:
- npm_dependencies
- build_ios_stg:
requires:
- test
filters:
branches:
only:
- /feature.*/
- master
- build_ios_prod:
filters:
tags:
only: /.*/
branches:
ignore: /.*/
- build_android_stg:
requires:
- test
filters:
branches:
only:
- /feature.*/
- master
- build_android_prod:
filters:
tags:
only: /.*/
branches:
ignore: /.*/
ワークフローとしては、test成功前提でビルドが動くようにしてます。こうすることで、並列にビルド動くので、かなりの時間削減にもつながります。
また、featureブランチ、masterブランチへのコミットがあった場合にのみbuild_ios_stgとbuild_android_stgが動くようにしてます。
そして、tag pushすることで、今回は省略しているbuild_ios_prodとbuild_android_prodが動くようにしてます。
まとめ
並列でビルドができ、環境構築など面倒ごとの削減ができ、かなり作業効率アップに寄与できたかなと思います。
(コード共有で長い部分はアコーディオンで畳んでるので、気になる方はそちらをくりっくしてください。)
さいごに
ちなみに、私はバックエンドな方のエンジニアなので、あまりReactNativeを触ったことがない中で開発しました。
ありがたいことに、エアクロでは、私のようなフリーランスで入っているエンジニアにも多くの新しい経験をさせてくれます。
皆様もご興味あればぜひぜひ!->採用サイト
Discussion