外观
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 示例均复用它,不需要改变项目已有的构建工具。
bash
#!/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;替换成自己的标签。仓库需已包含上面的脚本。
yaml
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。
yaml
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()。
groovy
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 先执行:
bash
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。
