파이프라인에는 성공으로 표시되지만, 다운로드한 IPA가 해당 빌드를 트리거한 커밋과 일치하지 않을 수 있습니다. 더 위험한 경우도 있습니다. 업로드가 중단된 뒤 후속 작업이 절반만 기록된 파일을 가져가는 상황입니다. 클라우드 Mac에서는 빌드, 내보내기, 업로드를 서로 다른 스크립트가 담당하는 경우가 많습니다. 따라서 “명령 종료 코드가 0”이라는 조건만으로 산출물 전달 여부를 판단하면, 이 두 문제는 해당 작업이 실행되는 동안 발견하기 어렵습니다.
해결책은 압축 명령을 하나 더 추가하는 것이 아니라, 산출물을 분리할 수 없는 하나의 묶음으로 정의하는 것입니다. IPA, SHA-256 매니페스트, 빌드 컨텍스트, 검증 결과를 함께 생성하고 모든 검사가 통과한 뒤에만 공개해야 합니다.
전달 가능한 산출물부터 정의하기
각 작업에는 고유한 BUILD_ID를 사용합니다. 전달 디렉터리에는 최소한 다음 파일이 포함되어야 합니다.
| 파일 | 용도 | 검수 방법 |
|---|---|---|
App.ipa |
설치 및 배포용 산출물 | 해시 일치, 내보내기 명령 성공 |
SHA256SUMS |
콘텐츠 무결성 매니페스트 | shasum -a 256 -c 성공 반환 |
build-context.tsv |
빌드 출처 추적 | 커밋, Xcode, 작업 식별자가 비어 있지 않음 |
export.log |
내보내기 실패 진단 | 전체 출력을 보관하고 접근 권한 제한 |
빌드 컨텍스트에는 스크립트에서 브랜치를 임의로 추측한 값이 아니라, 파이프라인에서 이미 확정된 변수를 사용해야 합니다. 최소한 전체 커밋 해시, 작업 식별자, Scheme, Xcode 버전, 생성 시간을 기록합니다. 시간은 로그를 연계하는 용도로만 사용하며, 산출물의 식별 여부는 여전히 커밋 해시와 체크섬을 기준으로 판단해야 합니다.
해시는 두 검사 지점 사이에서 파일이 변경되지 않았음을 증명할 수 있지만, 해당 파일이 처음부터 신뢰할 수 있는 프로세스에서 생성되었음을 증명하지는 못합니다. 코드 서명 검증과 빌드 컨텍스트가 모두 필요합니다.
내보내기 전에 아카이브 검증하기
먼저 아카이브가 존재하는지 확인한 다음, 그 안의 .app을 찾아 코드 서명을 검증합니다. --deep을 사용해 다시 서명해서는 안 됩니다. 여기서의 목적은 기존 서명 구조를 검사하는 것입니다. 앱에 확장 프로그램이 포함되어 있다면 검증 명령이 중첩된 코드까지 검사하도록 하고, 실패는 작업을 중단해야 하는 조건으로 처리해야 합니다.
아카이브, 내보내기 디렉터리, 최종 디렉터리는 동일한 작업 볼륨에 두어야 합니다. 그래야 마지막 디렉터리 이름 변경이 원자성을 갖습니다. MiniRent 클라우드 Mac에서 실행할 때도 동시 실행되는 각 작업에 독립된 루트 디렉터리를 할당하여, 두 작업이 out/latest 같은 고정 경로를 공유하지 않도록 해야 합니다.
바로 적용할 수 있는 스크립트
set -euo pipefail
: "${BUILD_ID:?BUILD_ID is required}"
: "${GIT_COMMIT:?GIT_COMMIT is required}"
: "${SCHEME:?SCHEME is required}"
ROOT="${ARTIFACT_ROOT:-$PWD/out}"
ARCHIVE="$ROOT/input/App.xcarchive"
EXPORT_OPTIONS="$ROOT/input/ExportOptions.plist"
STAGE="$ROOT/.stage-$BUILD_ID"
FINAL="$ROOT/releases/$BUILD_ID"
rm -rf "$STAGE"
mkdir -p "$STAGE" "$(dirname "$FINAL")"
trap 'rm -rf "$STAGE"' EXIT
test -d "$ARCHIVE"
test -f "$EXPORT_OPTIONS"
test ! -e "$FINAL"
APP_PATH="$(find "$ARCHIVE/Products/Applications" -maxdepth 1 -name '*.app' -print -quit)"
test -n "$APP_PATH"
codesign --verify --deep --strict --verbose=2 "$APP_PATH"
xcodebuild -exportArchive \
-archivePath "$ARCHIVE" \
-exportPath "$STAGE/export" \
-exportOptionsPlist "$EXPORT_OPTIONS" \
>"$STAGE/export.log" 2>&1
IPA_PATH="$(find "$STAGE/export" -maxdepth 1 -name '*.ipa' -print -quit)"
test -n "$IPA_PATH"
cp "$IPA_PATH" "$STAGE/App.ipa"
DIGEST="$(shasum -a 256 "$STAGE/App.ipa" | awk '{print $1}')"
printf '%s %s
' "$DIGEST" "App.ipa" >"$STAGE/SHA256SUMS"
XCODE_VERSION="$(xcodebuild -version | paste -sd ' ' -)"
printf 'build_id %s
commit %s
scheme %s
xcode %s
' \
"$BUILD_ID" "$GIT_COMMIT" "$SCHEME" "$XCODE_VERSION" \
>"$STAGE/build-context.tsv"
(
cd "$STAGE"
shasum -a 256 -c SHA256SUMS
)
mv "$STAGE" "$FINAL"
(
cd "$FINAL"
shasum -a 256 -c SHA256SUMS
)
스크립트의 ExportOptions.plist는 저장소에서 관리하는 설정으로 제공해야 합니다. 로그나 컨텍스트 파일에 암호, 개인 키, 임시 토큰 또는 전체 환경 변수 목록을 기록해서는 안 됩니다.
원자적 게시로 미완성 산출물 격리하기
핵심은 숨겨진 임시 디렉터리에서 모든 파일 기록과 검증을 완료한 뒤, 한 번의 mv로 최종 디렉터리를 공개하는 것입니다. 읽는 쪽에서는 releases/만 검색하고 .stage-*에는 접근하지 않으므로 생성 중인 파일을 가져가지 않습니다.
이 보장은 동일한 파일 시스템 안에서만 유효합니다. 게시 디렉터리가 다른 볼륨에 마운트되어 있으면 mv가 복사 후 삭제 작업으로 바뀔 수 있습니다. 올바른 방법은 대상 볼륨에 임시 디렉터리를 만들고 모든 파일을 복사한 뒤, 대상에서 해시 검사를 다시 실행하고 마지막으로 대상 볼륨 내부에서 디렉터리 이름을 변경하는 것입니다. 객체 스토리지나 아티팩트 서비스처럼 디렉터리 이름 변경 의미 체계가 없는 환경에서는 작업 식별자가 포함된 임시 키로 먼저 업로드한 다음, 검증을 마친 뒤 작은 완료 마커를 기록할 수 있습니다. 소비자는 산출물을 읽기 전에 반드시 이 마커를 확인해야 합니다.
latest를 덮어쓰지 않기
latest를 사용하면 재시도 작업이 더 최근에 성공한 결과를 덮어쓸 수 있습니다. 안정적인 방법은 변경 불가능한 BUILD_ID를 기준으로 산출물을 저장하고, 대상 작업 식별자만 담은 포인터 파일을 별도로 관리하는 것입니다. 포인터를 갱신하기 전에 대상 디렉터리가 검증을 통과했는지 확인해야 합니다. 롤백할 때도 과거 산출물을 수정하지 않고 포인터만 변경합니다.
실패를 올바른 단계에서 차단하기
서명 오류는 내보내기 전에, 내보내기 오류는 임시 디렉터리 단계에서, 해시 오류는 게시 전에 작업을 중단시켜야 합니다. 업로드 후 다시 수행하는 검증은 전송, 디스크 또는 스크립트가 잘못된 파일을 선택하여 발생한 변경을 발견하는 데 사용합니다.
흔한 실수로는 확장자를 제한하지 않은 채 내보내기 디렉터리에서 “첫 번째 파일”을 가져오는 경우, 여러 Scheme이 동일한 출력 경로를 공유하는 경우, 재시도 시 이전 실행에서 남은 임시 디렉터리를 그대로 사용하는 경우가 있습니다. 스크립트는 시작할 때 현재 작업의 스테이징 디렉터리를 정리해야 하지만, 광범위한 와일드카드로 다른 작업의 디렉터리까지 삭제해서는 안 됩니다.
로그에도 경계를 설정해야 합니다. xcodebuild의 전체 종료 상태와 출력을 보존하되 민감한 환경 정보는 출력하지 않아야 합니다. 지원 요청을 제출하기 전에는 관련 시간대의 로그만 추출하고, 저장소 토큰, 서명 자료 경로에 포함된 내부 식별자, 연결 자격 증명을 제거해야 합니다.
전달 전 체크리스트
파이프라인을 성공으로 표시하기 전에 다음 항목을 하나씩 확인합니다.
- 아카이브의 App이 엄격한 코드 서명 검증을 통과했습니다.
- IPA는 이전 디렉터리를 재사용한 것이 아니라 현재 작업의 아카이브에서 내보냈습니다.
GIT_COMMIT은 전체 커밋 해시이며 트리거 기록과 일치합니다.SHA256SUMS는 스테이징 디렉터리와 최종 디렉터리에서 각각 한 번씩 검증했습니다.- 최종 디렉터리는 고유한 작업 식별자로 이름을 지정했으며 재시도로 덮어쓰지 않습니다.
- 소비자는 게시된 디렉터리 또는 완료 마커가 있는 객체만 읽습니다.
- 로그로 실패 단계를 식별할 수 있지만 자격 증명이나 서명 자료의 내용은 포함하지 않습니다.
이 프로세스가 모든 빌드 실패를 없애 주는 것은 아니지만, “빌드가 성공했는가”와 “전달물이 신뢰할 수 있는가”를 서로 독립적으로 검증할 수 있는 두 가지 문제로 분리해 줍니다. 아카이브, 검증, 컨텍스트, 게시 경계를 고정하면 재시도와 롤백도 명확한 객체만 대상으로 처리하게 되며, 더 이상 계속 변하는 공유 디렉터리에 의존하지 않습니다.
자주 묻는 질문
SHA-256 값만으로 iOS 산출물이 신뢰할 수 있는 빌드 머신에서 생성됐음을 증명할 수 있나요?
아닙니다. SHA-256은 두 검증 시점 사이에서 파일이 바뀌지 않았다는 점만 확인합니다. 코드 서명을 검사하고 커밋, Xcode 버전, 작업 식별자와 매니페스트를 함께 보관해야 합니다.
임시 디렉터리를 거쳐 게시 디렉터리로 이름을 바꾸는 이유는 무엇인가요?
같은 파일 시스템의 이름 변경은 완성된 디렉터리를 한 번에 노출합니다. 다른 볼륨으로 옮길 때는 먼저 복사하고 다시 검증한 뒤 대상 볼륨 안에서 이름을 변경해야 합니다.
전용 클라우드 Mac
전용 물리 노드에서 빌드 작업 실행
일·주·월·분기 단위로 MiniRents M4 및 MiniRents M4 Pro를 선택하고, 팀과 코드 저장소 위치에 맞는 노드를 선택하세요. 장비는 전용 물리 머신이며 가상 머신이 아닙니다.