본문 바로가기
C.W.K.
Lesson 04 of 04 · published

TestFlight, 내부 그룹만

~14 min · TestFlight, iOS, 출시 파이프라인, 전달

Level 0새 탭
0 XP0/44 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete

모든 앱에 프로그램 하나

폰 앱은 가족 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 자체랑, 왜 빼는지 적어 둔 그 주석이야.

원칙: 전달 규칙이 진짜가 되는 건, 기능 하나와 맞붙어서 그 기능이 지는 순간이야.

Code

cwkVoyager, scripts/mobile-testflight.config.zsh: 가족 파이프라인에 건네는 앱의 사실 (원문 그대로, 1-23행)·shell
# cwkVoyager's facts for the family TestFlight pipeline (scripts/mobile-testflight.sh, vendored
# from cwkSiblingKit — edit the pipeline there, and this file here). The contract is the header
# of scripts/mobile-testflight.sh.

mtf_app=Voyager
mtf_marker=VOYAGER_MOBILE
mtf_mobile=clients/apple/VoyagerMobile
mtf_project=VoyagerMobile.xcodeproj
mtf_scheme=VoyagerMobile
mtf_bundle_id=com.cwk.voyager.mobile
mtf_icon=clients/apple/VoyagerMobile/Resources/Assets.xcassets/AppIcon.appiconset/icon-1024.png
mtf_simulator_env=VOYAGER_MOBILE_SIMULATOR_ID
# The app is built from its client, the package's parent and engine child, and Pippa's mark
# script; the gate script is part of what was checked.
mtf_inputs=(clients/apple/VoyagerMobile Sources/VoyagerCore Sources/VoyagerWebKit Package.swift
  scripts/test-mobile.sh scripts/copy-pippa-mark.swift)
mtf_build_number=project-yml
mtf_watch=no
mtf_commit_subject=voyager-mobile

# cwk-product.json stays 0.0.0 until the product claims a release, so the phone app carries
# its own bundle line in project.yml (cwkPathfinder's shape).
mtf_marketing_version=project-yml
cwkVoyager, scripts/testflight-facts.sh: 따로 지키는 네 가지 사실 (원문 그대로, 4-19행)·shell
# The four delivery facts after a TestFlight upload, read from App Store Connect —
# cwkPippa/docs/IOS-NATIVE-DELIVERY.md §1 keeps them apart, and the pipeline's
# `*_UPLOAD_OK` line asserts only the first:
#
#   1 uploaded       App Store Connect has the build
#   2 processed      Apple finished processing it (processingState VALID)
#   3 assigned       it is in internal testing (internalBuildState IN_BETA_TESTING)
#   4 installed      a tester installed it (the build's install count)
#
#   testflight-facts.sh <bundle-id> <build-number> [--wait <seconds>]
#
# One line per fact, then a machine line:
#   TESTFLIGHT_FACTS build=<n> uploaded=yes|no processed=yes|no|failed assigned=yes|no installed=yes|not_counted|unknown
# Fact 4 is App Store Connect's usage metric, which trails installs (measured 2026-09-25: a
# year-long data point, counts not live). A count above zero is a fact; zero is "not counted
# yet", never "not installed" — the phone's TestFlight app stays the witness.
cwkVoyager, clients/apple/VoyagerMobile/project.yml: 앱과 위젯이 함께 쓰는 버전 한 쌍 (원문 그대로, 12-26행)·text
name: VoyagerMobile
options:
  bundleIdPrefix: com.cwk
  deploymentTarget:
    iOS: "18.4"
  createIntermediateGroups: true

# Project-wide so every target carries the SAME version pair; App Store
# Connect refuses an extension whose numbers differ from its host.
settings:
  base:
    CURRENT_PROJECT_VERSION: 3
    # The phone app's own bundle line; cwk-product.json stays 0.0.0 until
    # the product claims a release (mtf_marketing_version=project-yml).
    MARKETING_VERSION: 0.1.0

External links

Exercise

내보내는 iOS 앱의 출시 보고서를 네 줄로 써 봐. 사실 하나에 한 줄씩, 줄마다 그 사실을 세우는 도구 이름을 적는 거야. 그리고 사실 2와 3을 자동으로 확인하게 만들어. App Store Connect API 키로 빌드의 처리 상태와 내부 베타 상태를 조회하고, 둘 다 맞을 때까지 0이 아닌 값으로 끝나는 스크립트를 써. 정해진 시간 동안 다시 물어보는 대기 옵션도 붙여.
Hint
Voyager 판독기가 보는 필드는 processingState VALID와 internalBuildState IN_BETA_TESTING이야. 사실 4는 따로 둬. 0으로 읽히는 사용량 숫자는 아직 아무 증거도 아니니까 "아직 안 셌음"이라고 찍고 기기를 봐. 업로드 스크립트의 성공 줄엔 자기가 아는 것만, 그러니까 업로드만 적어. 그래야 아무도 그걸 전달로 읽지 않아.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인 — 댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.