엔지니어링 실무

클라우드 Mac CI에서 iOS와 watchOS 동반 번들 검증하기

클라우드 Mac CI에서 iOS와 watchOS 동반 번들 검증하기

iOS 앱과 watchOS 앱을 함께 포함하는 아카이브는 메인 앱이 성공적으로 빌드된 후에도 배포에 실패할 수 있습니다. 시계용 앱이 포함되지 않았거나, 빌드 번호가 한 버전 뒤처졌거나, 확장이 이전 Bundle ID를 참조하거나, 내부 구성 요소 중 하나가 올바르게 서명되지 않았을 수 있기 때문입니다. 이러한 문제를 내보내기 또는 제출 단계에서야 발견해서는 안 됩니다. 더 안정적인 방법은 클라우드 Mac에서 xcarchive를 생성할 때마다 최종 패키지 구조를 직접 검사하고, 그 결과를 파이프라인 게이트로 사용하는 것입니다.

최종 아카이브를 검사해야 하는 이유

프로젝트 파일은 의도한 구성을 설명하지만, 실제 배포되는 결과물은 xcarchive입니다. 프로비저닝 프로파일, 빌드 스크립트, 환경 변수에 따라 버전 번호나 제품 식별자가 재정의될 수 있습니다. 프로젝트 설정만 읽어서는 watchOS 앱이 메인 앱의 Watch 디렉터리에 실제로 포함되었는지, 확장과 상위 앱의 관계가 올바른지 확인할 수 없습니다.

검증 대상은 DerivedData의 중간 디렉터리가 아니라 Release 아카이브로 고정하는 것이 좋습니다. 먼저 운영 파이프라인과 동일한 워크스페이스, Scheme 및 구성으로 결과물을 생성합니다.

set -euo pipefail

ROOT="$PWD"
OUT="$ROOT/out"
ARCHIVE="$OUT/Client.xcarchive"

rm -rf "$ARCHIVE"
mkdir -p "$OUT"

xcodebuild \
  -workspace Client.xcworkspace \
  -scheme Client \
  -configuration Release \
  -destination 'generic/platform=iOS' \
  -archivePath "$ARCHIVE" \
  clean archive

clean archive는 기준 검증에 적합합니다. 일상적인 파이프라인에서는 캐시 정책에 맞춰 조정할 수 있지만, 검증 스크립트는 항상 현재 작업에서 생성된 아카이브를 읽어야 하며 임의의 “최근 생성” 디렉터리로 대체해서는 안 됩니다.

게이트는 “이번에 배포할 패키지에 무엇이 들어 있는가”에 답해야지, “프로젝트가 이론상 무엇을 생성해야 하는가”에 답해서는 안 됩니다.

패키지 구조에서 구성 요소 목록 만들기

