CI/CD integration
Let your existing pipeline handle builds and signing, then pass artifacts to foxstore-cli. The CLI connects directly to stores from your own Runner, requires no Desktop, and provides neither an HTTP API nor hosted Runners. See the CLI / API reference for all commands, arguments, JSON request files, and exit codes.
Prepare the Runner and inputs
The Apple examples below require a preinstalled foxstore-cli matching the Runner's OS and architecture and available in PATH, plus Bash and jq. First run foxstore-cli version --output json to record the version. This page does not assume the desktop installer installs the CLI or depend on an extra Fox Store setup Action.
The build step must first produce a signed build/App.ipa. Your existing project manages building, signing, and store-account permissions; Fox Store CLI neither generates certificates nor builds apps. Serialize releases for the same app to avoid two pipelines writing the same version concurrently.
Configure these variables:
| Name | Type | Contents |
|---|---|---|
APPLE_PRIVATE_KEY | CI Secret | Complete PEM private key for the App Store Connect API Key, preserving newlines. |
APPLE_ISSUER_ID, APPLE_KEY_ID | CI variable | Issuer ID and Key ID corresponding to the private key. |
APPLE_APP_ID | CI variable | Numeric target-app ID in App Store Connect, passed to the CLI as a string. |
--private-key-env RELEASE_KEY takes a variable name, not its value; the default reads APPLE_PRIVATE_KEY. If the platform supplies a Secret file path, use --private-key-file "$APPLE_KEY_FILE"; do not put that path in APPLE_PRIVATE_KEY.
Shared publishing script
Save this script as ci/publish-apple.sh in your app repository. All CI examples reuse it without changing your existing build tools.
#!/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"An Apple preflight exit code of 0 only means analysis succeeded; it may still return data.status: "blocked", so the script also checks ready. A successful publish returns buildUploadId, meaning the upload call completed, not that Apple processing, TestFlight availability, or App Store approval is complete. status apple returns all builds visible to the credentials without an app-id filter. Match the target build using bundleId, version, and buildNumber in preflight.json. Results may appear later; a missing build in one snapshot does not prove upload failure.
With --output json, success goes to stdout and error envelopes to stderr. --output ndjson also currently emits only one final result, not live progress. A Runner-killed process may leave no complete envelope; reconcile remote state first, just as for exit code 7.
GitHub Actions
Add this release job under jobs in your existing build workflow. It depends on an existing build job that uploads App.ipa as a workflow artifact named ipa, with the file at the artifact root. The custom Runner label foxstore indicates CLI, Bash, and jq are already configured; replace it with your label. The repository must already contain the script above.
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: 14Inject Secrets through env, never by inserting private keys into shell source. See GitHub Actions Secrets for configuration and scope. Archive only the script's result directory, not private-key files or the full workspace. Restrict access to app information in results too.
GitLab CI
Configure the three ordinary variables and one protected Secret in project CI/CD Variables. This example stores PEM in a variable value. GitLab's File type instead yields a temporary file path, so adapt the script to --private-key-file. See GitLab CI/CD Variables.
Merge into the existing .gitlab-ci.yml, keeping the build job and having it archive build/App.ipa. Existing stages must include deploy; Runners tagged foxstore must have CLI, Bash, and jq preinstalled.
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
Place this stage after the build stage in your existing Declarative Pipeline using the same workspace. First build build/App.ipa and check out ci/publish-apple.sh. The Agent needs CLI, Bash, and jq; configure the three IDs in the existing environment. Create a Secret text credential named apple-private-key in Jenkins. This stage uses credentials binding; also consider disableConcurrentBuilds() in your existing pipeline options.
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
}
}
}See the Jenkins Pipeline documentation for Secret binding and post. Never interpolate private keys into Groovy commands or enable debug logging that prints Secrets.
Switching Providers and handling failures
A pipeline can invoke independent commands for multiple Providers after building; your existing CI handles dependencies and concurrency. For example, start an Android flow with:
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 jsonThis example requires GOOGLE_PLAY_CREDENTIALS_JSON to be injected first. In a real pipeline, preserve the script's stdout/stderr separation and exit-code handling; do not simply copy these three lines and enable unconditional retries for the entire release job. WeChat also requires Node.js and two miniprogram CI npm packages. Huawei Android doctor/status must explicitly select an API Client source; see the parameter reference.
Exit codes 2/3 usually require input or credential fixes; evaluate 4/5 with error.retryable and the remote-write stage. 6 means unsupported. For 7, keep the pipeline failed and reconcile first. For 10, retain the version and error results. There are no unified wait, cancel, reconcile, or --dry-run commands. If waiting is needed, use a bounded number of pipeline queries; do not publish again until remote state is confirmed.
