빌드 파이프라인에는 xcodebuild Exit Code 65만 남고, 실제 오류는 그보다 앞선 로그에 있을 수 있습니다.
가장 빠른 해결법은 캐시 삭제가 아니라 전체 명령과 결과 묶음을 보존한 뒤 첫 실패 동작을 찾고, 구성·의존성·실행 대상·서명·노드 환경 순서로 재현하는 것입니다.

이 글을 읽어야 하는 사람

로컬 Xcode에서는 성공하지만 원격 맥 CI에서만 빌드나 테스트가 실패하는 애플리케이션 개발자를 위한 글입니다.
공유 맥 빌드 노드와 Xcode 도구 체인을 관리하는 데브옵스 엔지니어, 컴파일·테스트·서명·보관 단계의 책임을 나누어야 하는 출시 담당자도 대상입니다.

첫 마일스톤은 종료 상태가 아니라 첫 실패입니다

xcodebuild의 종료 상태는 명령이 실패했다는 결과를 전달할 뿐, 프로젝트의 단일 고장 원인을 정의하지 않습니다. Apple의 xcodebuild 명령줄 기술 문서처럼 실제 판단은 실행한 동작과 앞선 오류 메시지를 기준으로 해야 합니다.

먼저 다음 정보를 같은 실행 묶음으로 보관합니다.

  • CI가 실행한 전체 xcodebuild 명령
  • 빌드, 테스트, 보관 중 어느 동작에서 멈췄는지
  • 선택된 Xcode 경로와 DEVELOPER_DIR
  • 실행 계정과 작업 폴더
  • 실패 시각과 원본 표준 출력 및 오류 출력
  • .xcresult 결과 묶음

마지막에 표시된 종료 상태, CI 서비스의 “작업 실패” 문구, 후속 정리 오류는 원인과 다를 수 있습니다. 첫 번째 실패 동작이 의존성 다운로드인지, 컴파일인지, 장치 실행인지, 서명인지 표시한 뒤 다음 단계로 이동해야 합니다.

주의: 결과 묶음과 원본 로그를 먼저 보존하세요. 파생 데이터나 시뮬레이터를 초기화하면 원인을 설명할 단서와 비교 기준이 사라질 수 있습니다.

Apple의 테스트 결과 해석 문서는 결과 묶음에서 테스트 세션, 대상 장치, 로그를 확인하는 흐름을 설명합니다. CI 화면에 마지막 줄만 보인다면 .xcresult를 작업 결과물로 저장하는 방식부터 고쳐야 합니다.

로컬 명령과 CI 구성의 차이를 먼저 대조합니다

로컬 성공과 원격 실패의 가장 흔한 차이는 코드가 아니라 입력 조건입니다. CI가 프로젝트 파일을 직접 읽는지 작업 공간을 읽는지, 선택한 구성과 실행 대상이 같은지부터 비교합니다.

작업 공간과 구성 확인

다음처럼 자리표시자를 사용해 CI와 로컬 명령을 나란히 기록합니다.

xcodebuild \
  -workspace "<작업공간 경로>" \
  -scheme "<공유된 구성표 이름>" \
  -configuration "<구성 이름>" \
  -destination "<실행 대상>" \
  test

프로젝트를 사용하는 저장소인데 CI에서 작업 공간을 지정했거나, 반대로 작업 공간에 포함된 의존성을 빼고 프로젝트만 지정하면 다른 빌드 그래프가 만들어집니다. 구성표가 공유되지 않았다면 CI 계정이 해당 구성표를 보지 못할 수도 있습니다.

사용 가능한 구성표와 실제 설정을 확인한 뒤 최소 명령으로 줄여 보세요.

xcodebuild -list -workspace "<작업공간 경로>"
xcodebuild -showBuildSettings \
  -workspace "<작업공간 경로>" \
  -scheme "<구성표 이름>"

명령줄에서 전달한 빌드 설정은 프로젝트 설정을 덮어쓸 수 있습니다. Apple의 빌드 설정 우선순위 안내를 기준으로 CI 스크립트에 -sdk, -destination, 서명 관련 값, 구성 이름이 몰래 추가되지 않았는지 확인합니다. 설정을 지운 뒤 성공했다면 그것은 해결이 아니라 덮어쓰기 지점을 찾은 결과입니다.

비교 지점 로컬에서 확인할 값 원격 맥 CI에서 확인할 값 판단
입력 파일 프로젝트 또는 작업 공간 같은 저장소의 같은 입력 다르면 명령부터 수정
구성표 공유 여부와 이름 CI 계정에서 검색 가능 여부 보이지 않으면 공유 설정 확인
빌드 구성 디버그 또는 출시 구성 실제 명령줄 전달값 명령줄 덮어쓰기 조사
실행 대상 플랫폼과 장치 조건 설치된 대상과 일치 여부 다르면 테스트 단계 분리
도구 경로 선택된 Xcode DEVELOPER_DIR와 명령줄 도구 경로 고정 후 재실행

의존성과 스크립트는 컴파일 오류와 분리합니다

