빌드 배포
CI 파이프라인을 연결하면 빌드가 App Center에 자동으로 등록되어 팀원 누구든 바로 확인할 수 있습니다.
동작 방식
CI 파이프라인
→ POST /api/v1/builds (tapflow relay)
→ App Center에 빌드 등록
→ 팀원이 브라우저에서 기기를 선택하고 직접 테스트
PO / PM: 기획한 대로 나왔는지
디자이너: 디자인 스펙에 맞는지
백엔드: API 연동이 맞는지
QA: 버그가 있는지CI 잡이 빌드 파일을 업로드하면 누구나 별도 도구 설치나 기기 세팅 없이 브라우저에서 바로 테스트할 수 있습니다.
수동 리뷰
이 가이드는 수동 리뷰를 다룹니다. CI가 빌드를 전달하면 팀원이 직접 테스트합니다.
CI에서 플로우를 재생하거나 LLM 에이전트가 기기를 조작하게 하려면 CI/CD에서 MCP 활용을 참고하세요. 둘 다 실험적 기능입니다.
어떤 빌드 도구든
tapflow는 빌드 도구가 아니라 완성된 아티팩트를 다룹니다. 네이티브 Xcode·Gradle 빌드, React Native, Flutter, 클라우드 빌드 서비스가 만드는 빌드를 모두 같은 방식으로 업로드합니다. 아티팩트(.app.zip·.tar.gz/.tgz·.apk)만 나오면 이후 과정은 이 페이지 그대로입니다.
사전 조건
| 항목 | 설명 |
|---|---|
| tapflow 릴레이 | 실행 중이고 CI 환경에서 접근 가능해야 합니다 |
| 개인 액세스 토큰(PAT) | Settings → Tokens에서 API 종류로 생성합니다(builds:write 권한 포함). 이 메뉴는 Admin, Developer, QA에게 보입니다 |
CI가 릴레이에 도달하려면
CI 잡이 릴레이의 POST /api/v1/builds에 접근할 수 있어야 합니다. 릴레이는 에이전트와 같은 내부 네트워크에 두는 것이 원칙입니다(배포 방식 선택). 그래서 CI가 어디서 실행되는지에 따라 경로가 갈립니다.
| 릴레이 배치 | CI가 업로드하는 방법 |
|---|---|
| LAN 전용 (기본) | 클라우드 러너(GitHub 호스티드 등)는 LAN 릴레이에 닿지 못합니다. 내부 네트워크에 둔 self-hosted 러너에서 릴레이 내부 주소(http://192.168.x.x:4000)로 업로드하세요 |
| VPS + rathole 터널 | 릴레이를 외부 접근용으로 열어 두면 공개 URL(https://your-vps.com)로 어디서든 업로드할 수 있어 클라우드 CI에 가장 잘 맞습니다 |
| Tailscale 터널 | tailnet 멤버만 접근할 수 있으므로 CI 러너도 tailnet에 연결돼 있어야 합니다 |
릴레이는 클라우드에 직접 올리지 않습니다
릴레이를 fly.io·Railway 같은 서비스에 직접 배포하면 에이전트→릴레이 구간이 인터넷을 타면서 스트림이 끊깁니다(미지원). 공개 접근이 필요하면 릴레이는 내부 네트워크에 둔 채 터널로 노출하세요. VPS는 릴레이 호스트가 아니라 터널 호스트입니다.
1. 토큰 생성
대시보드의 Settings → Tokens → New token에서 생성합니다. Tokens 메뉴는 Admin, Developer, QA에게 보이며 토큰은 만든 사람의 것이 됩니다.
- Name:
GitHub Actions처럼 용도를 알 수 있는 이름 - Expiration: 기본 30일. 7·30·60·90일, 직접 입력(1~365일), No expiration 중에서 고를 수 있으며 CI용이라면 90일 이하를 권장합니다.
- Type: API.
view, builds:write권한이 자동으로 부여됩니다.
토큰은 생성 시 한 번만 표시됩니다. CI 시크릿(예: TAPFLOW_PAT)으로 저장하세요.
2. 빌드 업로드
빌드 파일이 준비되면 POST /api/v1/builds를 호출합니다.
# iOS (.app.zip)
curl -X POST https://your-relay/api/v1/builds \
-H "Authorization: Bearer $TAPFLOW_PAT" \
-F "file=@MyApp.app.zip" \
-F "status=In Progress" \
-F "label=$GIT_BRANCH"
# Android (.apk)
curl -X POST https://your-relay/api/v1/builds \
-H "Authorization: Bearer $TAPFLOW_PAT" \
-F "file=@MyApp.apk" \
-F "status=In Progress" \
-F "label=$GIT_BRANCH"status=In Progress는 팀에게 리뷰 준비가 됐다는 신호입니다.label에는 브랜치명, 티켓 번호 등 맥락을 담을 수 있습니다. App Center의 빌드 행에 표시되고 검색으로 찾을 수 있습니다.
빌드 파일의 버전이나 빌드 번호로는 어떤 빌드인지 알 수 없을 때(같은 버전의 QA 빌드가 여러 개인 경우 등)는 version_name과 build_number도 보내세요. tapflow에서 빌드를 보여 주고 묶을 때 빌드 파일의 값 대신 쓰이며 파일 자체는 바뀌지 않습니다. 100자를 넘는 label은 100자로 잘립니다. version_name과 build_number는 100자를 넘으면 거절됩니다.
curl -X POST https://your-relay/api/v1/builds \
-H "Authorization: Bearer $TAPFLOW_PAT" \
-F "file=@MyApp.app.zip" \
-F "label=$GIT_BRANCH" \
-F "build_number=$CI_PIPELINE_ID"iOS 빌드
.ipa 파일은 지원하지 않습니다. 시뮬레이터 빌드는 .app.zip 또는 .tar.gz/.tgz를 올립니다. .app.zip은 xcodebuild -sdk iphonesimulator로 빌드해 .app 폴더를 zip으로 압축하고, .tar.gz/.tgz는 클라우드 시뮬레이터 빌드가 만드는 아카이브입니다.
3. 빌드 메타데이터 첨부 (선택)
커밋과 브랜치 정보를 댓글로 남기면 리뷰어가 무엇이 바뀌었는지 바로 확인할 수 있습니다.
BUILD_ID=$(curl -sf -X POST https://your-relay/api/v1/builds \
-H "Authorization: Bearer $TAPFLOW_PAT" \
-F "file=@MyApp.app.zip" \
-F "status=In Progress" \
-F "label=$GIT_BRANCH" | jq -r '.id')
curl -X POST https://your-relay/api/v1/comments \
-H "Authorization: Bearer $TAPFLOW_PAT" \
-F "build_id=$BUILD_ID" \
-F "body=Branch: $GIT_BRANCH
Commit: $GIT_SHA
$GIT_COMMIT_MSG"GitHub Actions 예시
이 예시는 릴레이 내부 주소에 닿는 self-hosted macOS 러너를 가정합니다. 릴레이를 VPS + rathole 터널처럼 공개 URL로 열어 두었다면 runs-on을 macos-latest 같은 클라우드 러너로 바꿔도 됩니다.
self-hosted 러너와 pull request
self-hosted 러너는 워크플로가 체크아웃한 코드를 내부 네트워크의 Mac에서 그대로 실행합니다. 아래의 if:는 포크에서 연 pull request를 건너뛰므로 같은 저장소의 브랜치만 이 러너에서 빌드됩니다. 이 조건을 지우지 말고 외부 pull request를 받는 공개 저장소에는 self-hosted 러너를 연결하지 마세요.
name: tapflow에 업로드
on:
push:
branches: [main, 'release/**']
pull_request:
jobs:
upload:
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository
runs-on: [self-hosted, macos]
steps:
- uses: actions/checkout@v4
- name: 앱 빌드
run: |
xcodebuild -scheme MyApp -sdk iphonesimulator \
-configuration Debug \
CONFIGURATION_BUILD_DIR=build/Debug
cd build/Debug && zip -r MyApp.app.zip MyApp.app
- name: tapflow에 업로드
env:
TAPFLOW_PAT: ${{ secrets.TAPFLOW_PAT }}
TAPFLOW_RELAY_URL: ${{ secrets.TAPFLOW_RELAY_URL }}
BRANCH: ${{ github.head_ref || github.ref_name }}
COMMIT: ${{ github.sha }}
COMMIT_MSG: ${{ github.event.head_commit.message || github.event.pull_request.title }}
run: |
BUILD_RESPONSE=$(curl -sf -X POST "$TAPFLOW_RELAY_URL/api/v1/builds" \
-H "Authorization: Bearer $TAPFLOW_PAT" \
-F "file=@build/Debug/MyApp.app.zip" \
-F "status=In Progress" \
-F "label=$BRANCH")
BUILD_ID=$(echo "$BUILD_RESPONSE" | jq -r '.id')
COMMENT="Branch: $BRANCH"$'\n'"Commit: $COMMIT"$'\n'"$COMMIT_MSG"
curl -sf -X POST "$TAPFLOW_RELAY_URL/api/v1/comments" \
-H "Authorization: Bearer $TAPFLOW_PAT" \
-F "build_id=$BUILD_ID" \
-F "body=$COMMENT"빌드 상태 참고
| 상태 | 의미 |
|---|---|
Backlog | 업로드됐지만 아직 리뷰 준비 전 |
In Progress | 준비 완료 — 팀이 테스트를 시작할 수 있음 |
Done | 이해관계자 승인 완료 |
Rejected | 문제 발견, 수정 필요 |
CI는 업로드 시 In Progress로 설정합니다. Done과 Rejected는 리뷰 후 팀원이 직접 변경합니다.