Integração CI/CD
Deixe compilação e assinatura com seu pipeline e entregue o artefato ao foxstore-cli. A CLI conecta às lojas pelo seu Runner sem iniciar o Desktop; não oferece HTTP API nem Runner hospedado. Veja comandos, parâmetros, arquivos JSON e códigos de saída na Referência CLI / API.
Preparar Runner e entradas
Os exemplos Apple exigem foxstore-cli pré-instalado para seu sistema e arquitetura, no PATH, além de Bash e jq. Execute primeiro foxstore-cli version --output json para registrar a versão. Não se presume que o instalador Desktop instale a CLI nem é necessária uma Action adicional de instalação Fox Store.
A compilação deve gerar antes um build/App.ipa assinado. Seu projeto gerencia compilação, assinatura e permissões da loja; a CLI não cria certificados nem compila aplicativos. Publique sequencialmente o mesmo aplicativo para evitar que dois pipelines gravem simultaneamente na mesma versão.
Configure estas variáveis:
| Nome | Tipo | Conteúdo |
|---|---|---|
APPLE_PRIVATE_KEY | CI Secret | Chave privada PEM completa da App Store Connect API Key, preservando quebras de linha. |
APPLE_ISSUER_ID, APPLE_KEY_ID | CI variable | Issuer ID e Key ID correspondentes à chave privada. |
APPLE_APP_ID | CI variable | ID numérico do aplicativo de destino no App Store Connect, enviado como string à CLI. |
--private-key-env RELEASE_KEY recebe o nome da variável, não o valor; por padrão lê APPLE_PRIVATE_KEY. Se a plataforma fornecer um caminho de arquivo Secret, use --private-key-file "$APPLE_KEY_FILE"; não coloque o caminho em APPLE_PRIVATE_KEY.
Script de publicação compartilhado
Salve o script abaixo como ci/publish-apple.sh no repositório. Todos os exemplos CI o reutilizam, sem mudar as ferramentas de compilação existentes.
#!/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"O código 0 do Apple preflight indica apenas análise bem-sucedida; pode retornar data.status: "blocked", por isso o script verifica ready. Um publish bem-sucedido retorna buildUploadId, confirmando a chamada de envio, não o processamento Apple, disponibilidade TestFlight ou aprovação. status apple retorna builds visíveis sem filtro app-id; compare bundleId, version e buildNumber de preflight.json. Pode haver atraso: uma captura sem o Build não comprova falha de envio.
Com --output json, sucessos vão para stdout e envelopes de erro para stderr. --output ndjson também emite apenas um resultado final, sem progresso contínuo. Um processo encerrado pelo Runner pode não produzir envelope completo; verifique o remoto como no código 7.
GitHub Actions
Adicione o job release aos jobs do workflow de compilação existente. Ele depende de build, que deve enviar App.ipa como artifact ipa, com o arquivo na raiz. A etiqueta personalizada foxstore indica um Runner com CLI, Bash e jq configurados; substitua pela sua. O repositório deve conter o script anterior.
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: 14Injete Secrets por env, sem inserir a chave privada no código shell. Veja configuração e escopo em Secrets do GitHub Actions. O artifact deve incluir apenas o diretório indicado, nunca chaves privadas ou todo o workspace. Restrinja também o acesso aos dados do aplicativo nos resultados.
GitLab CI
Configure as três variáveis comuns e um Secret protegido em CI/CD Variables. O exemplo salva PEM como valor; com o tipo File do GitLab, o valor será um caminho temporário e você deve adaptar o script para --private-key-file. Veja Variáveis CI/CD do GitLab.
Integre ao .gitlab-ci.yml mantendo o job build, que deve arquivar build/App.ipa. Os stages existentes devem incluir deploy; o Runner foxstore deve ter CLI, Bash e 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
Coloque o stage abaixo após a compilação do Declarative Pipeline existente, no mesmo workspace. Gere antes build/App.ipa e obtenha ci/publish-apple.sh. O Agent exige CLI, Bash e jq; configure os três IDs em environment e crie a credencial Secret text apple-private-key no Jenkins. O stage usa credentials binding; recomendamos disableConcurrentBuilds() nas options do pipeline.
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
}
}
}Veja Secret binding e post em Jenkins Pipeline. Não interpole chaves privadas em comandos Groovy nem ative logs de depuração que imprimam Secrets.
Trocar de provedor e tratar falhas
Após compilar, um pipeline pode chamar comandos independentes de vários provedores; o CI existente organiza dependências e concorrência. Por exemplo, para 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 jsonInjete previamente GOOGLE_PLAY_CREDENTIALS_JSON. Preserve a separação stdout/stderr e o tratamento dos códigos do script; não copie apenas as três linhas e ative novas tentativas incondicionais do job. WeChat exige Node.js e dois pacotes npm miniprogram CI; doctor/status do Huawei Android exige escolher explicitamente API Client, veja Parâmetros.
Códigos 2/3 geralmente exigem corrigir entradas ou credenciais; 4/5 exigem avaliar error.retryable e a etapa de gravação remota; 6 indica não suportado; com 7, mantenha a falha e concilie primeiro; para 10, preserve versão e erro. Não existem wait, cancel, reconcile ou --dry-run unificados. Se precisar esperar, configure consultas limitadas no pipeline e não repita publish antes de confirmar o status remoto.