Swift Package를 사용하는 프로젝트에서는 코드 컴파일 전에 의존성 해석이 실패할 수 있습니다. Package.resolved가 저장소에 포함되어 있는지, 사설 저장소 인증이 CI 계정에도 있는지, known_hosts와 SSH 설정이 해당 계정의 홈 폴더에 있는지 확인합니다.

로컬 셸의 SSH 키를 복사했다고 끝나지 않습니다. CI가 다른 계정으로 실행되면 키, 호스트 확인 파일, 환경 변수, 기본 셸이 모두 달라집니다. Apple의 지속적 통합에서 Swift Package를 다루는 안내는 재현 가능한 의존성 입력과 시스템 Git 도구의 사용 범위를 확인하는 출발점입니다.

Run Script 단계도 별도 실패원으로 기록합니다.

  • 스크립트가 실제로 시작되었는지
  • 작업 폴더가 로컬과 같은지
  • 실행 셸과 환경 변수가 같은지
  • 입력 파일이 생성되었는지
  • 스크립트의 종료 상태가 성공인지
  • 스크립트가 만든 파일을 다음 단계가 읽을 수 있는지

스크립트가 파일을 만들지 못했는데 컴파일러 오류만 보고 있으면 잘못된 층위를 고치는 셈입니다. 의존성 해석만 통과하는 최소 실행과 스크립트가 포함된 전체 실행을 나누어 결과를 저장하세요.

시뮬레이터 실행과 테스트 시작을 따로 판정합니다

빌드 성공, 시뮬레이터 부팅, 테스트 세션 시작은 같은 사건이 아닙니다. 구성표가 지원하는 플랫폼과 설치된 런타임, 지정한 실행 대상이 일치해야 합니다.

결과 묶음에서 다음 순서로 확인합니다.

  • 빌드 작업이 실패했는지
  • 테스트 대상이 설치되었는지
  • 지정한 시뮬레이터가 선택되었는지
  • 장치 부팅 뒤 테스트 세션이 시작되었는지
  • 특정 테스트 또는 테스트 호스트에서 멈췄는지

시뮬레이터 창이 표시되었다는 이유만으로 테스트가 가능한 것은 아닙니다. 실행 대상의 이름이나 식별자를 바꾸지 말고, 원래 실패한 대상과 같은 조건으로 다시 실행해야 합니다. Apple의 자동화 테스트와 SSH 세션 설명은 원격 환경에서 테스트 동작과 세션을 분리해 확인하는 데 참고할 수 있습니다.

이 단계에서 바로 시뮬레이터 전체 초기화를 선택하지 마세요. 특정 런타임만 사용할 수 없는지, 작업 계정이 장치 서비스를 이용할 수 없는지, 테스트 호스트가 실행되지 않은 것인지 먼저 구분해야 합니다.

서명과 보관은 별도 증거로 재검증합니다

첫 실패가 서명, 인증서, 프로비저닝 프로파일, 키체인 접근으로 확인된 경우에만 서명 층위를 조사합니다. 로컬 그래픽 세션에서 자동 서명이 성공했다는 사실은 원격 CI 계정의 비대화형 서명이 성공한다는 뜻이 아닙니다.

확인할 항목은 다음과 같습니다.

  • 팀 식별자와 보관 구성
  • CI 계정이 볼 수 있는 인증서와 개인 키
  • 프로비저닝 프로파일의 대상과 만료 상태
  • 키체인 잠금 상태와 접근 권한
  • 자동 서명과 수동 서명 설정의 혼용 여부
  • 보관 단계와 내보내기 단계의 오류 위치

Apple의 디버깅 정보가 포함된 빌드 안내명령줄 도구 설정 문서를 함께 확인하면 빌드 설정과 도구 선택을 서명 문제와 섞지 않을 수 있습니다.

서명을 끄는 것은 출시 파이프라인의 일반적인 해결책이 아닙니다. 보관과 내보내기를 한 번에 실행하지 말고, 보관 결과를 먼저 저장한 다음 같은 계정과 키체인 조건에서 내보내기를 별도로 검증하세요.

이번 주 복구 판단은 조건표로 진행합니다

아래 조건을 순서대로 적용하면 무작정 파생 데이터를 지우거나 노드를 교체하는 일을 줄일 수 있습니다.

  • 첫 실패가 작업 공간, 구성표, 설정 덮어쓰기라면 명령과 CI 변수를 고친 뒤 같은 저장소와 실행 대상으로 재실행합니다. 최소 명령과 전체 명령이 모두 통과해야 합니다.
  • 첫 실패가 Swift Package나 Run Script라면 잠금 파일, 인증, 계정별 셸 환경, 생성 파일을 고친 뒤 의존성 단계와 전체 빌드를 따로 검증합니다.
  • 빌드는 통과하고 테스트 시작만 실패한다면 시뮬레이터 런타임과 실행 대상을 맞춘 뒤 같은 대상에서 테스트 세션과 결과 묶음을 다시 확인합니다.
  • 첫 실패가 서명이라면 인증서와 개인 키의 계정 접근을 복구하고 보관과 내보내기를 분리해 검증합니다. 서명 비활성화로 우회하지 않습니다.
  • 깨끗한 작업 공간에서도 같은 노드가 반복 실패한다면 Xcode 선택, DEVELOPER_DIR, 디스크 상태, 권한, 키체인, 잔여 작업을 조사합니다.
  • 재시작 뒤 환경이 달라지거나 여러 저장소에서 재현된다면 노드를 격리하거나 재구성합니다. 한 번의 성공만으로 정상화를 선언하지 않습니다.

