이제 android/fastlane/Fastfile이 실제로 어떤 일을 하는지 lane 단위로 뜯어본다. 앞 편에서 만든 keystore·Service Account·.env 토대 위에 드디어 진짜 빌드·업로드 코드를 얹는 단계다.
구조는 iOS 편과 거의 대칭이다. 이 프로젝트는 iOS와 마찬가지로 환경(dev/beta/pro)을 셸 스크립트로 갈아끼우는 방식을 쓴다. (앞 설정 편에서 소개한 productFlavors 방식과는 다른 접근인데, 둘 다 현업에서 쓴다. 여기선 실제 코드에 맞춰 셸 스크립트 방식을 다룬다.)
세 lane 모두
internal트랙에draft(초안) 상태로만 올린다. pro(운영)조차 곧바로 Production으로 안 나간다. 업로드는 fastlane이 하되, "실제 출시" 버튼은 사람이 Play Console에서 누른다. 실수로 운영 배포가 나가는 사고를 막는 안전한 방식이다.
코드를 보기 전에, 바깥에 미리 있어야 하는 것들을 정리한다.
.env.<name>에서 주입)앞 편에서 만든 .env.dev / .env.beta / .env.pro가 넘겨주는 값이다. lane 안에서 ENV["..."]로 읽는다.
| 이름 | 용도 |
|---|---|
PACKAGE_NAME | 빌드할 환경의 패키지명 (예: com.yourcompany.myapp.dev, com.yourcompany.myapp) |
KEYSTORE_PATH | Upload Keystore 파일 경로 |
SIGNING_STORE_PASSWORD | keystore 비밀번호 |
SIGNING_KEY_ALIAS | 키 별칭 |
SIGNING_KEY_PASSWORD | 키 비밀번호 |
PLAY_STORE_JSON_PATH | Play Console 접근용 Service Account JSON 경로 |
환경변수 이름은 프로젝트가 정하는 거라 정답이 있는 건 아니다. (앞 설정 편에서 든 예시와 이름이 다를 수 있는데, 실제 값만 맞으면 된다.)
android/fastlane/에 같이 있는 bash 스크립트들이다. 환경 이름(dev/beta/pro)을 인자로 받아 네이티브 파일을 갈아끼운다. Fastfile은 sh "bash ./..."로 부른다.
| 스크립트 | 역할 |
|---|---|
change_package_name.sh <env> | 네이티브의 패키지명(applicationId)을 환경별 값으로 교체 |
change_appsflyer_name.sh <env> | AppsFlyer(마케팅 분석 SDK) 앱 이름·플래그 등 환경 분기 |
copy_config.sh <env> | google-services.json(Firebase 설정) 등을 환경별로 복사 |
iOS의
change_app_identifier.sh에 대응하는 게 여기선change_package_name.sh다. (iOS는 "번들 ID", Android는 "패키지명"이라 이름만 다르고 역할은 같다.)
이 Fastfile은 android_get_version_name / android_set_version_name / android_get_version_code / android_set_version_code를 쓰는데, 이건 fastlane 기본 액션이 아니라 플러그인(fastlane-plugin-versioning_android)이다. 없으면 "액션을 못 찾는다"는 에러가 나니, 먼저 설치해야 한다.
cd android
bundle exec fastlane add_plugin versioning_android
이 액션들은 android/app/build.gradle의 versionName·versionCode를 직접 읽고 쓴다.
android/fastlane/Appfile은 Play Console 인증 정보를 담는다. 환경마다 바뀌므로 ENV로 읽게 둔다.
# android/fastlane/Appfile
json_key_file(ENV["PLAY_STORE_JSON_PATH"])
package_name(ENV["PACKAGE_NAME"])
실제 lane들은
upload_to_play_store에json_key·package_name을 직접 넘기기도 한다. Appfile은 그 기본값을 잡아주는 역할이라, 둘 다 있어도 무방하다.
error 블록platform :android do 바로 안에, 모든 lane에 공통으로 걸리는 에러 핸들러가 있다.
platform :android do
# 빌드/배포 실패 시 작업트리를 항상 dev 상태로 복원
error do |lane, exception, options|
sh "bash ./change_package_name.sh dev"
sh "bash ./change_appsflyer_name.sh dev"
sh "bash ./copy_config.sh dev"
end
# ... lane 들 ...
end
이게 왜 중요한가. beta/pro 빌드는 중간에 패키지명·설정 파일을 beta/pro용으로 바꿔놓는다. 그런데 빌드가 중간에 실패하면 그 상태로 로컬이 멈춰버린다. 그러면 다음에 npm run android를 돌릴 때 엉뚱하게 beta 설정으로 켜지는 혼선이 생긴다. error 블록은 어느 lane이든 실패하는 순간 자동으로 dev로 되돌려 이 사고를 막는다. (iOS Fastfile엔 없던, Android 쪽에만 있는 안전장치다.)
| lane | 빌드 | 업로드 | 버전 정하는 방식 | git commit | 용도 |
|---|---|---|---|---|---|
apk | O (APK) | X | — | X | 로컬용 APK 하나 뽑기 |
dev | O (AAB) | internal/draft | 대화형(버전 유지 + 코드 +1 제안) | O | 개발용 배포 |
beta | O (AAB) | internal/draft | 대화형(patch +1 제안, 2회 확인) | O | 내부 QA용 배포 |
pro | O (AAB) | internal/draft | 코드에 직접 박음 | X | 운영용 빌드(이후 수동 승급) |
dev_mode | X | X | — | X | 로컬을 dev 환경으로 전환 + clean |
beta_mode | X | X | — | X | 로컬을 beta 환경으로 전환 + clean |
pro_mode | X | X | — | X | 로컬을 pro 환경으로 전환 + clean |
*_mode lane은 빌드/업로드 없이 환경 전환 + 빌드 폴더 청소만 한다. 예를 들어 로컬 Android Studio에서 직접 빌드·디버깅하면서 beta API/DB를 바라보고 싶을 때 bundle exec fastlane beta_mode --env beta를 쓴다.
세 배포 lane 모두 업로드 목적지가
internal트랙draft라는 점을 다시 강조한다. pro도 예외가 아니다. 출시는 Play Console에서 사람이 승급(promote)한다.
lane :apk — 로컬용 APK 한 방lane :apk do
gradle(task: "clean assembleRelease")
end
가장 단순한 lane이다. 서명·업로드 없이 그냥 Release APK 하나를 만든다. 테스터에게 파일로 직접 건네거나, 스토어를 안 거치고 설치해볼 때 쓴다. (assembleRelease는 AAB가 아니라 APK를 만든다.)
lane :dev — dev 빌드 → internal/draft 업로드lane :dev do
package_name = ENV["PACKAGE_NAME"]
keystore_path = ENV["KEYSTORE_PATH"]
store_password = ENV["SIGNING_STORE_PASSWORD"]
key_alias = ENV["SIGNING_KEY_ALIAS"]
key_password = ENV["SIGNING_KEY_PASSWORD"]
json_path = ENV["PLAY_STORE_JSON_PATH"]
sh "bash ./change_package_name.sh dev"
sh "bash ./change_appsflyer_name.sh dev"
sh "bash ./copy_config.sh dev"
# 버전 유지가 기본(versionCode만 +1), No 선택 시 수동 입력
current_version = android_get_version_name()
current_version_code = android_get_version_code()
suggested_version = current_version
suggested_version_code = current_version_code.to_i + 1
# ... UI.confirm 으로 자동/수동 분기 ...
android_set_version_name(version_name: new_version)
android_set_version_code(version_code: new_version_code)
gradle(task: "clean")
gradle(
task: "bundle",
build_type: "Release",
print_command: false,
properties: {
"android.injected.signing.store.file" => keystore_path,
"android.injected.signing.store.password" => store_password,
"android.injected.signing.key.alias" => key_alias,
"android.injected.signing.key.password" => key_password,
}
)
upload_to_play_store(
track: 'internal',
release_status: 'draft',
aab: "app/build/outputs/bundle/release/app-release.aab",
package_name: package_name,
version_name: new_version,
version_code: new_version_code,
json_key: json_path,
skip_upload_apk: true,
skip_upload_metadata: true,
skip_upload_images: true,
skip_upload_screenshots: true
)
git_commit(
path: ".",
message: "deploy(APP-00): android dev - #{new_version}(#{new_version_code})",
skip_git_hooks: false
)
end
단계별로 풀면 이렇다.
dev 인자로 호출해 패키지명·AppsFlyer·Firebase 설정을 dev용으로 교체한다.versionName/versionCode를 읽어 "마케팅 버전은 그대로, versionCode만 +1"을 제안한다. UI.confirm에서 Yes면 그 값, No면 UI.input으로 직접 입력받는다. (versionCode를 매번 올리므로 Play의 "같은 버전 코드 거부"에 안 걸린다.)android_set_version_name / android_set_version_code로 build.gradle에 기록한다.gradle(task: "clean")으로 한번 청소하고, gradle(task: "bundle", build_type: "Release")로 AAB를 만든다. 서명 정보는 android.injected.signing.* 프로퍼티로 빌드 시점에 주입한다.upload_to_play_store로 internal 트랙 draft에 올린다. (자세한 옵션은 5번에서)deploy(APP-00): android dev - {버전}({코드}) 메시지로 커밋한다. (APP-00은 이슈 키 자리표시자다.)
android.injected.signing.*가 핵심이다. 이걸 쓰면build.gradle에signingConfigs를 안 넣어도, fastlane이 빌드할 때 keystore를 그 자리에서 꽂아준다.print_command: false는 이 비밀번호가 콘솔·CI 로그에 찍히지 않게 가리는 옵션이다.
lane :beta — patch 자동 증가 + 2회 확인 + dev 복원 + commitbeta는 dev와 비슷하되 확인을 두 번 받고, 끝나면 dev로 되돌린 뒤 커밋한다.
increment_patch_version으로 마케팅 버전 맨 끝자리를 +1, versionCode는 +1 제안.UI.user_error!로 중단.UI.confirm. 두 번 확인으로 잘못된 버전 배포를 예방한다.beta.gradle clean이 없다).dev로 호출해 로컬을 되돌린다. (빌드 직후 beta 상태로 남는 걸 방지. 실패 시엔 2번의 error 블록이 대신 처리한다.)deploy(APP-00): android beta - ....lane :pro — 운영용 빌드beta와 거의 같지만 대화형·자동 증가·git commit이 전부 빠지고, 버전을 코드에 직접 박는다.
lane :pro do
# ... ENV 읽기 ...
version_name = "2.0.3" # ← pro는 매 배포 전 이 두 값을 직접 수정
version_code = 37
sh "bash ./change_package_name.sh pro"
sh "bash ./change_appsflyer_name.sh pro"
sh "bash ./copy_config.sh pro"
android_set_version_name(version_name: version_name)
android_set_version_code(version_code: version_code)
gradle(task: "bundle", build_type: "Release", print_command: false, properties: { ... })
upload_to_play_store(
track: 'internal', # ← pro도 internal/draft로 올림 (곧바로 운영 배포 X)
release_status: 'draft',
aab: "app/build/outputs/bundle/release/app-release.aab",
package_name: package_name,
version_name: version_name,
version_code: version_code,
json_key: json_path,
skip_upload_apk: true, skip_upload_metadata: true,
skip_upload_images: true, skip_upload_screenshots: true
)
# pro -> dev 복원 (pro는 git_commit 없음)
sh "bash ./change_package_name.sh dev"
sh "bash ./change_appsflyer_name.sh dev"
sh "bash ./copy_config.sh dev"
end
version_name·version_code를 매 배포 전에 직접 수정해야 한다. dev/beta 같은 자동 증가/입력 분기가 없다.lane :dev_mode / :beta_mode / :pro_mode — 환경 전환 전용빌드·업로드 없이 로컬을 해당 환경으로 전환하고 빌드 폴더를 청소만 한다.
lane :dev_mode do
# ... (주석 처리된 android_set_version_* 있음: 필요 시 수동 버전 고정용)
sh "bash ./change_package_name.sh dev"
sh "bash ./change_appsflyer_name.sh dev"
sh "bash ./copy_config.sh dev"
sh "rm -rf ../app/build ../build" # 빌드 산출물 청소
gradle(task: "clean")
end
용도: 로컬 Android Studio에서 직접 빌드·디버깅하면서 특정 환경(예: beta DB·beta API)을 바라보게 하고 싶을 때.
| 액션 | 한 줄 설명 |
|---|---|
gradle | ./gradlew <task> 실행. task: "bundle" + build_type: "Release" → bundleRelease(AAB) |
android_get_version_name / _code | build.gradle의 현재 versionName / versionCode 조회 (플러그인) |
android_set_version_name / _code | versionName / versionCode 설정 (플러그인) |
upload_to_play_store (= supply) | Service Account JSON으로 AAB를 Play Console 트랙에 업로드 |
git_commit | 변경 파일들을 묶어 git commit (Fastlane 내장) |
gradle 서명 주입gradle(
task: "bundle", # bundle=AAB, assemble=APK
build_type: "Release",
print_command: false, # 비밀번호가 인자로 들어가니 로그 숨김
properties: {
"android.injected.signing.store.file" => keystore_path,
"android.injected.signing.store.password" => store_password,
"android.injected.signing.key.alias" => key_alias,
"android.injected.signing.key.password" => key_password,
}
)
android.injected.signing.*는 gradle 표준 주입 프로퍼티다. build.gradle을 안 건드리고도 빌드 시점에 서명 정보를 넣을 수 있다.
upload_to_play_store 옵션 풀이upload_to_play_store(
track: 'internal', # 업로드할 트랙 (여기선 항상 internal)
release_status: 'draft', # draft=초안. 사람이 Play Console에서 최종 출시
aab: "app/build/outputs/bundle/release/app-release.aab", # 빌드된 AAB 경로
package_name: package_name, # 어느 앱(패키지)에 올릴지
version_name: new_version,
version_code: new_version_code,
json_key: json_path, # Service Account JSON
skip_upload_apk: true, # APK는 안 올림 (AAB만)
skip_upload_metadata: true, # 스토어 설명 등 메타데이터 건드리지 않음
skip_upload_images: true, # 아이콘/피처 그래픽 안 건드림
skip_upload_screenshots: true # 스크린샷 안 건드림
)
aab경로가app/build/outputs/bundle/release/app-release.aab로 고정돼 있는 건, 이 프로젝트가 flavor 없이 셸 스크립트로 환경을 바꾸기 때문이다. 그래서 빌드 변형이 하나뿐이라 출력 경로가 항상 같다. (flavor를 쓰면 경로가.../devRelease/app-dev-release.aab처럼 달라져서, 뒤 "개선 방향"의lane_context방식이 필요해진다.)
지금은 몰라도 되고, 규모가 커지면 이렇게 다듬을 수 있다는 정도만 알아두면 된다.
1) versionCode를 Play 기준으로 자동 계산. 지금은 로컬 build.gradle 값을 +1 하는데, google_play_track_version_codes로 "이미 올라간 최대 코드 + 1"을 가져오면 여러 명이 배포해도 충돌이 안 난다. iOS의 latest_testflight_build_number + 1과 같은 발상이다.
latest = google_play_track_version_codes(
package_name: ENV["PACKAGE_NAME"], track: "internal", json_key: ENV["PLAY_STORE_JSON_PATH"]
).max || 0
new_version_code = latest + 1
2) AAB 경로를 하드코딩하지 않기. aab: lane_context[SharedValues::GRADLE_AAB_OUTPUT_PATH]를 쓰면 직전 gradle이 만든 AAB 경로를 그대로 받아온다. flavor를 도입해 경로가 바뀌어도 안전하다.
3) 복붙을 private_lane으로 묶기. dev/beta/pro의 겹치는 "환경전환 → 빌드 → 업로드"를 private_lane :build_and_upload 하나로 모으면, 새 환경 추가와 수정이 한 곳에서 끝난다.
터미널에서 매번 길게 치기 번거로우니 단축 스크립트를 등록한다. iOS 것과 합쳐 정리하면 이렇다.
{
"scripts": {
"deploy-ios-dev": "cd ios && bundle exec fastlane dev --env dev",
"deploy-ios-beta": "cd ios && bundle exec fastlane beta --env beta",
"deploy-ios-pro": "cd ios && bundle exec fastlane pro --env pro",
"deploy-android-dev": "cd android && bundle exec fastlane dev --env dev",
"deploy-android-beta": "cd android && bundle exec fastlane beta --env beta",
"deploy-android-pro": "cd android && bundle exec fastlane pro --env pro",
"set-android-dev": "cd android && bundle exec fastlane dev_mode --env dev",
"set-android-beta": "cd android && bundle exec fastlane beta_mode --env beta",
"set-android-pro": "cd android && bundle exec fastlane pro_mode --env pro"
}
}
deploy-android-* → 실제 빌드·업로드까지 하는 dev/beta/pro laneset-android-* → 로컬 환경만 전환(+clean)하는 *_mode lane초안에서는
set-android-dev가 곧 "빌드 & 업로드"인 것처럼 적혀 있었는데, 실제로는*_mode가 환경 전환만 한다. 실제 배포는dev/beta/prolane(deploy-android-*)이다. 이 둘을 헷갈리면 "업로드했는데 스토어에 안 뜬다" 같은 상황이 생긴다.
dev 환경으로 먼저 검증한다.
pnpm deploy-android-dev
# 또는: cd android && bundle exec fastlane dev --env dev
대략 이런 흐름이 뜬다.
[fastlane] Driving the lane 'android dev' 🚀
[fastlane] Loading from '.env.dev'
(패키지명·설정 dev로 전환)
(버전/버전코드 확인 프롬프트 → Yes 또는 직접 입력)
[gradle] $ ./gradlew clean
[gradle] $ ./gradlew bundle -Pandroid.injected.version.code=... Release
[gradle] BUILD SUCCESSFUL
[supply] Uploading AAB to internal (draft)...
[supply] Successfully uploaded ✅
Play Console의 Internal testing 트랙에서 draft 상태로 빌드가 보이면 성공이다. 실제 테스터에게 풀거나 운영으로 올리는 건, 거기서 사람이 검토 후 승급하면 된다.
dev/beta는 중간에 버전 확인 프롬프트가 떠서 사람이 입력해줘야 한다. 완전 무인 CI에 넣으려면 이 대화형 부분을 환경변수로 우회하도록 고쳐야 한다. (이 프로젝트는 로컬에서 사람이 돌리는 걸 전제로 짜여 있다.)
iOS와 마찬가지로 Hermes(JS 엔진)는 RN 0.70+ 기본이고, gradle 빌드 때 자동 활성화된다. fastlane에서 추가 설정할 건 없다. (New Architecture를 쓰더라도 동일하다.)
가끔 release 빌드인데 JS 번들이 안 들어가는 경우가 있다. android/app/build.gradle에 RN gradle 플러그인이 적용돼 있는지 확인한다. (RN 0.71+ 템플릿엔 기본 포함이다.)
apply from: file("../../node_modules/@react-native/gradle-plugin/react-native-gradle-plugin/build.gradle")
이 플러그인이 bundleRelease 때 JS 번들을 만들어 assets에 넣어준다.
기본 RN 설정은 모든 ABI(arm64-v8a 등)를 한 파일에 담은 universal AAB를 만든다. Play가 AAB에서 디바이스별로 잘라 배포하므로, 별도 ABI splits 설정은 불필요하다. (APK 배포하던 시절 가이드를 따라 splits를 켜면 오히려 문제가 생기니 주의.)
캐시가 꼬여 빌드가 깨질 때가 있다. *_mode lane이 rm -rf ../app/build ../build + gradle clean을 하는 이유가 이거다. 배포 lane에서 강제 클린을 옵션화하려면:
gradle(task: "clean") if ENV["CLEAN_BUILD"] == "true"
Package name xxx not found
Play Console에 그 패키지명 앱이 등록돼 있지 않다. 환경별 앱을 따로 만들었는지, 패키지명 오타가 없는지 확인한다.
Only releases with status draft may be created on draft app
그 앱에 아직 한 번도 수동 업로드를 안 한 상태다. Play Console에서 최소 1회 수동 업로드를 해야 API 업로드가 열린다. (앞 편에서 강조한 그 전제다.)
Version code XX has already been used
versionCode 충돌이다. 코드 +1이 제대로 도는지, 환경을 잘못 지정해 다른 패키지의 코드를 가져오지 않았는지 확인한다.
keystore 비밀번호 인증 실패
환경변수가 안 들어온 경우가 많다.
cd android
echo $SIGNING_STORE_PASSWORD # 빈 값이면 .env 로딩 실패
.env 위치가 android/fastlane/.env인지 확인한다 (android/.env 아님).
The caller does not have permission
Service Account 권한 부족이다. Play Console → Setup → API access에서 권한과, 특히 App permissions에 해당 앱이 등록됐는지 재확인한다.
aapt2 ... daemon / 메모리 부족
android/gradle.properties에서 JVM 메모리를 늘린다.
org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=512m
Could not find action 'android_get_version_name'
versioning 플러그인이 안 깔린 것이다. 1번의 add_plugin versioning_android를 실행한다.
지금까지 뜯어본 코드의 전체 모습이다. 프로젝트에 맞게 커밋 스코프(APP-00)와 pro 버전 값만 바꿔 쓰면 된다.
# This file contains the fastlane.tools configuration
# 문서: https://docs.fastlane.tools
# update_fastlane
default_platform(:android)
# 패치 버전 자동 증가 함수 ("1.2.11" → "1.2.12")
def increment_patch_version(current_version)
version_parts = current_version.split('.')
major = version_parts[0].to_i
minor = version_parts[1].to_i
patch = version_parts[2].to_i + 1
"#{major}.#{minor}.#{patch}"
end
platform :android do
# 빌드/배포 실패 시 작업트리를 항상 dev 상태로 복원 (pro/beta 찌꺼기 방지)
error do |lane, exception, options|
sh "bash ./change_package_name.sh dev"
sh "bash ./change_appsflyer_name.sh dev"
sh "bash ./copy_config.sh dev"
end
desc "create apk"
lane :apk do
gradle(task: "clean assembleRelease")
end
desc "dev build"
lane :dev do
package_name = ENV["PACKAGE_NAME"]
keystore_path = ENV["KEYSTORE_PATH"]
store_password = ENV["SIGNING_STORE_PASSWORD"]
key_alias = ENV["SIGNING_KEY_ALIAS"]
key_password = ENV["SIGNING_KEY_PASSWORD"]
json_path = ENV["PLAY_STORE_JSON_PATH"]
sh "bash ./change_package_name.sh dev"
sh "bash ./change_appsflyer_name.sh dev"
sh "bash ./copy_config.sh dev"
# 버전 유지가 기본(versionCode만 +1), No 선택 시 수동 입력
current_version = android_get_version_name()
current_version_code = android_get_version_code()
suggested_version = current_version
suggested_version_code = current_version_code.to_i + 1
UI.message("========================================")
UI.message("현재 버전: #{current_version} (#{current_version_code})")
UI.message("자동 배포 버전: #{suggested_version} (#{suggested_version_code})")
UI.message("========================================")
if UI.confirm("자동 버전(#{suggested_version})으로 배포하시겠습니까? (No 선택시 수동 입력)")
new_version = suggested_version
new_version_code = suggested_version_code
else
new_version = UI.input("배포할 버전을 입력하세요 (예: 1.2.12): ")
if new_version.empty?
UI.user_error!("버전이 입력되지 않았습니다. 배포가 취소되었습니다.")
end
version_code_input = UI.input("배포할 버전 코드를 입력하세요 (예: 108): ")
if version_code_input.empty?
UI.user_error!("버전 코드가 입력되지 않았습니다. 배포가 취소되었습니다.")
end
new_version_code = version_code_input.to_i
end
android_set_version_name(version_name: new_version)
android_set_version_code(version_code: new_version_code)
gradle(task: "clean")
gradle(
task: "bundle",
build_type: "Release",
print_command: false,
properties: {
"android.injected.signing.store.file" => keystore_path,
"android.injected.signing.store.password" => store_password,
"android.injected.signing.key.alias" => key_alias,
"android.injected.signing.key.password" => key_password,
}
)
upload_to_play_store(
track: 'internal',
release_status: 'draft',
aab: "app/build/outputs/bundle/release/app-release.aab",
package_name: package_name,
version_name: new_version,
version_code: new_version_code,
json_key: json_path,
skip_upload_apk: true,
skip_upload_metadata: true,
skip_upload_images: true,
skip_upload_screenshots: true
)
git_commit(
path: ".",
message: "deploy(APP-00): android dev - #{new_version}(#{new_version_code})",
skip_git_hooks: false
)
end
desc "beta build"
lane :beta do
package_name = ENV["PACKAGE_NAME"]
keystore_path = ENV["KEYSTORE_PATH"]
store_password = ENV["SIGNING_STORE_PASSWORD"]
key_alias = ENV["SIGNING_KEY_ALIAS"]
key_password = ENV["SIGNING_KEY_PASSWORD"]
json_path = ENV["PLAY_STORE_JSON_PATH"]
# 현재 버전 조회 + 패치 버전 증가
current_version = android_get_version_name()
current_version_code = android_get_version_code()
suggested_version = increment_patch_version(current_version)
suggested_version_code = current_version_code.to_i + 1
UI.message("========================================")
UI.message("현재 버전: #{current_version} (#{current_version_code})")
UI.message("자동 배포 버전: #{suggested_version} (#{suggested_version_code})")
UI.message("========================================")
if UI.confirm("자동 버전(#{suggested_version})으로 배포하시겠습니까? (No 선택시 수동 입력)")
new_version = suggested_version
new_version_code = suggested_version_code
else
new_version = UI.input("배포할 버전을 입력하세요 (예: 1.2.12): ")
if new_version.empty?
UI.user_error!("버전이 입력되지 않았습니다. 배포가 취소되었습니다.")
end
version_code_input = UI.input("배포할 버전 코드를 입력하세요 (예: 108): ")
if version_code_input.empty?
UI.user_error!("버전 코드가 입력되지 않았습니다. 배포가 취소되었습니다.")
end
new_version_code = version_code_input.to_i
end
UI.message("최종 배포 버전: #{new_version} (#{new_version_code})")
unless UI.confirm("버전 #{new_version} (#{new_version_code})으로 배포를 진행하시겠습니까?")
UI.user_error!("배포가 취소되었습니다.")
end
sh "bash ./change_package_name.sh beta"
sh "bash ./change_appsflyer_name.sh beta"
sh "bash ./copy_config.sh beta"
android_set_version_name(version_name: new_version)
android_set_version_code(version_code: new_version_code)
gradle(
task: "bundle",
build_type: "Release",
print_command: false,
properties: {
"android.injected.signing.store.file" => keystore_path,
"android.injected.signing.store.password" => store_password,
"android.injected.signing.key.alias" => key_alias,
"android.injected.signing.key.password" => key_password
}
)
upload_to_play_store(
track: 'internal',
release_status: 'draft',
aab: "app/build/outputs/bundle/release/app-release.aab",
package_name: package_name,
version_name: new_version,
version_code: new_version_code,
json_key: json_path,
skip_upload_apk: true,
skip_upload_metadata: true,
skip_upload_images: true,
skip_upload_screenshots: true
)
# beta -> dev 복원
sh "bash ./change_package_name.sh dev"
sh "bash ./change_appsflyer_name.sh dev"
sh "bash ./copy_config.sh dev"
git_commit(
path: ".",
message: "deploy(APP-00): android beta - #{new_version}(#{new_version_code})",
skip_git_hooks: false
)
end
desc "pro build"
lane :pro do
package_name = ENV["PACKAGE_NAME"]
keystore_path = ENV["KEYSTORE_PATH"]
store_password = ENV["SIGNING_STORE_PASSWORD"]
key_alias = ENV["SIGNING_KEY_ALIAS"]
key_password = ENV["SIGNING_KEY_PASSWORD"]
json_path = ENV["PLAY_STORE_JSON_PATH"]
# pro는 매 배포 전 아래 두 값을 직접 수정
version_name = "2.0.3"
version_code = 37
sh "bash ./change_package_name.sh pro"
sh "bash ./change_appsflyer_name.sh pro"
sh "bash ./copy_config.sh pro"
android_set_version_name(version_name: version_name)
android_set_version_code(version_code: version_code)
gradle(
task: "bundle",
build_type: "Release",
print_command: false,
properties: {
"android.injected.signing.store.file" => keystore_path,
"android.injected.signing.store.password" => store_password,
"android.injected.signing.key.alias" => key_alias,
"android.injected.signing.key.password" => key_password,
}
)
upload_to_play_store(
track: 'internal',
release_status: 'draft',
aab: "app/build/outputs/bundle/release/app-release.aab",
package_name: package_name,
version_name: version_name,
version_code: version_code,
json_key: json_path,
skip_upload_apk: true,
skip_upload_metadata: true,
skip_upload_images: true,
skip_upload_screenshots: true
)
# pro -> dev 복원 (pro는 git_commit 없음)
sh "bash ./change_package_name.sh dev"
sh "bash ./change_appsflyer_name.sh dev"
sh "bash ./copy_config.sh dev"
end
desc "dev mode (환경 전환 + clean)"
lane :dev_mode do
# 필요 시 주석 해제해 수동으로 버전 고정
# android_set_version_name(version_name: "0.1.0")
# android_set_version_code(version_code: 1)
sh "bash ./change_package_name.sh dev"
sh "bash ./change_appsflyer_name.sh dev"
sh "bash ./copy_config.sh dev"
sh "rm -rf ../app/build ../build"
gradle(task: "clean")
end
desc "beta mode (환경 전환 + clean)"
lane :beta_mode do
sh "bash ./change_package_name.sh beta"
sh "bash ./change_appsflyer_name.sh beta"
sh "bash ./copy_config.sh beta"
sh "rm -rf ../app/build ../build"
gradle(task: "clean")
end
desc "pro mode (환경 전환 + clean)"
lane :pro_mode do
sh "bash ./change_package_name.sh pro"
sh "bash ./change_appsflyer_name.sh pro"
sh "bash ./copy_config.sh pro"
sh "rm -rf ../app/build ../build"
gradle(task: "clean")
end
end
이번 편을 끝낸 시점의 상태는 다음과 같다.
internal/draft까지 자동 업로드error 블록이 dev로 자동 복원android.injected.signing.*로 build.gradle 수정 없이 서명 주입pnpm deploy-android-*(배포) / pnpm set-android-*(환경 전환) 단축 스크립트