일반적인 경로는 Products/Applications/*.app입니다. 그 안의 Watch 디렉터리에는 watchOS 앱이 있고, 시계 앱의 PlugIns 디렉터리에는 다시 확장이 들어갑니다. 제품 이름을 스크립트에 하드코딩하지 마십시오. Target 이름이 바뀌면 고정 경로는 쉽게 무효화됩니다.

IOS_APP=$(find "$ARCHIVE/Products/Applications" \
  -maxdepth 1 -type d -name '*.app' -print -quit)

test -n "${IOS_APP:-}" || {
  printf '%s
' "iOS app not found"
  exit 1
}

WATCH_APP=$(find "$IOS_APP/Watch" \
  -maxdepth 1 -type d -name '*.app' -print -quit)

test -n "${WATCH_APP:-}" || {
  printf '%s
' "watchOS app not found"
  exit 1
}

WATCH_EXTENSION=$(find "$WATCH_APP/PlugIns" \
  -maxdepth 1 -type d -name '*.appex' -print -quit)

printf 'ios=%s
watch=%s
extension=%s
' \
  "$IOS_APP" "$WATCH_APP" "${WATCH_EXTENSION:-embedded}"

일부 최신 프로젝트에는 별도의 .appex 계층이 없으므로, 확장이 없다는 이유만으로 오류로 판단해서는 안 됩니다. 게이트는 저장소에 선언된 제품 구조를 기준으로 판단해야 합니다. 기존 구조는 세 계층의 구성 요소를 요구하지만, 최신 단일 Target 구조는 iOS 앱과 watchOS 앱만 요구합니다. 디렉터리를 보고 프로젝트 유형을 추측하는 대신, 저장소에 간단한 예상 구성 요소 목록을 보관할 수 있습니다.

버전 및 식별자 관계 검증

먼저 각 Info.plistplutil -lint를 실행한 다음 CFBundleIdentifier, CFBundleShortVersionString, CFBundleVersion을 읽습니다. 일반적으로 동일한 릴리스에서는 iOS와 watchOS의 마케팅 버전 및 빌드 번호가 완전히 같아야 합니다. 팀에서 독립적인 빌드 번호를 사용한다면 허용 규칙을 스크립트에 명시해야 합니다.

검사 항목 권장 규칙 실패 위험
마케팅 버전 iOS와 watchOS가 동일 스토어의 버전 관계 불일치
빌드 번호 동일한 릴리스 작업에서 동일 시계 앱이 이전 빌드로 인식됨
동반 앱 식별자 iOS 메인 앱 ID를 참조 설치 후 동반 관계를 설정할 수 없음
확장 소속 현재 watchOS 앱 ID를 참조 확장과 상위 앱이 잘못 연결됨
read_plist() {
  /usr/libexec/PlistBuddy -c "Print :$2" "$1"
}

IOS_PLIST="$IOS_APP/Info.plist"
WATCH_PLIST="$WATCH_APP/Info.plist"

plutil -lint "$IOS_PLIST" "$WATCH_PLIST"

IOS_ID=$(read_plist "$IOS_PLIST" CFBundleIdentifier)
WATCH_ID=$(read_plist "$WATCH_PLIST" CFBundleIdentifier)
IOS_VERSION=$(read_plist "$IOS_PLIST" CFBundleShortVersionString)
WATCH_VERSION=$(read_plist "$WATCH_PLIST" CFBundleShortVersionString)
IOS_BUILD=$(read_plist "$IOS_PLIST" CFBundleVersion)
WATCH_BUILD=$(read_plist "$WATCH_PLIST" CFBundleVersion)
COMPANION_ID=$(read_plist "$WATCH_PLIST" WKCompanionAppBundleIdentifier)

test "$IOS_VERSION" = "$WATCH_VERSION"
test "$IOS_BUILD" = "$WATCH_BUILD"
test "$COMPANION_ID" = "$IOS_ID"

“watchOS Bundle ID는 반드시 iOS Bundle ID로 시작해야 한다”는 규칙으로 정확한 비교를 대신하지 마십시오. 팀에서 명시적 식별자를 사용할 수 있으므로 접두사가 비슷하다고 올바르게 연결되었다고 볼 수 없습니다. 릴리스 구성에서 예상값을 읽고 WKCompanionAppBundleIdentifier의 전체 값을 검증해야 합니다.

확장이 포함된 구조 지원

아카이브에 .appex가 있으면 해당 구성 요소의 WKAppBundleIdentifier도 읽고, 그 값이 현재 watchOS 앱의 Bundle ID와 같은지 확인합니다. 또한 확장 자체의 버전과 빌드 번호를 기록하여 특정 Target에 공통 버전 스크립트가 적용되지 않는 문제를 방지합니다.

서명과 실행 파일 아키텍처 검사

상위 앱의 서명이 성공했다고 해서 모든 중첩 구성 요소가 검증 가능한 것은 아닙니다. 메인 앱에만 재귀 검사를 적용하는 대신 실제 구성 요소를 열거하면 로그에서 실패한 대상을 명확히 확인할 수 있습니다.

verify_component() {
  local item="$1"
  codesign --verify --strict --verbose=2 "$item"
  codesign -d --entitlements :- "$item" >/dev/null
}

verify_component "$IOS_APP"
verify_component "$WATCH_APP"

if test -n "${WATCH_EXTENSION:-}"; then
  verify_component "$WATCH_EXTENSION"
fi

이어서 각 구성 요소의 CFBundleExecutable을 사용해 바이너리를 찾고 file 또는 lipo -archs로 아키텍처를 기록합니다. 도구 체인과 배포 대상은 달라질 수 있으므로 특정 아키텍처 조합을 게이트에 영구적으로 하드코딩할 필요는 없습니다. 실행 파일이 비어 있거나 예상치 못한 시뮬레이터 결과물인 경우를 거부하고, 현재 아키텍처 목록을 검토용으로 보관하는 편이 더 안정적입니다.

중첩 구성 요소에 패키지 경계를 벗어나는 심볼릭 링크, 중복 Bundle ID 또는 선언되지 않은 추가 확장이 있는지도 검사해야 합니다. 이러한 문제는 복사 스크립트에서 지나치게 넓은 와일드카드를 사용할 때 자주 발생합니다.

검증 결과를 안정적인 게이트로 전환

스크립트의 종료 코드는 차단을 담당하고, 보고서는 원인을 설명합니다. 각 작업에서는 구성 요소의 상대 경로, Bundle ID, 버전, 빌드 번호, 아키텍처, 서명 검증 상태 및 아카이브 체크섬을 저장해야 합니다. 보고서에 인증서 개인 키, 전체 환경 변수 또는 전체 사용자 디렉터리 경로를 수집해서는 안 됩니다.

다음과 같이 프로세스를 네 단계로 나누는 것이 좋습니다.

  1. 고유한 아카이브 디렉터리를 생성하고 이전 결과물의 재사용을 금지합니다.
  2. 구성 요소를 검색하고 저장소의 예상 구조와 비교합니다.
  3. 메타데이터, 연결 필드, 서명 및 실행 파일 아키텍처를 검증합니다.
  4. 텍스트 또는 JSON 보고서를 출력한 다음 내보내기 작업을 실행합니다.

게이트를 처음 도입할 때는 몇 차례 동안 기록만 하고 차단하지 않는 방식으로 실행하여 기존 프로젝트와 최신 프로젝트의 구조가 모두 처리되는지 확인할 수 있습니다. 규칙이 안정화되면 누락된 구성 요소, 버전 불일치, 서명 실패를 하드 오류로 전환합니다. 동일한 클라우드 Mac에서 여러 아카이브 작업을 병렬로 실행한다면 각 작업에 독립적인 archivePath와 보고서 디렉터리를 할당하여 한 작업이 다른 작업의 결과를 읽지 않도록 해야 합니다.

최종 목표는 형식적인 검사를 한 단계 더 추가하는 것이 아닙니다. 파이프라인이 메인 앱, 시계 앱 및 확장이 실제로 동일한 릴리스 컨텍스트에서 생성되었고 서로의 관계가 명확하며, 최종 아카이브를 내보내기 단계로 진행할 수 있음을 입증하도록 만드는 것입니다.

자주 묻는 질문

프로젝트 설정 검사만으로 충분한가요?

충분하지 않습니다. 최종 아카이브에는 설정 덮어쓰기, 빌드 스크립트, 실제 포함 과정과 코드 서명 결과가 함께 반영됩니다.

iOS 앱과 watchOS 앱 버전은 같아야 하나요?

같은 릴리스라면 마케팅 버전과 빌드 번호를 맞추는 것이 원칙입니다. 별도 정책이 있다면 허용 관계를 CI 규칙으로 명시해야 합니다.

전용 물리 노드

클라우드에서 Mac mini 워크플로를 계속 실행하세요

RunAMac M4와 대여 기간, 배포 노드를 선택하고 개발, 빌드 또는 실험 작업을 전용 물리 머신에서 실행하세요.

대여 플랜 선택