Fastlane은 모바일 앱 개발의 여러 빌드 및 배포 과정을 자동화하기 위한 도구입니다. iOS와 Android 플랫폼 모두를 지원하며, 여러가지 "lanes"라고 불리는 스크립트를 통해 테스트 자동화, 빌드 생성, 메타데이터 관리, 앱 스토어 배포 등을 손쉽게 처리할 수 있습니다.
Fastlane은 다음과 같은 기능을 제공합니다:
이 외에도 Fastlane은 플러그인 아키텍처를 지원하여 커뮤니티에서 다양한 플러그인을 개발하고 공유하고 있습니다.
이 글에서는 Fastlane의 Gym, Deliver, Supply 를 통해서 React Native 프로젝트를 Android, iOS 각각 빌드하여 Beta 트랙에 배포하는 방법을 설명합니다.
아래 명령어를 통해서 Fastlane을 설치합니다. MacOS 기준으로 작성되었습니다.
# Using RubyGems
sudo gem install fastlane -NV
# Using Homebrew
brew install fastlane
.gitignore
*.mobileprovision
*.cer
*.dSYM.zip
*.p12
*.certSigningRequest
.env
pc-api-*.json
사용자 및 액세스
를 클릭합니다키
탭을 클릭합니다Issuer ID
를 기록합니다Fastlane
제품 개발
키 ID
를 기록합니다API 키 파일
를 다운로드합니다.위 과정을 완료하면 Issuer ID
, 키 ID
, API 키 파일
을 가져올 수 있습니다.
위 내용을 .env에 기록합니다.
./ios/fastlane/.env
APP_STORE_CONNECT_ISSUER_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
APP_STORE_CONNECT_API_KEY_ID=xxxxxxxxxx
APP_STORE_CONNECT_API_KEY_CONTENT="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----"
APP_STORE_CONNECT_API_KEY_CONTENT
는\n
으로 구분하여 작성되어야 합니다.
cd ./ios
fastlane init
> 2
이어서 로그인을 진행하고, 팀이 여러개 있다면, App Store Connect teams 및 Developer Portal 선택 작업을 진행하게 됩니다.
설치를 마치면 기본적으로 다음과 같은 파일이 생깁니다.
./ios/fastlane/Fastfile
default_platform(:ios)
platform :ios do
desc "Push a new beta build to TestFlight"
lane :beta do
increment_build_number(xcodeproj: "MyApp.xcodeproj")
build_app(workspace: "MyApp.xcworkspace", scheme: "MyApp")
upload_to_testflight
end
end
기본 생성 Fastfile에는 다음과 같은 문제점이 있습니다.
1. 아카이브(archive)는 성공했지만, 프로비저닝 프로파일(provisioning profile) 매핑이 제공되지 않음
2. 사용자의 Apple 계정 로그인 상태에 따라서 실패할 수 있음
3. Marketing 버전을 설정할 수 없음
위 문제들을 해결하기 위해서 다음 단계를 진행합니다.
./ios/fastlane/Fastfile
require 'dotenv'
Dotenv.load
default_platform(:ios)
platform :ios do
desc "Push a new beta build to TestFlight"
def set_marketing_version(options)
return unless options[:version]
version = options[:version]
re = /\d+\.\d+\.\d+/
versionNum = version[re, 0]
if (versionNum)
increment_version_number(
version_number: versionNum
)
else
UI.user_error!("[ERROR] Wrong version!!!!!!")
end
end
lane :beta do |options|
cert
sigh(force: true)
increment_build_number(xcodeproj: "SmartParking.xcodeproj")
set_marketing_version(options)
build_app(workspace: "SmartParking.xcworkspace", scheme: "SmartParking")
api_key = app_store_connect_api_key(
key_id: ENV["APP_STORE_CONNECT_API_KEY_ID"],
issuer_id: ENV["APP_STORE_CONNECT_ISSUER_ID"],
key_content: ENV["APP_STORE_CONNECT_API_KEY_CONTENT"],
duration: 1200,
in_house: false
)
upload_to_testflight(
api_key: api_key
)
end
end
cd ./ios
fastlane beta version:1.0.0
+------+---------------------------+-------------+
| fastlane summary |
+------+---------------------------+-------------+
| Step | Action | Time (in s) |
+------+---------------------------+-------------+
| 1 | default_platform | 0 |
| 2 | cert | 2 |
| 3 | sigh | 4 |
| 4 | increment_build_number | 0 |
| 5 | increment_version_number | 0 |
| 6 | build_app | 207 |
| 7 | app_store_connect_api_key | 0 |
| 8 | upload_to_testflight | 329 |
+------+---------------------------+-------------+
[14:47:13]: fastlane.tools just saved you 9 minutes! 🎉
./android/local.properties
...
storeFile=/Users/user/keystore/MyApp.KEYSTORE
storePassword=
keyAlias=
keyPassword=
./android/app/build.gradle
...
def keystorePropertiesFile = rootProject.file("keystore.properties")
def keystoreProperties = new Properties()
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
android {
signingConfigs {
Properties properties = new Properties()
properties.load(new FileInputStream("$project.rootDir/local.properties"))
properties.each { prop ->
project.ext.set(prop.key, prop.value)
}
debug {
...
}
release {
storeFile file(properties['keystore'])
keyPassword properties['key_password']
keyAlias properties['key_alias']
storePassword properties['store_password']
}
}
buildTypes {
...
release {
...
signingConfig signingConfigs.release
}
}
}
pc-api-xxx.json
파일을 ./android/
경로에 복사cd ./ios
fastlane init
> Path to the json secret file: pc-api-xxx.json
> Download existing metadata and setup metadata management? (y/n) n
설치를 마치면 기본적으로 다음과 같은 파일이 생깁니다
./android/fastlane/Fastfile
default_platform(:android)
platform :android do
desc "Runs all the tests"
lane :test do
gradle(task: "test")
end
desc "Submit a new Beta Build to Crashlytics Beta"
lane :beta do
gradle(task: "clean assembleRelease")
crashlytics
# sh "your_script.sh"
# You can also use other beta testing services here
end
desc "Deploy a new version to the Google Play"
lane :deploy do
gradle(task: "clean assembleRelease")
upload_to_play_store
end
end
위 파일을 다음과 같이 수정합니다.
./android/fastlane/Fastfile
require 'dotenv'
Dotenv.load
default_platform(:android)
platform :android do
def increment_version_code()
path = '../app/build.gradle'
re = /versionCode\s+(\d+)/
s = File.read(path)
versionCode = s[re, 1].to_i
s[re, 1] = (versionCode + 1).to_s
f = File.new(path, 'w')
f.write(s)
f.close
end
def set_version_name(options)
return unless options[:version]
version = options[:version]
path = '../app/build.gradle'
re = /versionName\s+("\d+.\d+.\d+")/
s = File.read(path)
if version
s[re, 1] = "\"#{version}\""
f = File.new(path, 'w')
f.write(s)
f.close
end
end
desc "Submit a new Beta Build to Crashlytics Beta"
lane :beta do |options|
increment_version_code()
set_version_name(options)
gradle(task: "clean bundleRelease")
upload_to_play_store(
skip_upload_metadata: true,
skip_upload_changelogs: true,
skip_upload_screenshots: true,
skip_upload_images: true,
skip_upload_apk: true,
track: 'internal'
)
end
end
cd ./android
fastlane beta version:1.0.0
[19:32:48]: Successfully finished the upload to Google Play
+------+----------------------+-------------+
| fastlane summary |
+------+----------------------+-------------+
| Step | Action | Time (in s) |
+------+----------------------+-------------+
| 1 | default_platform | 0 |
| 2 | clean bundleRelease | 117 |
| 3 | upload_to_play_store | 38 |
+------+----------------------+-------------+
[19:32:48]: fastlane.tools finished successfully 🎉
./scripts/fastlane-beta.js
const { execSync } = require('child_process');
const packageJson = require('../package.json');
const version = packageJson.version;
const platform = process.argv[2]; // android or ios
if (platform === 'android' || platform === 'ios') {
const command = `cd ./${platform} && bundle exec fastlane beta version:${version}`;
execSync(command, { stdio: 'inherit' });
} else {
console.error("Invalid platform. Use 'android' or 'ios'");
}
./package.json
{
"version": "0.0.1",
...
"scripts": {
"beta:android": "node ./scripts/fastlane-beta.js android",
"beta:ios": "node ./scripts/fastlane-beta.js ios",
"beta": "yarn beta:android && yarn beta:ios"
}
}
package.json 의 version 수정 후
yarn beta
Exit status of command '/Users/kimjisub/GitHub/user-app/android/gradlew clean bundleRelease -p .' was 1 instead of 0.
FAILURE: Build failed with an exception.
* What went wrong:
Could not open settings generic class cache for settings file '/Users/kimjisub/GitHub/user-app/android/settings.gradle' (/Users/kimjisub/.gradle/caches/8.0.1/scripts/cxlxtcmt173waj7d1c80a10te).
> BUG! exception in phase 'semantic analysis' in source unit '_BuildScript_' Unsupported class file major version 64
* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights.
* Get more help at https://help.gradle.org
BUILD FAILED in 362ms
위의 경우 Java를 17버전으로 내리고 다시 실행하면 된다.
PlayStore에 아직 앱을 올린적이 없어서, 해당 패키지명이 등록되지 않아서 발생하는 오류이다.
./android/app/build/outputs/bundle/release/app-release.aab 파일을 최초 1번 PlayStore에 업로드하면 그 이후부터는 문제없이 업로드가 된다.
https://dev-yakuza.posstree.com/ko/react-native/fastlane
이 문서는 대부분 위 문헌을 참고하였으며, 실제로 설정을 하면서 필요했던 추가적인 내용을 덧대고, 사용하지 않는 부분을 가감하여 작성하였습니다.