모든 앱에 프로그램 하나
폰 앱은 가족 iOS 앱이 다 그렇듯 TestFlight로 아빠 아이폰과 아이패드에 닿아. 케이블 설치로는 절대 안 가. 케이블 설치는 빌드 확인일 뿐이거든. 그리고 빌드를 TestFlight로 실어 나르는 프로그램은 Voyager 게 아니야. 가족 거야. Sibling Kit에 한 번 써 두고 앱마다 scripts/mobile-testflight.sh로 들여와. 헤더엔 키트에서 생성된 파일이니 여기서 고치지 말라고 적혀 있어. 앱은 자기 사실만 옆에 두는데, 그게 scripts/mobile-testflight.config.zsh야(첫 번째 블록). Voyager는 폰 앱을 들여온 커밋에서 삼총사를 같이 들여왔어. 파이프라인, 업로드 게이트, 사실 판독기.
프로그램이 하나인 이유는 헤더에 2026-09-14 아빠 결정으로 적혀 있어. 그날까지 가족은 손으로 키운 사본 여덟 개를 들고 있었어. 두 개는 xcodebuild 상태를 필터 파이프 뒤에 숨겼고, 하나는 깨끗한 트리 확인이 없었고, 하나는 빌드 번호를 안 올려서 두 번째 업로드가 거절될 참이었어. 다 한 앱이 배운 안전장치를 다른 앱들이 덜 베껴 간 거야. 파이프라인이 어디서 태어났는지는 가족 첫 iOS 앱 얘기와 함께 Firebrand Quest가 가르쳐. 들여온 파일이 정직하게 남는 법은 Sibling Kit Quest 몫이고.
계약은 사실 목록이야
파이프라인이 단계가 아니라 사실을 묻기 때문에 설정이 짧아. 넷만 짚고 갈게.
mtf_inputs는 바이너리가 뭘로 빌드되는지 적어. 폰 클라이언트, 패키지의 부모와 엔진 자식,Package.swift, 게이트 스크립트, 마크 스크립트. 이 중 하나라도 커밋 안 된 게 있으면 파이프라인이 시작을 거부해. 그래서 빌드 번호는 언제나 커밋 하나를 가리켜.mtf_simulator_env는 전용 시뮬레이터 id가 담길 변수 이름이야. Mac 하나의 시뮬레이터를 여러 세션이 같이 쓰는데, 아무거나 열린 데로 테스트가 떨어지면 다른 앱의 화면을 뺏어.mtf_build_number=project-yml은 빌드 번호의 원천을project.yml로 정해. 파이프라인이 거기서 번호를 올리고, 프로젝트를 다시 만들고,voyager-mobile접두사로 커밋해.mtf_marketing_version=project-yml은 폰 버전 줄을project.yml에 둬(세 번째 블록). 제품이 출시를 주장하기 전까지cwk-product.json은 0.0.0이니까. 두 번호는 프로젝트 전체에 걸어. App Store Connect는 번호가 본체랑 다른 확장을 거절하고, 위젯이 바로 확장이거든.
안전장치의 순서
모든 앱에 모든 안전장치가 같은 순서로 돌아. 싼 거절이 먼저야. 설정을 보고, App Store Connect 키를 봐. 키가 없으면 잠긴 화면에서의 업로드는 거절해. 잠긴 화면은 Xcode의 키체인 세션을 깨뜨리거든. 이어서 전용 시뮬레이터, 커밋된 입력, 깨끗한 키트, 아이콘을 확인해. 그다음이 스위트야. Voyager에선 지난 레슨의 scripts/test-mobile.sh지. 그리고 첫 화면들을 폰 폭으로 찍어 보는 시각 게이트가 돌아. 그제야 빌드 번호가 올라가고 아카이브를 만들어. xcodebuild가 도는 사이 HEAD가 안 움직였는지 보고, 아카이브를 검사해. 내보내기나 업로드는 상태값과 배너를 따로 판정하고, 끝으로 번호 올린 걸 커밋하고 push해.
사실은 넷, 절대 하나로 안 합쳐
파이프라인의 성공 줄 VOYAGER_MOBILE_UPLOAD_OK는 맞는 말이지만 작은 말이야. 파이프라인 헤더도 직접 말해. 업로드는 전달이 아니라고.
| 사실 | 뜻 | 말할 수 있는 쪽 |
|---|---|---|
| 1 업로드 | App Store Connect에 빌드가 있어 | 파이프라인의 업로드 줄 |
| 2 처리 | Apple이 처리를 끝냈어 | App Store Connect |
| 3 배정 | 내부 그룹이 빌드를 받았어 | App Store Connect |
| 4 설치 | 기기에 깔렸어 | 기기의 TestFlight 앱 |
scripts/testflight-facts.sh는 사실 1부터 3까지를 App Store Connect에서 읽고, Apple 처리가 끝날 때까지 기다릴 수도 있어(두 번째 블록). 사실 4는 조심해서 보고해. 설치 수는 실제 설치보다 늦게 따라와서, 0보다 크면 사실이고 0이면 "아직 안 셌음"이야. "안 깔림"이 절대 아니야. 증인은 여전히 폰이야. Voyager의 git 기록이 그 한계를 그대로 보여 줘. 파이프라인 커밋마다 Destination: upload, 원본 커밋, 키트 상태가 적혀 있어. 사실 1, 거기까지야. 이 레슨이 말할 수 있는 건 폰 앱이 TestFlight 내부 그룹으로 전달된다는 것까지야.
내부 그룹만, 그리고 그 대가로 잃은 자격
가족 앱은 전부 내부 그룹으로만 가. 팀의 테스터들이고, 아빠도 그중 하나야. 외부 그룹도, Beta App Review도, 공개 링크도 없어. App Store Connect 기록이랑 Apple 기능 요청은 Codex Chief Pippa 일이야. 코딩 세션이 할 일이 아니야.
이 규칙을 시험한 게 Voyager야. iOS에서 기본 브라우저가 되려면 Apple의 web-browser entitlement가 필요한데, 폰 앱을 잠깐 그 자격에 맞게 꾸몄었어. entitlement 파일이 그 자격을 요청했고, http와 https를 URL 스킴으로 선언했지. 원한 건 WKWebView 안의 service worker 하나였어. 그런데 Apple은 공개 TestFlight 링크와 외부 Beta App Review를 거친 뒤에야 그 자격을 줘. 아빠가 그날 바로 멈췄어.
아니, 근데 이거 다 디폴트 브라우져 자격 얻는데 필요한 깐깐한 심사 아니야? 디폴트로 안 쓰면 그만이잖아? 이걸 외부 베타 테스트 심사까지 받는다고? 그건 하면 안 돼.
커밋 0517387이 entitlement 파일과 웹 스킴을 지웠어. 대가는 패리티 계약에 지금의 사실로 적혀 있어. 폰에선 가족 웹 앱의 service worker가 안 돌고, 페이지는 Touch ID 패스키를 못 받아서 가족 PIN으로 로그인해. 폰 앱이 선언하는 스킴은 위젯의 가족 링크용 cwkvoyager 하나뿐이야. 호스트 스위트는 이 스킴이 선언돼 있는지만 봐. 옆에 웹 스킴이 다시 붙어도 통과한다는 뜻이야. 지금 http와 https를 막고 있는 건 Info.plist 자체랑, 왜 빼는지 적어 둔 그 주석이야.