Bitbucket Pipelines의 호스팅 러너에서는 iOS 앱을 빌드할 수 없습니다. 모든 단계가 Linux 컨테이너에서 실행되지만 Xcode는 macOS에서만 동작하기 때문입니다. 두 가지 해결책이 있습니다. 자체 Mac을 Bitbucket 셀프호스트드 러너로 등록하거나, Linux 단계에서 iOS 컴파일, 서명, TestFlight 업로드를 클라우드 빌드 서비스에 위임하는 것입니다. 이 가이드에서는 두 가지 방법을 모두 다루며, 완전한 bitbucket-pipelines.yml 파일을 Capacitor 앱용으로 제공합니다.
팀이 이미 Bitbucket에서 code 리뷰, Jira 링크, 배포를 관리하고 있다면 iOS를 위해 별도의 CI 도구가 필요 없습니다. 파이프라인은 Bitbucket에 그대로 두고, Mac이 필요한 부분만 이동시키면 됩니다.
Bitbucket Pipelines에서 iOS 빌드가 실패하는 이유
Bitbucket Pipelines는 사용자가 선택한 Docker 이미지(node:22, cimg/android등)에서 각 단계를 Atlassian의 Linux 인프라에서 실행합니다. 이는 bun install유닛 테스트, 린팅, 심지어 Android 전체 빌드에는 문제가 없습니다. 하지만 iOS에서는 막힙니다:
xcodebuild,codesigniOS SDK는 macOS에만 존재하기 때문입니다.- Apple 라이선스는 Apple 하드웨어에서만 macOS를 실행할 수 있도록 허용하므로, 서명된
.ipa. - 2026년 4월 28일부터 App Store Connect는 Xcode 26과 iOS 26 SDK로 빌드되지 않은 업로드를 거부합니다. 따라서 사용하는 Mac이 최신 상태를 유지해야 합니다. 참고: Capacitor 앱을 위한 Apple의 Xcode 26 요구사항.
Bitbucket은 Linux, Windows, macOS에서 self-hosted runner를 지원합니다. Atlassian은 자체 runner에서 실행되는 단계에 대해 빌드 시간을 청구하지 않지만, 하드웨어 비용과 유지 관리 비용이 발생합니다.
세 가지 옵션
| 옵션 | iOS 빌드 실행 위치 | 유지 관리 항목 | 적합한 경우 |
|---|---|---|---|
| 자체 호스팅 macOS runner | 보유 중인 Mac mini 또는 대여한 Mac | macOS, Xcode, keychain, 인증서, 디스크 공간, 가동 시간 | 이미 Mac을 보유하고 관리 인력이 있는 팀 |
| 모바일 전용 보조 CI | 타사 제공 Mac | 파이프라인 2개, 시크릿과 트리거 세트 2개 | 어쨌든 전용 모바일 CI를 원하는 팀 |
| Linux 단계에서 호출되는 클라우드 빌드 | Capgo Build의 Mac 플릿 | CLI 호출 1회와 몇 가지 보안 변수 | Bitbucket이 유일한 CI로 남기를 원하는 Capacitor 팀 |
이 가이드의 나머지 부분은 첫 번째와 세 번째 옵션을 자세히 다룹니다. 플랫폼에 대한 더 넓은 관점을 보려면 iOS 앱용 CI/CD 플랫폼 비교.
옵션 1: Capgo Build를 사용하여 Linux 단계에서 iOS 빌드
Capgo 빌드 Capacitor 앱의 컴파일과 서명을 Capgo 관리 Mac에서 수행합니다. Bitbucket 단계는 Linux에서 잘하는 일을 그대로 수행합니다: 의존성 설치, 웹 레이어 빌드, 그리고 cap sync ios. 그런 다음 Capgo CLI가 준비된 네이티브 프로젝트를 업로드합니다(웹 소스 code 또는 node_modules), Xcode 빌드를 실행하고 인증서로 서명하며, 결과를 TestFlight에 업로드할 수 있습니다. 로그는 Bitbucket 단계로 스트리밍되고, 빌드가 실패하면 명령이 0이 아닌 코드로 종료되어 iOS 빌드 실패 시 파이프라인도 실패합니다.
러너가 프로젝트를 Capgo에 푸시하므로, Capgo은 Bitbucket 저장소에 접근할 필요가 없습니다. 저장소 연결을 승인하거나 OAuth 앱을 승인할 필요도 없습니다.
사전 요구사항
- 로컬에서 빌드되는 Capacitor 앱 (Capacitor 8은 Node.js 22 이상 필요)
- 팀 관리자 권한이 있는 Apple Developer Program 멤버십
- Capgo 계정과 업로드 권한이 있는 API 키
- Capgo에 등록된 앱:
bunx @capgo/cli@latest app add
머신에 대한 일회성 설정
온보딩을 한 번 실행하세요. iOS에서는 배포 인증서와 프로비저닝 프로필을 생성하거나 재사용하고, App Store Connect API 키를 요청합니다:
bunx @capgo/cli@latest login
bunx @capgo/cli@latest build init --platform ios
Mac이 없다면, 브라우저에서 다음을 사용하여 인증서 서명 요청을 생성할 수 있습니다. iOS 인증서 생성기, 또는 다음을 따르세요 Capgo Build로 Windows에서 iOS 앱 빌드하기.
CI를 건드리기 전에 노트북에서 빌드를 한 번 실행해 보세요. 첫 빌드를 디버깅하기엔 CI가 적합하지 않습니다:
bun run build
bunx cap sync ios
bunx @capgo/cli@latest build request com.example.app --platform ios --build-mode release
Bitbucket 자격 증명 내보내기
CLI는 자격 증명을 로컬 머신에 보관합니다. 이를 Bitbucket으로 옮기려면 dotenv 파일로 내보내세요:
bunx @capgo/cli@latest build credentials manage --appId com.example.app --platform ios
# choose "Export to .env" in the menu
다음 파일을 작성합니다 .env.capgo.com.example.app.ios 모드: 0600. 다음 키들이 포함되어 있습니다:
| 변수 | 내용 |
|---|---|
BUILD_CERTIFICATE_BASE64 |
Base64로 인코딩된 값 .p12 배포 인증서 |
P12_PASSWORD |
인증서 비밀번호 (없으면 비워둠) |
CAPGO_IOS_PROVISIONING_MAP_BASE64 |
CLI가 생성한 Provisioning profile 매핑 |
APPLE_KEY_ID |
App Store Connect API 키 ID |
APPLE_ISSUER_ID |
App Store Connect 발급자 ID |
APPLE_KEY_CONTENT |
Base64 인코딩된 .p8 API 키 |
APP_STORE_CONNECT_TEAM_ID |
Apple 팀 ID |
각 줄을 보안 변수로 Bitbucket의 리포지토리 설정 > 파이프라인 > 리포지토리 변수, 그리고 CAPGO_TOKEN 에 Capgo API 키를 추가하세요. 보안 변수는 로그에서 마스킹되며 UI에서 다시 읽을 수 없습니다.
API를 선호한다면, 이 루프가 한 번에 생성합니다. pipeline variables 쓰기 권한이 있는 Bitbucket API 토큰을 사용합니다:
WORKSPACE=my-team
REPO=my-app
ENV_FILE=.env.capgo.com.example.app.ios
grep -v '^#' "$ENV_FILE" | grep '=' | while IFS= read -r line; do
key="${line%%=*}"
value="${line#*=}"
value="${value%\"}"; value="${value#\"}"
jq -n --arg k "$key" --arg v "$value" '{key:$k, value:$v, secured:true}' |
curl -s -u "$BB_EMAIL:$BB_API_TOKEN" \
-H 'Content-Type: application/json' \
-X POST --data @- \
"https://api.bitbucket.org/2.0/repositories/$WORKSPACE/$REPO/pipelines_config/variables" > /dev/null
echo "created $key"
done
rm "$ENV_FILE"
이후 파일을 삭제하고 .env.capgo.* 대상 .gitignore을 추가하세요. 평문 서명 자료가 포함되어 있습니다.
프로덕션 자격 증명은 리포지토리 변수 대신 배포 변수 를 고려해 보세요. 배포 환경(예: Production)에 스코프가 적용되며, Premium 플랜에서는 해당 환경을 관리자나 특정 브랜치로 제한할 수 있습니다.
파이프라인 파일
Bitbucket은 리포지토리와 deployment variables를 환경 변수로 주입하므로, Capgo CLI가 매핑 없이 이를 가져옵니다. 이 파일은 모든 push 시 테스트를 실행하고, 요청 시 ad hoc iOS 빌드를 생성하며, 버전 태그 시 TestFlight에 배포합니다:
# bitbucket-pipelines.yml
image: node:22
definitions:
caches:
bun: ~/.bun/install/cache
pipelines:
default:
- step:
name: Lint and test
caches:
- bun
script:
- curl -fsSL https://bun.sh/install | bash
- export PATH="$HOME/.bun/bin:$PATH"
- bun install --frozen-lockfile
- bun run lint
- bun run test
tags:
'v*':
- step:
name: Build iOS and upload to TestFlight
deployment: Production
caches:
- bun
script:
- curl -fsSL https://bun.sh/install | bash
- export PATH="$HOME/.bun/bin:$PATH"
- bun install --frozen-lockfile
- bun run build
- bunx cap sync ios
- >
bunx @capgo/cli@latest build request com.example.app
--platform ios
--build-mode release
--ios-distribution app_store
custom:
ios-adhoc:
- step:
name: Ad hoc iOS build for testers
caches:
- bun
script:
- curl -fsSL https://bun.sh/install | bash
- export PATH="$HOME/.bun/bin:$PATH"
- bun install --frozen-lockfile
- bun run build
- bunx cap sync ios
- echo "$ADHOC_PROFILE_BASE64" | base64 --decode > adhoc.mobileprovision
- >
bunx @capgo/cli@latest build request com.example.app
--platform ios
--build-mode release
--ios-distribution ad_hoc
--ios-provisioning-profile ./adhoc.mobileprovision
--output-upload
--output-retention 2d
--output-record /tmp/build.json
- bunx @capgo/cli@latest build last-output --path /tmp/build.json --field outputUrl
중요한 세부 사항 몇 가지:
- 이미지는 일반 Linux
node:22. 이 파일에는 macOS가 필요하지 않습니다. - Linux에서는
cap sync ios웹 에셋을 복사하고 네이티브 설정을 업데이트하지만 CocoaPods를 실행할 수 없습니다. 괜찮습니다. 빌드를 실행하는 Mac이 pods를 설치하거나 Swift 패키지를 직접 해결합니다. - Capgo은 App Store Connect에서 최신 빌드 번호를 읽고 증가시키므로, “bundle version must be higher than the previously uploaded version” 오류가 발생하지 않습니다.
--skip-build-number-bump직접 관리하는 경우입니다. - Ad hoc 빌드는 Ad Hoc provisioning profile이 필요하며, 이는 목록에 있는 기기에서만 설치됩니다. 보안
ADHOC_PROFILE_BASE64변수로 저장하세요.--ios-provisioning-profile플래그는 이 빌드에서만 환경 변수의 App Store profile을 덮어씁니다. 테스터 UDID를 iOS UDID 찾기. - CLI을 정확한 버전(예:
@capgo/cli@7.104.0)으로 고정하세요. 파이프라인이 안정화되면 CLI 릴리스가 스프린트 중간에 동작을 변경하지 않도록 하기 위함입니다.
커스텀 파이프라인을 Pipelines > Run pipeline > ios-adhoc. 마지막 명령은 보존 기간이 지나면 만료되는 다운로드 링크를 출력합니다.
TestFlight 전 수동 승인
Bitbucket은 trigger: manual 를 스텝에서 지원합니다. 각 TestFlight 업로드를 사람이 승인하도록 하려면 릴리스를 빌드 스텝과 게이트된 업로드 스텝으로 분리하세요:
tags:
'v*':
- step:
name: Test
script:
- curl -fsSL https://bun.sh/install | bash
- export PATH="$HOME/.bun/bin:$PATH"
- bun install --frozen-lockfile
- bun run test
- step:
name: Ship iOS to TestFlight
trigger: manual
deployment: Production
script:
- curl -fsSL https://bun.sh/install | bash
- export PATH="$HOME/.bun/bin:$PATH"
- bun install --frozen-lockfile
- bun run build
- bunx cap sync ios
- bunx @capgo/cli@latest build request com.example.app --platform ios --build-mode release
TestFlight 업로드 후 바로 App Review로 넘어가려면 --submit-to-store-review 포함 --store-release-name "$BITBUCKET_TAG"을 추가하세요. App Store Connect에서 먼저 App Store 리스팅, 스크린샷, 컴플라이언스 답변이 완료되어 있어야 합니다.
Android를 병렬로 추가
Android 빌드는 Bitbucket의 Linux 러너에서 직접 실행할 수 있지만, 두 플랫폼이 동일한 서명 및 업로드 플로우를 사용하도록 Capgo Build로 보내는 것도 가능합니다. parallel 블록:
tags:
'v*':
- parallel:
- step:
name: iOS release
script:
- curl -fsSL https://bun.sh/install | bash
- export PATH="$HOME/.bun/bin:$PATH"
- bun install --frozen-lockfile && bun run build
- bunx cap sync ios
- bunx @capgo/cli@latest build request com.example.app --platform ios --build-mode release
- step:
name: Android release
script:
- curl -fsSL https://bun.sh/install | bash
- export PATH="$HOME/.bun/bin:$PATH"
- bun install --frozen-lockfile && bun run build
- bunx cap sync android
- bunx @capgo/cli@latest build request com.example.app --platform android --build-mode release --android-track internal
Android 스텝에는 ANDROID_KEYSTORE_FILE, KEYSTORE_KEY_ALIAS, KEYSTORE_KEY_PASSWORD, KEYSTORE_STORE_PASSWORD, 그리고 PLAY_CONFIG_JSON 보안 변수로 설정하세요. 다음과 같은 방식으로 내보내세요: --platform android.
네이티브 빌드 없이 웹 전용 변경 사항 배포
Capacitor 앱에 대한 대부분의 커밋은 HTML, CSS, JavaScript만 수정합니다. 이 경우 새로운 바이너리가 필요하지 않습니다. Capgo Live Update 사용하기을(를) 사용하면 Bitbucket 단계에서 스토어 빌드와 OTA 번들 중 하나를 선택할 수 있습니다:
branches:
main:
- step:
name: Release (live update or native)
script:
- curl -fsSL https://bun.sh/install | bash
- export PATH="$HOME/.bun/bin:$PATH"
- bun install --frozen-lockfile
- bun run build
- |
if bunx @capgo/cli@latest build needed com.example.app --channel production; then
echo "No native change, shipping a live update"
bunx @capgo/cli@latest bundle upload com.example.app --channel production
else
echo "Native change detected, building iOS"
bunx cap sync ios
bunx @capgo/cli@latest build request com.example.app --platform ios --build-mode release
fi
build needed 는 네이티브 빌드가 필요하지 않으면 code 0, 필요하면 1로 종료됩니다. 채널에 라이브 중인 네이티브 플러그인 버전과 비교합니다. ios/하단의 수동 편집은 감지하지 않으므로, 해당 파일이 변경되면 네이티브 경로를 강제해야 합니다. 전체 패턴은 live update 또는 Native Build 자동 선택.
옵션 2: 셀프호스팅 macOS 러너
이미 Mac을 보유하고 있다면 이를 Bitbucket 러너로 등록하여 Xcode를 직접 실행할 수 있습니다.
러너 등록
- Bitbucket에서 열기 리포지토리 설정 > Runners (또는 Workspace settings > Workspace runners 공유하려면) 클릭하세요 Runner 추가.
- 선택 MacOS 를 시스템으로 설정합니다. Bitbucket에 runner UUID와 OAuth 인증 정보가 포함된 시작 명령어가 표시됩니다.
- Mac에 지원되는 JDK(runner는 Java 프로그램입니다), Xcode 26, fastlane을 설치하세요. 그런 다음 시작 명령어를 붙여넣으세요.
- 재부팅 후에도 살아 있도록 runner를 launch agent로 계속 실행해 두세요.
스텝은 레이블로 runner를 타겟팅합니다:
pipelines:
tags:
'v*':
- step:
name: iOS build on our Mac
runs-on:
- self.hosted
- macos
script:
- bun install --frozen-lockfile
- bun run build
- bunx cap sync ios
- echo "$BUILD_CERTIFICATE_BASE64" | base64 --decode > dist.p12
- echo "$PROFILE_BASE64" | base64 --decode > profile.mobileprovision
- bundle exec fastlane ios beta
macOS runner는 컨테이너가 아니라 호스트에서 사용자 환경 그대로 직접 실행됩니다. Mac에 Bun와 Ruby를 직접 설치하세요.
최소 Fastfile
default_platform(:ios)
platform :ios do
lane :beta do
setup_ci(force: true)
api_key = app_store_connect_api_key(
key_id: ENV["APPLE_KEY_ID"],
issuer_id: ENV["APPLE_ISSUER_ID"],
key_content: ENV["APPLE_KEY_CONTENT"],
is_key_content_base64: true
)
import_certificate(
certificate_path: "dist.p12",
certificate_password: ENV["P12_PASSWORD"],
keychain_name: "fastlane_tmp_keychain",
keychain_password: ""
)
install_provisioning_profile(path: "profile.mobileprovision")
update_code_signing_settings(
path: "ios/App/App.xcodeproj",
use_automatic_signing: false,
team_id: ENV["APP_STORE_CONNECT_TEAM_ID"],
code_sign_identity: "Apple Distribution",
profile_name: ENV["PROFILE_NAME"]
)
increment_build_number(
xcodeproj: "ios/App/App.xcodeproj",
build_number: latest_testflight_build_number(api_key: api_key) + 1
)
build_app(
project: "ios/App/App.xcodeproj", # use workspace: "ios/App/App.xcworkspace" for CocoaPods projects
scheme: "App",
export_method: "app-store"
)
upload_to_testflight(api_key: api_key, skip_waiting_for_build_processing: true)
end
end
새 Capacitor 8 프로젝트는 Swift Package Manager를 사용하므로, 빌드할 때 App.xcodeproj. CocoaPods 빌드를 사용하는 구형 프로젝트 App.xcworkspace 필요합니다 pod install 러너에 설치해야 합니다. 저희 GitHub Actions iOS 가이드 에서 각 fastlane 액션을 자세히 설명합니다. Bitbucket에서도 레인은 동일합니다.
가입 시 제공되는 것
아카이브는 쉬운 부분입니다. 셀프호스트 Mac을 사용하면:
- Xcode 업그레이드 Apple이 최소 SDK를 높일 때마다, 마감일 전에 테스트하여 진행합니다.
- Keychain 상태. 크래시한 job이 임시 keychain을 남길 수 있으며, 다음 job이 잘못된 identity를 가져갈 수 있습니다.
- 격리 없음. Job들이 동일한 디스크에서 실행됩니다. DerivedData, 오래된 archive, 시뮬레이터가 디스크를 가득 채웁니다.
- runner당 한 번에 하나의 build 추가 Mac을 구매하지 않는 한.
- 매년 만료되어 기계에서 갱신해야 하는 인증서와 프로필
팀이 이미 Mac을 관리하고 있다면 합리적인 선택입니다. 그 box를 관리하고 싶지 않다면 Option 1이 이를 제거합니다.
문제 해결
| 증상 | 가능한 원인 및 해결 방법 |
|---|---|
CAPGO_TOKEN is not set |
변수가 누락되었거나, 배포 변수인데 해당 단계에 deployment: 키가 없습니다. |
단계 작동 대상 main 태그에서는 작동하지 않음 |
배포 변수는 해당 배포 환경을 선언한 단계에만 적용됩니다. |
Provisioning profile doesn't match bundle ID |
이 프로필은 다른 App ID용으로 생성되었습니다. 다시 실행한 뒤 build init 다시 내보내세요. |
| App Store Connect 인증 실패 | Issuer ID 또는 Key ID가 잘못되었거나 키가 해지되었습니다. App Store Connect > Users and Access > Integrations에서 키를 확인하세요. |
| SDK 버전 업로드 거부됨 | 빌드에 Xcode 26 미만 버전이 사용되었습니다. Self-hosted Mac을 업그레이드하세요. |
| Self-hosted 단계가 “Pending” 상태에서 멈춤 | 러너 오프라인 또는 라벨 불일치 self.hosted 및 macos. |
errSecInternalComponent 코드 서명 중 |
비대화형 세션에서 키체인이 잠겨 있습니다. 다음을 사용하세요 setup_ci 그리고 인증서를 임시 키체인에 가져오세요. |
| 빌드는 통과하지만 TestFlight에 아무것도 없음 | 처리에는 몇 분 정도 걸리며, 내보내기 규정 준수 상태가 대기 중일 수 있습니다. 다음을 추가하세요 ITSAppUsesNonExemptEncryption 대상 Info.plist 앱이 표준 암호화만 사용하는 경우. |
실패한 파이프라인에 대한 추가 수정 사항은 Capacitor CI/CD 파이프라인 빌드 실패 수정 및 Capgo 빌드 문제 해결 가이드.
어떤 옵션을 선택해야 할까요?
하드웨어를 이미 보유하고 있거나, Xcode 환경을 완전히 제어해야 하거나, Capacitor 앱이 아닌 네이티브 Swift 타겟을 빌드해야 한다면 셀프호스트드 Mac을 선택하세요. 앱이 Capacitor 앱이고 팀이 CI를 설정하는 단일 플랫폼으로 Bitbucket을 유지하고 싶다면 Linux 단계에서 클라우드 빌드를 선택하세요. 어느 경우든 빌드 로직은 bitbucket-pipelines.yml code 옆에 서명 자료를 보안 변수에 넣고, 네이티브 code이 실제로 바뀐 경우에만 네이티브 빌드를 실행하세요.