CI/CD 연결
기존 파이프라인에서 빌드와 서명을 처리한 다음 산출물을 foxstore-cli에 전달하세요. CLI는 자체 Runner에서 스토어에 직접 연결하며 Desktop 실행이 필요 없고 HTTP API나 호스팅 Runner도 제공하지 않습니다. 전체 명령, 인수, JSON 요청 파일과 종료 코드는 CLI / API 참조를 확인하세요.
Runner 및 입력 준비
아래 Apple 예제는 Runner의 OS와 아키텍처에 맞는 foxstore-cli가 미리 설치되어 PATH에 있어야 하며 Bash와 jq도 필요합니다. 먼저 foxstore-cli version --output json을 실행해 버전을 기록하세요. 데스크톱 설치 프로그램이 CLI를 설치한다고 가정하지 않으며 별도 Fox Store 설치 Action에도 의존하지 않습니다.
빌드 단계에서 서명된 build/App.ipa를 먼저 생성해야 합니다. 빌드, 서명과 스토어 계정 권한은 기존 프로젝트가 관리하며 Fox Store CLI는 인증서를 생성하거나 앱을 빌드하지 않습니다. 동일 앱 출시는 직렬 실행해 두 파이프라인이 같은 버전에 동시에 쓰지 않도록 하세요.
다음 변수를 설정하세요.
| 이름 | 유형 | 내용 |
|---|---|---|
APPLE_PRIVATE_KEY | CI Secret | 줄바꿈을 유지한 App Store Connect API Key의 전체 PEM 개인 키. |
APPLE_ISSUER_ID, APPLE_KEY_ID | CI variable | 개인 키에 해당하는 Issuer ID와 Key ID. |
APPLE_APP_ID | CI variable | App Store Connect 대상 앱의 숫자 ID. CLI에는 문자열로 전달합니다. |
--private-key-env RELEASE_KEY는 값이 아니라 변수 이름을 받으며 기본적으로 APPLE_PRIVATE_KEY를 읽습니다. 플랫폼에서 Secret 파일 경로를 제공하면 --private-key-file "$APPLE_KEY_FILE"을 사용하고 APPLE_PRIVATE_KEY에 경로를 넣지 마세요.
공통 출시 스크립트
이 스크립트를 앱 저장소의 ci/publish-apple.sh로 저장하세요. 모든 CI 예제가 이를 재사용하며 기존 빌드 도구는 바꾸지 않아도 됩니다.
#!/usr/bin/env bash
set -euo pipefail
: "${APPLE_ISSUER_ID:?Set APPLE_ISSUER_ID}"
: "${APPLE_KEY_ID:?Set APPLE_KEY_ID}"
: "${APPLE_APP_ID:?Set APPLE_APP_ID}"
: "${APPLE_PRIVATE_KEY:?Set APPLE_PRIVATE_KEY secret}"
test -s build/App.ipa
mkdir -p release-results
foxstore-cli version --output json > release-results/version.json
foxstore-cli artifact inspect --file build/App.ipa --output json \
> release-results/artifact.json 2> release-results/artifact-error.json
foxstore-cli doctor apple \
--issuer-id "$APPLE_ISSUER_ID" --key-id "$APPLE_KEY_ID" --output json \
> release-results/doctor.json 2> release-results/doctor-error.json
foxstore-cli preflight apple --file build/App.ipa --output json \
> release-results/preflight.json 2> release-results/preflight-error.json
jq -e '.status == "success" and .data.status == "ready"' \
release-results/preflight.json > /dev/null
if foxstore-cli publish apple \
--issuer-id "$APPLE_ISSUER_ID" --key-id "$APPLE_KEY_ID" \
--app-id "$APPLE_APP_ID" --file build/App.ipa --output json \
> release-results/publish.json 2> release-results/publish-error.json; then
publish_exit=0
else
publish_exit=$?
fi
printf '%s\n' "$publish_exit" > release-results/publish-exit-code.txt
# 一次查询留存快照;保持原发布退出码,不自动重发。
if [ "$publish_exit" -eq 0 ] || [ "$publish_exit" -eq 7 ]; then
if foxstore-cli status apple \
--issuer-id "$APPLE_ISSUER_ID" --key-id "$APPLE_KEY_ID" --output json \
> release-results/status.json 2> release-results/status-error.json; then
printf '%s\n' '已保存远端构建快照,请核对目标应用与构建号。'
else
printf '%s\n' '状态查询失败,请在 App Store Connect 核对结果。' >&2
fi
fi
if [ "$publish_exit" -eq 7 ]; then
printf '%s\n' '发布结果未知,请先对账,禁止自动重发。' >&2
fi
exit "$publish_exit"Apple preflight 종료 코드 0은 분석 성공만 뜻하며 data.status: "blocked"일 수 있어 스크립트가 ready도 확인합니다. publish 성공의 buildUploadId는 업로드 호출 완료를 뜻하며 Apple 처리 완료, TestFlight 사용 가능 또는 App Store 심사 통과를 의미하지 않습니다. status apple은 app-id 필터 없이 자격 증명으로 볼 수 있는 빌드 목록을 반환하므로 preflight.json의 bundleId, version, buildNumber로 대상 빌드를 대조하세요. 결과 반영이 늦을 수 있으므로 한 번의 스냅샷에 빌드가 없다고 업로드 실패로 판단하면 안 됩니다.
--output json의 성공 결과는 stdout, 오류 envelope는 stderr로 출력됩니다. --output ndjson도 현재 최종 결과 한 줄만 출력하며 실시간 진행률은 보내지 않습니다. Runner가 강제 종료하면 완전한 envelope가 없을 수 있으므로 종료 코드 7과 마찬가지로 원격 상태부터 대조하세요.
GitHub Actions
이 release job을 기존 빌드 workflow의 jobs 아래에 추가하세요. 기존 build job에 의존하며 이 job은 App.ipa를 루트에 담은 ipa라는 workflow artifact를 업로드해야 합니다. 사용자 지정 Runner 레이블 foxstore는 CLI, Bash와 jq를 미리 구성했다는 뜻이므로 자신의 레이블로 바꾸세요. 저장소에는 위 스크립트가 있어야 합니다.
jobs:
# 保留已有 build job:构建签名后的 App.ipa,并上传名为 ipa 的 artifact。
release:
needs: build
runs-on: [self-hosted, foxstore]
permissions:
contents: read
concurrency:
group: apple-release-${{ github.repository }}
cancel-in-progress: false
env:
APPLE_ISSUER_ID: ${{ vars.APPLE_ISSUER_ID }}
APPLE_KEY_ID: ${{ vars.APPLE_KEY_ID }}
APPLE_APP_ID: ${{ vars.APPLE_APP_ID }}
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
name: ipa
path: build
- name: Preflight and upload
shell: bash
env:
APPLE_PRIVATE_KEY: ${{ secrets.APPLE_PRIVATE_KEY }}
run: bash ci/publish-apple.sh
- name: Save release results
if: always()
uses: actions/upload-artifact@v4
with:
name: release-results
path: release-results/
retention-days: 14Secret은 env로 주입하고 개인 키를 셸 소스에 삽입하지 마세요. 설정과 사용 범위는 GitHub Actions Secrets를 참고하세요. 스크립트가 지정한 결과 디렉터리만 보관하고 개인 키 파일이나 전체 워크스페이스는 포함하지 마세요. 결과의 앱 정보도 접근을 제한해야 합니다.
GitLab CI
프로젝트 CI/CD Variables에 일반 변수 3개와 보호된 Secret 1개를 설정하세요. 이 예제는 변수 값에 PEM을 저장합니다. GitLab의 File 유형은 임시 파일 경로를 값으로 제공하므로 스크립트를 --private-key-file 사용으로 변경하세요. GitLab CI/CD Variables를 참고하세요.
기존 .gitlab-ci.yml에 병합하고 기존 build job을 유지하여 build/App.ipa를 보관하게 하세요. 기존 stages에 deploy가 있어야 하며 foxstore 태그 Runner에는 CLI, Bash와 jq가 미리 설치되어야 합니다.
release_apple:
stage: deploy
tags: [foxstore]
needs:
- job: build
artifacts: true
resource_group: apple-release
script:
- bash ci/publish-apple.sh
artifacts:
when: always
paths:
- release-results/
expire_in: 14 daysJenkins
기존 Declarative Pipeline의 빌드 stage 뒤에 같은 workspace를 쓰는 아래 stage를 추가하세요. 먼저 build/App.ipa를 빌드하고 ci/publish-apple.sh를 체크아웃하세요. Agent에는 CLI, Bash, jq가 필요하고 ID 3개는 기존 environment에서 설정합니다. Jenkins에 ID가 apple-private-key인 Secret text credential을 만드세요. 이 stage는 credentials binding을 사용하며 기존 pipeline options에 disableConcurrentBuilds() 설정을 권장합니다.
stage('Release Apple') {
environment {
APPLE_PRIVATE_KEY = credentials('apple-private-key')
}
steps {
sh 'bash ci/publish-apple.sh'
}
post {
always {
archiveArtifacts artifacts: 'release-results/*', allowEmptyArchive: true
}
}
}Secret 바인딩과 post는 Jenkins Pipeline 문서를 참고하세요. Groovy에서 개인 키를 명령에 보간하거나 Secret을 출력하는 디버그 로그를 켜지 마세요.
Provider 변경 및 실패 처리
한 파이프라인에서 빌드 후 여러 Provider의 독립 명령을 호출할 수 있으며 의존성과 동시 실행은 기존 CI가 관리합니다. 예를 들어 Android에서는 먼저 다음을 실행합니다.
foxstore-cli preflight android --file build/App.aab \
--expected-package-name com.example.app --output json
foxstore-cli publish google-play --package-name com.example.app \
--file build/App.aab --track internal --release-status completed --output json
foxstore-cli status google-play --package-name com.example.app --output json이 예제에는 GOOGLE_PLAY_CREDENTIALS_JSON을 미리 주입해야 합니다. 실제 파이프라인에서는 위 스크립트의 stdout/stderr 분리와 종료 코드 처리를 유지하세요. 세 줄만 복사한 뒤 전체 출시 job에 무조건 재시도를 설정하면 안 됩니다. WeChat에는 Node.js와 miniprogram CI npm 패키지 2개가 추가로 필요합니다. Huawei Android doctor/status는 API Client 소스를 명시해야 합니다. 매개변수 참조를 확인하세요.
종료 코드 2/3은 보통 입력이나 자격 증명 수정이 필요하고 4/5는 error.retryable과 원격 쓰기 단계를 함께 판단합니다. 6은 미지원, 7은 파이프라인 실패 상태를 유지하고 먼저 대조, 10은 버전과 오류 결과를 보존해 조사합니다. 공통 wait, cancel, reconcile 또는 --dry-run은 없습니다. 계속 기다려야 한다면 파이프라인에서 조회 횟수를 제한하고 원격 상태가 확인되기 전에는 다시 publish하지 마세요.
