fastlane과 Jenkins로 iOS TestFlight 빌드 넘버 충돌 막기
TestFlight에 IPA를 업로드할 때 “이미 제출된 빌드 넘버” 에러가 간헐적으로 발생했다.
기존 방식의 문제점
섹션 제목: “기존 방식의 문제점”기존에는 latest_testflight_build_number로 특정 버전의 최신 빌드 넘버를 가져와 +1 하는 단순한 방식이었다.
latest_testflight_build_number(version) → +1 → 다음 빌드 넘버이 방식은 다음 상황에서 빌드 넘버가 충돌한다.
| 상황 | 설명 |
|---|---|
| Xcode 직접 업로드 | TestFlight API에 아직 반영되지 않은 빌드 존재 |
| TestFlight 처리 중 | 제출되었지만 처리 완료 전인 빌드 존재 |
| API 응답 이상 | 최근 업로드된 비정상 빌드 넘버가 반환되는 경우 |
TestFlight API 반영 지연이나 처리 중 상태 때문에 조회한 값이 실제보다 낮을 수 있고, 그 값으로 업로드하면 “이미 제출된 build number”로 reject된다.
빌드 넘버 결정 로직
섹션 제목: “빌드 넘버 결정 로직”자동 계산 (기본)
섹션 제목: “자동 계산 (기본)”두 값을 비교해 더 높은 값에 1을 더한다.
next_build_number = max(TestFlight, Local) + 1| 비교 대상 | 설명 |
|---|---|
| TestFlight | 해당 버전의 TestFlight 최신 빌드 넘버 |
| Local | 프로젝트별 CFBundleVersion.txt에 기록된 빌드 넘버 |
로그 출력 예시는 다음과 같다.
Build number: TestFlight(49) / Local(49) → Next(50)수동 지정 (BUILD_NUMBER_OVERRIDE)
섹션 제목: “수동 지정 (BUILD_NUMBER_OVERRIDE)”자동 계산으로도 해결할 수 없는 예외 상황을 위해 빌드 넘버를 명시적으로 지정할 수 있게 했다.
build_number 파라미터 있음?├── YES → 해당 값을 그대로 사용│ 로그: "Build number: Manual(50)"│└── NO → 자동 계산 (TestFlight / Local 비교)앱 버전이 바뀔 때의 문제
섹션 제목: “앱 버전이 바뀔 때의 문제”max(tf_build, local_build) + 1 방식은 앱 버전이 올라가도(3.14.0 -> 3.14.1) 로컬 기록이 크면 그대로 이어서 증가한다. 신규 버전 첫 업로드에서 실제로 다음 로그가 나왔다.
Build number: TestFlight(1) / Local(116) -> Next(117)신규 버전 첫 업로드는 낮은 번호(1, 또는 정책에 따라 0)로 시작하길 기대했지만 117이 선택됐다. 버그가 아니라 로직이 설계대로 동작한 결과다. 로컬 값은 앱 버전 경계와 상관없이 누적되고 신규 버전인지 판단하지 않아 전역 카운터처럼 동작한다. 그래서 신규 버전 첫 빌드 번호가 크게 시작되고, 버전별로 리셋되리라는 운영 기대와 어긋난다.
버전 기반 분기와 안전 조회
섹션 제목: “버전 기반 분기와 안전 조회”그래서 로직을 다음과 같이 바꿨다.
latest_testflight_build_number호출 시initial_build_number: 0을 명시한다.- 로컬 파일(
CFBundleVersion.txt)에 마지막 빌드의 앱 버전을 함께 저장한다.- 예:
LAST_APP_VERSION=3.14.0 - 예:
CFBundleVersion=116
- 예:
- 계산을 버전 기준으로 분기한다.
- 동일 버전:
max(tf_build, local_build) + 1 - 신규 버전:
tf_build + 1(해당 버전에 빌드가 없으면 1부터 시작)
- 동일 버전:
구현 항목은 다음과 같다.
Fastfile:latest_testflight_build_number에initial_build_number: 0적용, 버전 비교 분기 로직 추가- Jenkins write-back:
CFBundleVersion.txt에LAST_APP_VERSION도 함께 저장 - 운영 가이드: 신규 버전 첫 빌드는
1부터 시작하는 것으로 통일
신규 버전과 동일 버전의 산정 규칙을 나누면 신규 버전의 시작 번호가 정해지므로, 빌드 번호를 버전 단위로 예측할 수 있고 중복 위험도 줄어든다.
프로젝트별 CFBundleVersion.txt 관리
섹션 제목: “프로젝트별 CFBundleVersion.txt 관리”여러 앱을 하나의 fastlane 저장소에서 빌드하므로, 각 프로젝트 폴더에 빌드 넘버를 독립적으로 기록한다.
Fastlane_iOS/├── fastlane/│ └── Fastfile # 공통 Fastfile├── GameA/│ ├── Appfile│ ├── .env│ └── CFBundleVersion.txt ← GameA 전용├── GameB/│ └── CFBundleVersion.txt ← GameB 전용├── GameC/│ └── CFBundleVersion.txt ← GameC 전용└── GameD/ └── CFBundleVersion.txt ← GameD 전용- 빌드에 성공하면 Jenkins 스크립트가 해당 프로젝트의
CFBundleVersion.txt에 빌드 넘버를 자동 기록한다. - 새 프로젝트를 추가할 때는
CFBundleVersion.txt를 만들고 App Store Connect에서 확인한 해당 앱의 현재 최신 빌드 넘버로 초기화한다. - 앱 버전을 새로 올리는 경우(예: 3.13.0 → 3.14.0) 빌드 넘버가 1부터 시작되므로
CFBundleVersion=0으로 설정한다.
Jenkins 설정
섹션 제목: “Jenkins 설정”파라미터
섹션 제목: “파라미터”| Name | 필수 | Default Value | Description |
|---|---|---|---|
FASTLANE_CONFIG |
O | - | 프로젝트 폴더명 (예: GameA) |
XCODE_PROJECT |
O | - | Xcode 프로젝트 경로 |
iPA_NAME |
O | - | IPA 파일명 |
APP_VERSION |
O | - | TestFlight에 업로드할 앱 버전 (예: 3.13.0). latest_testflight_build_number 조회 시 이 버전의 빌드 넘버를 기준으로 한다. |
COMMENTS |
O | - | 변경 로그 내용 |
ENVIRONMENT |
O | - | 빌드 환경 |
BUILD_NUMBER_OVERRIDE |
X | (비워 둠) | 아래 참고 |
BUILD_NUMBER_OVERRIDE는 iOS Build Number를 수동으로 지정한다.
- 비워 두면 TestFlight 최신 빌드 넘버와 로컬 기록(
CFBundleVersion.txt) 중 높은 값을 기준으로 자동 계산한다. - 빌드 넘버 중복 에러가 나면 App Store Connect에서 해당 앱 버전의 build number를 확인하고 원하는 숫자를 입력한다.
Inject environment variables
섹션 제목: “Inject environment variables”| 필드 | 값 |
|---|---|
| Properties File Path | ./${FASTLANE_CONFIG}/CFBundleVersion.txt |
Shell 스크립트
섹션 제목: “Shell 스크립트”echo 'Fastfile을 수행해서 .ipa를 TestFlight에 업로드합니다.'echo "업로드할 .ipa : ${iPA_NAME}!"
# 이전 빌드에서 남은 fastlane 폴더 삭제rm -rf ${XCODE_PROJECT}/fastlane
# 프로젝트별 폴더 전체 복사 (Appfile, .env, CFBundleVersion.txt 등)cp -r ./${FASTLANE_CONFIG} ${XCODE_PROJECT}/fastlane
# 공통 Fastfile 덮어쓰기cp ./fastlane/Fastfile ${XCODE_PROJECT}/fastlane/Fastfile
cd ${XCODE_PROJECT}echo "${COMMENTS} (${ENVIRONMENT})" > fastlane/Changelog.txt
# BUILD_NUMBER_OVERRIDE 파라미터가 있으면 빌드 넘버 수동 지정if [ -n "${BUILD_NUMBER_OVERRIDE}" ]; then fastlane beta name:"${iPA_NAME}" version:"${APP_VERSION}" build_number:"${BUILD_NUMBER_OVERRIDE}"else fastlane beta name:"${iPA_NAME}" version:"${APP_VERSION}"fi
# 빌드 넘버를 프로젝트별 폴더에 기록echo "CFBundleVersion=$(/usr/libexec/PlistBuddy -c 'Print CFBundleVersion' 'Info.plist')" > ${WORKSPACE}/${FASTLANE_CONFIG}/CFBundleVersion.txtFastfile 실행 예시
섹션 제목: “Fastfile 실행 예시”# 자동 계산 (기본)fastlane beta name:"GameA" version:"1.0.0"
# 빌드 넘버 수동 지정fastlane beta name:"GameA" version:"1.0.0" build_number:"50"
# 인증 방식 변경 (ID/Password)fastlane beta name:"GameA" version:"1.0.0" auth:"id_password"주의 사항
섹션 제목: “주의 사항”CFBundleVersion.txt를 수동으로 삭제하지 않는다. 삭제하면 TestFlight 빌드 넘버만으로 판단하게 되어 중복 문제가 다시 생길 수 있다.- 새 프로젝트를 추가할 때
CFBundleVersion.txt초기값은 App Store Connect에서 확인한 해당 앱의 현재 최신 빌드 넘버로 설정한다. 새 버전부터 시작하는 경우 0으로 설정한다. BUILD_NUMBER_OVERRIDE로 수동 지정할 때는 App Store Connect에서 현재 최신 빌드 넘버를 확인한 뒤 입력한다.- 인증 관련 환경 변수(
.env)는 보안상 Git에 포함하지 않는다.