CI/CD 接入
讓現有流水線負責構建與簽名,再把產物交給 foxstore-cli。CLI 在你自己的 Runner 上直連商店,不需要啟動 Desktop,也不提供 HTTP API 或托管 Runner。全部命令、參數、JSON 請求檔案與退出碼見 CLI / API 參考。
Runner 與輸入準備
以下 Apple 示例要求 Runner 已預裝與你的系統和架構匹配的 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。 |
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(檔案放在 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: 14通過 env 注入 Secret,不把私鑰插入 shell 源碼。密鑰的配置和可用範圍見 GitHub Actions Secret 文檔。結果 artifact 只包含腳本指定目錄,不要把私鑰檔案或完整工作區加入歸檔;結果中的應用資料也應限制訪問。
GitLab CI
在項目 CI/CD Variables 中配置上述三個普通變量與一個受保護的 Secret。這個示例使用變量值保存 PEM;若用 GitLab 的 File 類型,變量值會變成臨時檔案路徑,應調整腳本改用 --private-key-file。參見 GitLab CI/CD Variables。
合併到已有 .gitlab-ci.yml,保留已有 build job,並讓它歸檔 build/App.ipa。deploy 必須包含在已有 stages 中,Runner 標籤 foxstore 需已預裝 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
將下面 stage 放在已有 Declarative Pipeline 的構建 stage 後,使用同一 workspace;先構建 build/App.ipa 並檢出 ci/publish-apple.sh。Agent 預裝 CLI、Bash、jq,三個 ID 由現有 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 配置無條件重試。微信需要額外 Node.js 與兩個 miniprogram CI npm 包;華為 Android 的 doctor/status 需顯式選 API Client 來源,見 參數參考。
退出碼 2/3 通常需修正輸入或憑據,4/5 結合 error.retryable 和遠端寫入階段判斷,6 表示未支援,7 保持流水線失敗並先對賬,10 留存版本和錯誤結果排查。不存在統一 wait、cancel、reconcile 或 --dry-run。需要持續等待時由流水線設定有限查詢次數;遠端狀態未確認前不重複 publish。
