CI genérico: github-actions. Aqui: code sign, archive e App Store Connect. UI: swiftui-development. Testes: swift-testing. Android: github-actions-android.
Detecte nativo (Xcode/fastlane) vs Expo (eas.json). Runners macOS são caros — jobs curtos e cache.
Regras inegociáveis
- Sem
.p12,.mobileprovisionou.p8no git. Secrets: API Key e (se Match) credenciais do repo de certs. - Preferência: App Store Connect API Key (
AUTHKEY_P8,ASC_KEY_ID,ASC_ISSUER_ID) — não Apple ID + senha/app-specific no YAML. - TestFlight antes de App Store. Prod (
deliver/ submit review) comenvironment: production+ reviewer. - Scheme e
CODE_SIGN_STYLEalinhados ao perfil Distribution (App Store), não Development no job de upload. macos-latestou a imagem que pinna o Xcode doxcode-versiondo projeto.
CI (nativo)
jobs:
test:
runs-on: macos-latest
permissions: { contents: read }
timeout-minutes: 45
steps:
- uses: actions/checkout@v4
- uses: actions/cache@v4
with:
path: ~/Library/Developer/Xcode/DerivedData
key: dd-${{ hashFiles('**/*.xcodeproj/**', '**/Package.resolved', '**/Podfile.lock') }}
- run: xcodebuild test -scheme App -destination 'platform=iOS Simulator,name=iPhone 16' -quiet
- Unitários / Swift Testing no Simulator. XCUITest só no caminho crítico ou
workflow_dispatch— flaky e lento (swift-testing). - SPM: cache
SourcePackagesse o setup já não cobrir. CocoaPods:pod install+ cachePodsse o repo usa Pods. xcode-select -squando a imagem tiver vários Xcodes.
Assinatura
| Método | Quando |
|---|---|
| API Key + Fastlane Match | Time; certs num repo/S3 privado |
| Import keychain | Cert + profile em secrets (base64) — último recurso |
| Xcode Cloud | Se o produto já estiver lá — não force GHA |
Match:
MATCH_PASSWORD,MATCH_GIT_URL(ou S3),APP_STORE_CONNECT_API_KEY_*.match appstoreno job de release;readonly: trueno CI para não rotacionar cert a cada run.create-keychain/setup_cido Fastlane no runner (keychain temporário).
Nunca CODE_SIGNING_ALLOWED=NO no archive de loja.
Archive e upload
deploy-testflight:
needs: test
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
runs-on: macos-latest
environment: testflight
timeout-minutes: 90
steps:
- uses: actions/checkout@v4
# match + gym + pilot (ou xcodebuild archive + altool/notary)
- Fastlane:
gym(archive) →pilot/upload_to_testflight. Skip waiting for processing se o time já tiver hook. - Sem Fastlane:
xcodebuild -archivePath+xcodebuild -exportArchive+xcrun altool/notarytool/iTMSTransporter— o que o repo já usa. - Export options:
method: app-store, team ID correto. - Artifact: IPA em
upload-artifactretenção curta.
App Store (review)
- Job separado,
environment: production, não em todo merge. - Metadata/
deliversó se o time versionar screenshots/notes; senão upload binário + review manual no Console. - Build number (
CFBundleVersion) sempre sobe (agvtool, Fastlaneincrement_build_number, ouGITHUB_RUN_NUMBERdocumentado).
Expo / React Native
EXPO_TOKEN;eas build --platform ios+eas submit --platform ios.- Credenciais iOS no EAS (não duplicar Match + EAS no mesmo app sem dono).
- Profile production → TestFlight primeiro.
Custo e flakiness
timeout-minutesem todo job macOS.concurrencyno workflow de deploy (um archive por vez).- Não rode matrix de 4 Simulators no PR.
Anti-padrões
- Apple ID e senha no
fastlaneenv - Certificado Development no submit
xcodebuildsem-destination/ scheme errado- Submit App Store em todo push
- Commit de
AuthKey_*.p8 - UI tests como gate único do merge (sem unit)
Integração com outras skills
- Pipeline:
github-actions - App:
swiftui-development - Testes:
swift-testing - Android:
github-actions-android - Widgets no mesmo IPA:
widgetkit(mesmo archive) - Go-live:
production-readiness
Critérios de conclusão
- Testes de unit no Simulator no PR
- Archive Distribution assinado via API Key / Match (secrets)
- Upload TestFlight (prod protegido)
- Build number crescente
- Sem certs no git; timeout no job macOS