클라우드 Mac에서 두 개의 iOS 아카이브 파이프라인이 동시에 실행됩니다. 하나는 운영 환경의 문제를 수정하고, 다른 하나는 정기 릴리스를 준비합니다. 두 파이프라인 모두 프로젝트 파일에서 동일한 CURRENT_PROJECT_VERSION을 읽기 때문에, 결국 빌드 번호는 같지만 코드는 서로 다른 두 개의 산출물이 만들어집니다. 이 문제는 대개 배포 시스템에 제출할 때야 드러나며, 그 시점에는 파일 이름과 커밋 기록만으로 어느 산출물을 유지해야 할지 판단하기 어렵습니다.
빌드 번호 관리의 목적은 단순히 값을 “자동으로 1씩 증가”시키는 것이 아닙니다. 배포 가능한 모든 산출물이 번호를 누가 할당했는지, 어떤 커밋에 해당하는지, 아카이브에 실제로 기록된 값이 무엇인지 답할 수 있어야 합니다.
버전 번호와 빌드 번호를 먼저 구분하기
MARKETING_VERSION은 사용자가 보는 버전으로, 예를 들면 3.8.0입니다. CURRENT_PROJECT_VERSION은 빌드 번호이며 숫자로만 구성된 증가 값이어야 합니다. 두 값의 수명 주기는 서로 다르므로 커밋 태그를 두 필드에 그대로 동시에 기록해서는 안 됩니다.
| 필드 | 예시 | 변경 시점 | 주요 용도 |
|---|---|---|---|
MARKETING_VERSION |
3.8.0 |
제품 버전 변경 시 | 기능 버전 식별 |
CURRENT_PROJECT_VERSION |
18427 |
배포 가능한 아카이브를 만들 때마다 | 동일 버전의 산출물 구분 |
| Git 커밋 | a1b2c3d |
커밋할 때마다 | 소스 코드 식별 |
| 파이프라인 번호 | 5821 |
작업을 실행할 때마다 | 실행 기록 식별 |
프로젝트 저장소에는 안정적인 버전 번호를 보관할 수 있지만, 여러 실행기가 빌드 번호를 동시에 수정하고 커밋하게 해서는 안 됩니다. 병렬 작업이 각각 “읽기, 1 증가, 다시 쓰기”를 수행하면 모든 작업이 성공하더라도 같은 결과를 받을 수 있습니다.
빌드 번호는 소스 코드 변경 결과가 아니라 파이프라인 입력값으로 취급해야 합니다. 소스 코드는 그 값을 사용하는 방법을 선언하고, 스케줄링 시스템은 값의 고유성을 보장합니다.
고유한 번호 발급원 구축하기
단조 증가하는 시퀀스 선택하기
정식 아카이브는 파이프라인 시스템의 전역 실행 번호나 내부 조정 작업이 원자적으로 할당하는 시퀀스처럼 중앙화된 단일 소스에서 정수를 받아야 합니다. 번호는 다음 세 가지 조건을 충족해야 합니다.
- 동일한 배포 대상에서 중복되지 않아야 합니다.
- 새 번호는 이미 제출된 번호보다 커야 합니다.
- 해당 커밋, 브랜치, 작업 실행 기록을 역추적할 수 있어야 합니다.
git rev-list --count HEAD는 이력을 다시 작성하지 않는 단일 브랜치 프로젝트에는 적합하지만, 얕은 클론이나 리베이스 또는 여러 릴리스 브랜치를 사용하는 환경에는 적합하지 않습니다. 브랜치마다 동일한 카운트가 나올 수 있고, 이력을 다시 작성하면 숫자가 감소할 수도 있습니다. 내부 디버그 빌드에는 사용할 수 있지만 복잡한 릴리스 프로세스의 유일한 정보 원본으로 삼아서는 안 됩니다.
OpsVM의 병렬 작업에서는 스케줄링 계층이 먼저 BUILD_SEQUENCE를 생성한 후 실제 빌드 노드에 전달하도록 구성할 수 있습니다. 노드는 번호를 소비하기만 하며, 번호를 두고 경쟁하거나 다시 기록하지 않습니다.
작업 진입점에서 잘못된 데이터 거부하기
#!/bin/zsh
set -euo pipefail
: "${BUILD_SEQUENCE:?BUILD_SEQUENCE is required}"
: "${RELEASE_VERSION:?RELEASE_VERSION is required}"
if [[ ! "$BUILD_SEQUENCE" =~ ^[0-9]+$ ]]; then
print -u2 "BUILD_SEQUENCE must contain digits only"
exit 64
fi
if [[ ! "$RELEASE_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
print -u2 "RELEASE_VERSION must use major.minor.patch"
exit 64
fi
진입점 검증은 종속성 설치와 컴파일보다 먼저 수행해야 합니다. 그래야 변수 누락으로 인해 십여 분 뒤에 배포할 수 없는 아카이브가 만들어지는 일을 방지할 수 있습니다.
아카이브 명령에 번호 주입하기
파이프라인에서 project.pbxproj를 수정할 필요는 없습니다. xcodebuild 명령 끝에서 빌드 설정을 직접 재정의하면 저장소에 변경된 파일을 만들지 않으며, 로그를 바탕으로 동일한 작업을 재현하기도 쉽습니다.
archive_path="$PWD/output/App.xcarchive"
result_path="$PWD/output/Archive.xcresult"
xcodebuild \
-workspace App.xcworkspace \
-scheme App \
-configuration Release \
-destination "generic/platform=iOS" \
-archivePath "$archive_path" \
-resultBundlePath "$result_path" \
MARKETING_VERSION="$RELEASE_VERSION" \
CURRENT_PROJECT_VERSION="$BUILD_SEQUENCE" \
clean archive
재정의할 항목은 동일한 아카이브 명령에 포함해야 합니다. 테스트에서는 한 번호를 사용하고 아카이브에서는 프로젝트 기본값을 다시 읽는 상황을 방지하기 위해서입니다. 프로젝트에 확장 컴포넌트가 포함되어 있다면 기본 앱과 확장이 동일한 설정을 상속하는지도 확인해야 합니다. 릴리스 규정에 명시적인 요구 사항이 없는 한 target마다 별도의 번호를 생성하지 마십시오.
아카이브 전에 해석된 설정을 먼저 확인할 수 있습니다.
xcodebuild \
-workspace App.xcworkspace \
-scheme App \
-configuration Release \
-showBuildSettings |
awk '/MARKETING_VERSION|CURRENT_PROJECT_VERSION/ { print }'
이 단계에서는 주로 스크립트, 구성 파일 또는 target 수준 설정이 명령줄 매개변수를 예기치 않게 덮어쓰는 문제를 찾아냅니다.
로그를 믿지 말고 아카이브를 직접 검증하기
명령이 성공했다는 것은 아카이브가 완료되었다는 의미일 뿐, 원하는 값이 최종 앱에 실제로 반영되었다는 뜻은 아닙니다. 검증 스크립트는 .xcarchive 안에 있는 기본 앱의 Info.plist를 읽고 파이프라인 입력값과 비교해야 합니다.
app_path="$(find "$archive_path/Products/Applications" \
-maxdepth 1 -name '*.app' -type d -print -quit)"
if [[ -z "$app_path" ]]; then
print -u2 "Application bundle not found"
exit 65
fi
plist="$app_path/Info.plist"
actual_version=$(/usr/libexec/PlistBuddy \
-c "Print :CFBundleShortVersionString" "$plist")
actual_build=$(/usr/libexec/PlistBuddy \
-c "Print :CFBundleVersion" "$plist")
[[ "$actual_version" == "$RELEASE_VERSION" ]] || exit 66
[[ "$actual_build" == "$BUILD_SEQUENCE" ]] || exit 67
검증이 통과하면 버전 번호, 빌드 번호, 전체 커밋 해시, 아카이브 체크섬, 파이프라인 실행 식별자를 하나의 일반 텍스트 매니페스트에 기록하고 산출물과 함께 보관합니다. 파일 이름은 사람이 읽기 쉽게 만들 수 있지만, 매니페스트와 아카이브 내부 필드를 대신할 수는 없습니다.
재시도, 브랜치, 동시성 처리하기
파이프라인 계층에서 재시도 규칙 고정하기
컴파일 전에 실패했고 아카이브가 생성되지 않았다면 동일한 작업을 재시도할 때 원래 번호를 재사용할 수 있습니다. 아카이브가 이미 생성 또는 업로드되었거나 후속 처리 단계에 들어갔다면 새 번호를 발급받아야 합니다. 숫자를 연속으로 유지하기 위해 이전 번호를 회수하지 말고 기록에 남겨 두십시오.
여러 릴리스 브랜치가 동일한 배포 대상을 공유한다면 번호 공간도 공유해야 합니다. 브랜치마다 1부터 시작하면 정돈되어 보이지만, 브랜치를 병합한 뒤에는 충돌이 발생합니다. 브랜치 이름, 커밋 해시, 버전 번호는 출처를 나타내고, 빌드 번호는 고유성과 증가 순서만 보장해야 합니다.
최소 점검 목록
- 중앙화된 단일 소스가 번호를 원자적으로 할당합니다.
- 빌드 노드는 번호를 읽기만 하고 프로젝트 파일을 수정하지 않습니다.
- 아카이브 명령에서 두 버전 필드를 명시적으로 전달합니다.
- 기본 앱과 확장 사이의 설정 상속 관계를 확인합니다.
- 성공 후 아카이브 내부 필드를 읽어 입력값과 비교합니다.
- 모든 산출물에 커밋, 작업, 체크섬의 매핑을 저장합니다.
- 산출물이 이미 생성된 실패 작업의 번호는 회수하지 않습니다.
- 병렬 노드는 콘솔에서 현재 선택 가능한 구성을 확인한 후 빌드만 담당하고 번호는 할당하지 않습니다.
이러한 제약을 적용하면 빌드 번호는 더 이상 릴리스 직전에 임시로 수정하는 숫자가 아니라 소스 코드, 실행 기록, 최종 산출물을 연결하는 안정적인 인덱스가 됩니다. 롤백이나 병렬 작업 충돌이 발생해도 팀은 먼저 번호로 아카이브를 찾은 다음, 유일하게 대응하는 커밋과 파이프라인 실행 환경으로 돌아갈 수 있습니다.
자주 묻는 질문
Git 커밋 개수를 iOS 빌드 번호로 사용해도 되나요?
기록을 다시 쓰지 않는 단일 브랜치에서는 가능합니다. 리베이스, 얕은 클론, 여러 릴리스 브랜치가 있다면 CI가 중앙에서 발급하는 증가 정수를 사용해야 합니다.
실패한 파이프라인을 재시도할 때 이전 빌드 번호를 재사용해야 하나요?
이전 실행이 아카이브나 배포 산출물을 만들지 않았을 때만 재사용합니다. 산출물이 생성됐다면 새 번호를 발급하고 두 실행을 같은 커밋에 연결합니다.
OpsVM에서 클라우드 Mac 워크플로 배포
세 가지 Apple Silicon 구성과 판매 중인 6개 노드 중에서 선택할 수 있으며, 실제 사용 가능 여부는 콘솔에서 실시간으로 확인되는 상태를 따릅니다.