복구 마일스톤

기록은 다음 네 결과를 한 표에 남기는 방식이 좋습니다.

  1. 원래 명령의 실패 로그와 결과 묶음
  2. 입력을 줄인 최소 명령의 결과
  3. 원인을 수정한 뒤 같은 명령의 반복 결과
  4. 노드 재시작 뒤 동일 조건의 결과

이 기록이 있으면 프로젝트 설정을 고칠지, 작업 공간만 정리할지, 노드를 격리할지 판단할 수 있습니다. 원격 맥 CI를 장기간 운영한다면 노드마다 Xcode 선택, 실행 계정, 키체인 접근, 디스크 상태를 점검하는 인수 기준도 문서화해야 합니다.

필요한 장비를 직접 구매하기 전에 VMSPIN의 맥 대여 요금과 이용 조건을 확인하면, 단기 재현과 지속적인 빌드 노드 운영을 나누어 계산할 수 있습니다. 같은 저장소와 명령을 새 환경에서 먼저 검증하는 것이 플랫폼을 바꾸는 것보다 안전합니다.

자주 묻는 문제를 마지막으로 점검합니다

로컬에서는 성공하는데 원격 맥 CI에서만 실패하는 이유

두 환경의 작업 공간, 구성, 실행 대상, 계정, Xcode 경로가 다르기 때문일 수 있습니다. 로컬의 성공 로그만 비교하지 말고 CI가 실제로 실행한 명령과 DEVELOPER_DIR, 작업 계정, 서명 자산 접근 여부를 확인해야 합니다. 특히 명령줄 설정이 프로젝트 설정을 덮어쓰는지 먼저 살펴보세요.

로그에서 진짜 원인을 찾는 기준

마지막 종료 상태가 아니라 처음 실패한 동작을 기준으로 삼습니다. 전체 표준 출력과 오류 출력, 작업 단계, 결과 묶음을 함께 보존한 뒤 의존성, 컴파일, 테스트 시작, 서명 중 어느 층위에서 멈췄는지 표시합니다. 뒤따르는 취소 메시지나 CI 포장 오류는 원래 실패의 결과일 수 있습니다.

파생 데이터 삭제가 필요한 경우

캐시 오염이 의심되어도 전체 노드가 아니라 해당 작업 공간만 대상으로 제한해야 합니다. 먼저 깨끗한 작업 공간에서 같은 명령을 실행해 비교하고, 원본 결과를 저장하세요. 삭제 뒤 성공했다면 다시 같은 실행 대상과 의존성 조건에서 반복해야 하며, 성공 한 번만으로 캐시가 근본 원인이라고 단정하면 안 됩니다.

시뮬레이터가 떠도 테스트가 실패하는 이유

부팅은 테스트 세션 시작과 다릅니다. 런타임, 플랫폼, 장치 식별자, 테스트 호스트 설치, 계정 권한 중 하나가 맞지 않으면 시뮬레이터 화면이 보여도 테스트가 시작되지 않을 수 있습니다. 결과 묶음에서 빌드와 장치 부팅, 테스트 세션을 각각 확인하고 원래 실행 대상 그대로 재검증해야 합니다.

노드 교체를 결정하는 시점

깨끗한 작업 공간과 최소 명령이 같은 노드에서 반복 성공하면 먼저 프로젝트나 작업 공간을 수정합니다. 반대로 Xcode 선택이나 권한, 디스크, 키체인 상태가 재시작 뒤 달라지거나 여러 저장소에서 반복 실패하면 노드 격리와 재구성을 검토합니다. 새 노드를 쓰더라도 동일한 명령과 결과 보존 절차로 비교해야 합니다.

로컬 맥이나 기존 클라우드 서버는 각각 다른 제약이 있습니다. 로컬 장비는 공유와 재현이 어렵고, 일반적인 리눅스 서버는 Xcode와 macOS 전용 서명 도구를 실행할 수 없으며, 가상 환경은 장치와 키체인 조건이 달라질 수 있습니다. 반대로 장기간 고정 부하가 있고 물리 포트나 독점 장비가 필요하다면 직접 구매가 더 적합할 수 있습니다.

아직 원인이 노드인지 프로젝트인지 확정하지 못했다면, 먼저 완전한 권한과 초기화 가능한 환경을 가진 VMSPIN 원격 맥 이용 화면에서 같은 저장소와 명령을 재현해 보세요. 오류가 노드와 함께 사라질 때만 대여 기간, 환경 격리, 지속적인 CI 노드 운영을 검토하는 편이 안전합니다. 문제를 찾기 전에 플랫폼을 바꾸는 것보다, 증거가 남는 새 원격 맥에서 비교하는 편이 복구 비용을 더 정확하게 통제할 수 있습니다.