콘텐츠로 이동

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 파라미터 있음?
├── 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이 선택됐다. 버그가 아니라 로직이 설계대로 동작한 결과다. 로컬 값은 앱 버전 경계와 상관없이 누적되고 신규 버전인지 판단하지 않아 전역 카운터처럼 동작한다. 그래서 신규 버전 첫 빌드 번호가 크게 시작되고, 버전별로 리셋되리라는 운영 기대와 어긋난다.

그래서 로직을 다음과 같이 바꿨다.

  1. latest_testflight_build_number 호출 시 initial_build_number: 0을 명시한다.
  2. 로컬 파일(CFBundleVersion.txt)에 마지막 빌드의 앱 버전을 함께 저장한다.
    • 예: LAST_APP_VERSION=3.14.0
    • 예: CFBundleVersion=116
  3. 계산을 버전 기준으로 분기한다.
    • 동일 버전: max(tf_build, local_build) + 1
    • 신규 버전: tf_build + 1 (해당 버전에 빌드가 없으면 1부터 시작)

구현 항목은 다음과 같다.

  1. Fastfile: latest_testflight_build_number에 initial_build_number: 0 적용, 버전 비교 분기 로직 추가
  2. Jenkins write-back: CFBundleVersion.txt에 LAST_APP_VERSION도 함께 저장
  3. 운영 가이드: 신규 버전 첫 빌드는 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으로 설정한다.
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를 확인하고 원하는 숫자를 입력한다.
필드 값
Properties File Path ./${FASTLANE_CONFIG}/CFBundleVersion.txt
터미널 창
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.txt
터미널 창
# 자동 계산 (기본)
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에 포함하지 않는다.