Introdução
Você ainda faz Archive → Distribute... manualmente no Xcode a cada release do seu app iOS?
Esse processo é demorado e tende a criar dependências de ambiente — o famoso problema: "na minha máquina funciona, mas na do outro não."
Neste artigo, vamos construir um pipeline completo para compilar, assinar e fazer upload automático de um app iOS para o TestFlight com GitHub Actions.
Muitos tutoriais usam fastlane, mas aqui adotamos uma abordagem diferente: usar apenas xcodebuild e xcrun altool, as ferramentas nativas da Apple — sem fastlane. Não é preciso introduzir um ambiente Ruby, o workflow fica mais simples e você se livra dos problemas de compatibilidade do fastlane em atualizações do Xcode.
Para autenticação, usamos uma chave de API do App Store Connect — mais segura e não afetada pela autenticação de dois fatores — em vez de Apple ID + senha.
Pré-requisitos
- Inscrição no Apple Developer Program (pago)
- App registrado no App Store Connect
- Projeto iOS publicado em um repositório GitHub
- Build local verificado no macOS / Xcode
Fluxo geral
O resumo do pipeline que vamos construir é o seguinte:
- O workflow é acionado por push no
mainou manualmente - O Xcode é preparado no runner macOS do GitHub Actions
- O certificado e o perfil de provisionamento são importados temporariamente no keychain
- O número de build é atualizado
- O
.xcarchiveé gerado comxcodebuild archive - O
.ipaé exportado comxcodebuild -exportArchive - O upload para o TestFlight é feito com
xcrun altool - O keychain temporário é deletado e a limpeza é realizada
push → Actions acionado → Instalar certificado → archive → export → altool → TestFlight
Passo 1: Preparar certificados e perfil de provisionamento
Para realizar a assinatura de código em CI, os dois arquivos a seguir são codificados em Base64 e armazenados nos GitHub Secrets.
1-1. Exportar o certificado de distribuição (.p12)
Abra o "Acesso às Chaves.app" no seu Mac local, clique com o botão direito no certificado Apple Distribution ou iPhone Distribution e exporte no formato .p12. Anote a senha definida durante a exportação — você vai precisar dela mais tarde.
Se não tiver um certificado, crie um na seção Certificates do site Apple Developer.
1-2. Baixar o perfil de provisionamento (.mobileprovision)
Baixe o perfil de provisionamento para distribuição na App Store na seção Profiles do site Apple Developer. Anote também o nome do perfil (ex.: YourApp AppStore) — você vai precisar colocá-lo no ExportOptions.plist.
1-3. Codificar em Base64
Execute os seguintes comandos no terminal para converter os arquivos em strings Base64.
# Certificado
base64 -i Certificates.p12 | pbcopy
# Perfil de provisionamento
base64 -i YourApp_AppStore.mobileprovision | pbcopy
O conteúdo é copiado para a área de transferência com pbcopy e pode ser colado diretamente nos GitHub Secrets.
Passo 2: Gerar uma chave de API do App Store Connect
Para evitar complicações com a autenticação de dois fatores, usamos uma chave de API.
- Faça login no App Store Connect
- Selecione "Usuários e acesso" → "Integrations" → "App Store Connect API"
- Gere uma chave com o botão "+" (função App Manager ou superior necessária)
- Salve o arquivo
AuthKey_XXXXXXXXXX.p8baixado (só pode ser baixado uma vez — guarde com cuidado) - Anote a Key ID e o Issuer ID exibidos ao gerá-la
Codifique também o arquivo .p8 em Base64:
base64 -i AuthKey_XXXXXXXXXX.p8 | pbcopy
Passo 3: Registrar nos GitHub Secrets
Em Settings → Secrets and variables → Actions do repositório, registre os seguintes Secrets:
| Nome do Secret | Conteúdo |
|---|---|
BUILD_CERTIFICATE_BASE64 |
String Base64 do certificado .p12 |
P12_PASSWORD |
Senha definida ao exportar o .p12 |
BUILD_PROVISION_PROFILE_BASE64 |
String Base64 do .mobileprovision |
KEYCHAIN_PASSWORD |
Senha arbitrária para o keychain temporário |
APP_STORE_CONNECT_API_KEY_ID |
Key ID da chave de API |
APP_STORE_CONNECT_API_ISSUER_ID |
Issuer ID |
APP_STORE_CONNECT_API_KEY_BASE64 |
String Base64 do .p8 |
Passo 4: Criar o ExportOptions.plist
Este arquivo de configuração é necessário para gerar um .ipa a partir de um .xcarchive. Faça commit no repositório como ios/ExportOptions.plist (não contém informações sensíveis).
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>method</key>
<string>app-store</string>
<key>teamID</key>
<string>ABCD123456</string>
<key>signingStyle</key>
<string>manual</string>
<key>signingCertificate</key>
<string>Apple Distribution</string>
<key>provisioningProfiles</key>
<dict>
<key>com.example.yourapp</key>
<string>YourApp AppStore</string>
</dict>
<key>uploadSymbols</key>
<true/>
<key>uploadBitcode</key>
<false/>
</dict>
</plist>
Passo 5: Criar o workflow do GitHub Actions
Aqui está a parte principal. Crie .github/workflows/ios-testflight.yml.
name: iOS TestFlight Deploy
on:
push:
branches: [main]
workflow_dispatch:
env:
SCHEME: YourApp
WORKSPACE: YourApp.xcworkspace # Usar PROJECT para .xcodeproj
CONFIGURATION: Release
EXPORT_OPTIONS_PLIST: ios/ExportOptions.plist
jobs:
deploy:
runs-on: macos-14
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Select Xcode
run: sudo xcode-select -s /Applications/Xcode_15.4.app
- name: Install the Apple certificate and provisioning profile
env:
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
BUILD_PROVISION_PROFILE_BASE64: ${{ secrets.BUILD_PROVISION_PROFILE_BASE64 }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: |
CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
PP_PATH=$RUNNER_TEMP/build_pp.mobileprovision
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
# Decodificar Base64
echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode -o $CERTIFICATE_PATH
echo -n "$BUILD_PROVISION_PROFILE_BASE64" | base64 --decode -o $PP_PATH
# Criar keychain temporário
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
# Importar certificado
security import $CERTIFICATE_PATH -P "$P12_PASSWORD" \
-A -t cert -f pkcs12 -k $KEYCHAIN_PATH
security set-key-partition-list -S apple-tool:,apple: \
-k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security list-keychain -d user -s $KEYCHAIN_PATH
# Colocar o perfil de provisionamento
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles
- name: Set build number
run: |
# Usar o número de execução do GitHub Actions como número de build (unicidade garantida)
BUILD_NUMBER=${{ github.run_number }}
/usr/libexec/PlistBuddy -c "Set :CFBundleVersion $BUILD_NUMBER" \
YourApp/Info.plist
echo "Build number set to $BUILD_NUMBER"
- name: Build archive
run: |
xcodebuild archive \
-workspace "$WORKSPACE" \
-scheme "$SCHEME" \
-configuration "$CONFIGURATION" \
-archivePath "$RUNNER_TEMP/YourApp.xcarchive" \
-destination "generic/platform=iOS" \
-allowProvisioningUpdates \
CODE_SIGNING_ALLOWED=YES \
| xcbeautify
- name: Export IPA
run: |
xcodebuild -exportArchive \
-archivePath "$RUNNER_TEMP/YourApp.xcarchive" \
-exportOptionsPlist "$EXPORT_OPTIONS_PLIST" \
-exportPath "$RUNNER_TEMP/export" \
| xcbeautify
- name: Prepare API key for altool
env:
APP_STORE_CONNECT_API_KEY_BASE64: ${{ secrets.APP_STORE_CONNECT_API_KEY_BASE64 }}
APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
run: |
# altool lê de ~/.appstoreconnect/private_keys/ ou ./private_keys/
mkdir -p ~/.appstoreconnect/private_keys
echo -n "$APP_STORE_CONNECT_API_KEY_BASE64" | base64 --decode \
-o ~/.appstoreconnect/private_keys/AuthKey_${APP_STORE_CONNECT_API_KEY_ID}.p8
- name: Upload to TestFlight
env:
APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
APP_STORE_CONNECT_API_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_ISSUER_ID }}
run: |
xcrun altool --upload-app \
--type ios \
--file "$RUNNER_TEMP/export/YourApp.ipa" \
--apiKey "$APP_STORE_CONNECT_API_KEY_ID" \
--apiIssuer "$APP_STORE_CONNECT_API_ISSUER_ID"
- name: Clean up keychain and profiles
if: always()
run: |
security delete-keychain $RUNNER_TEMP/app-signing.keychain-db || true
rm -f ~/Library/MobileDevice/Provisioning\ Profiles/build_pp.mobileprovision
rm -rf ~/.appstoreconnect/private_keys
Pontos importantes
Usar github.run_number como número de build
Em vez do increment_build_number do fastlane, aproveitamos o número de execução atribuído automaticamente pelo GitHub Actions. É garantidamente crescente e atende ao requisito do TestFlight de "sem números de build duplicados". Um número baseado em data ($(date +%Y%m%d%H%M)) também funciona.
Formatar logs com xcbeautify
Os logs brutos do xcodebuild são muito difíceis de ler, por isso os redirecionamos pelo xcbeautify. Ele vem pré-instalado nos runners macos-14. Se não estiver disponível, adicione brew install xcbeautify como etapa.
Localização do arquivo de chave de API
O xcrun altool detecta automaticamente a chave de API em qualquer um dos seguintes caminhos. Não existe flag para especificar o caminho explicitamente, portanto o arquivo deve ser colocado em um dos diretórios designados:
./private_keys/AuthKey_<KEY_ID>.p8~/private_keys/AuthKey_<KEY_ID>.p8~/.private_keys/AuthKey_<KEY_ID>.p8~/.appstoreconnect/private_keys/AuthKey_<KEY_ID>.p8
Por que usar um keychain temporário
Importar certificados no keychain padrão do ambiente CI é difícil de limpar e levanta problemas de segurança. A boa prática é criar um keychain por job e deletá-lo ao final.
Limpeza com if: always()
O keychain temporário e os arquivos de chave são sempre deletados, mesmo que o build falhe. Isso é especialmente importante ao usar runners self-hosted.
Passo 6: Verificação
Faça push na branch main ou execute manualmente pela aba Actions com Run workflow.
Conclui em aproximadamente 5-10 minutos e um novo build aparece na aba TestFlight do App Store Connect. A transição do status "Processando" para concluído pode levar mais 10-30 minutos.
Opções de gerenciamento de certificados e configurações recomendadas
Construímos o pipeline usando o método de codificar .p12 em Base64 e registrá-lo nos GitHub Secrets, mas o GitHub Actions oferece outras opções. Escolha a mais adequada para o tamanho e as necessidades da sua equipe.
Opção 1: Armazenar como Base64 nos Secrets (método deste artigo)
Funcionamento: .p12 e .mobileprovision são codificados em Base64 e salvos como strings nos GitHub Secrets.
Vantagens
- Sem dependências externas; tudo funciona dentro do GitHub
- Configuração inicial mais simples
.p12geralmente tem alguns KB a dezenas de KB, bem abaixo do limite de 64 KB dos Secrets
Desvantagens
- Atualização manual dos Secrets na rotação de certificados
- Gerenciar vários apps multiplica os Secrets e fica trabalhoso
- Os Secrets não podem ser tratados como arquivos no estilo do Azure DevOps Secure Files
Ideal para: desenvolvedores solo, equipes pequenas de 1-3 pessoas, app único
Opção 2: Commitar arquivos criptografados no repositório
Funcionamento: .p12 é criptografado com GPG ou OpenSSL e commitado no repositório; apenas a frase secreta de descriptografia é armazenada nos Secrets.
# Criptografar localmente
gpg --symmetric --cipher-algo AES256 Certificates.p12
# → Commitar Certificates.p12.gpg no repositório
No workflow:
- name: Decrypt certificate
run: |
gpg --quiet --batch --yes --decrypt \
--passphrase="$CERT_PASSPHRASE" \
--output $RUNNER_TEMP/cert.p12 \
secrets/Certificates.p12.gpg
env:
CERT_PASSPHRASE: ${{ secrets.CERT_PASSPHRASE }}
Vantagens
- Gerenciável como arquivo; histórico de rotações rastreável via Git
- Sem limite de tamanho dos Secrets
- O mais próximo do Azure DevOps Secure Files
Desvantagens
- Se o repositório for comprometido, a força da frase secreta é a última linha de defesa
- Arquivos
.gpgno repositório podem incomodar visualmente alguns
Ideal para: migração do Azure DevOps, gerenciamento como arquivos desejado, repositórios privados
Opção 3: fastlane match
Funcionamento: Certificados criptografados são armazenados em um repositório privado dedicado (ou S3/GCS), e o fastlane cuida automaticamente de buscá-los, instalá-los e descriptografá-los.
Vantagens
- Gerenciamento centralizado de certificados para múltiplos apps e ambientes (dev/adhoc/appstore)
- Novos membros da equipe configuram o ambiente local com um único
fastlane match - Suporte à regeneração automática de certificados
Desvantagens
- Requer Ruby e fastlane
- Possíveis problemas de compatibilidade do fastlane com atualizações do Xcode
- Vai contra a abordagem sem fastlane deste artigo
Ideal para: gerenciamento de múltiplos apps, equipes iOS com 5+ pessoas, rotação frequente de certificados
Opção 4: AWS Parameter Store + Integração OIDC
Funcionamento: As strings Base64 do .p12 e as senhas são armazenadas como SecureString no AWS Systems Manager Parameter Store e recuperadas do GitHub Actions via OIDC. Não são necessárias credenciais de longa duração — uma grande vantagem.
Parameter Store vs. Secrets Manager
A AWS tem dois serviços similares; aqui está uma comparação:
| Item | Parameter Store | Secrets Manager |
|---|---|---|
| Custo (Standard) | Gratuito | $0,40/secret/mês + tarifas de API |
| Custo (Advanced) | $0,05/10.000 chamadas de API | Igual |
| Limite de tamanho | Standard 4 KB / Advanced 8 KB | 64 KB |
| Rotação automática | Não | Sim |
| Criptografia KMS | Via tipo SecureString | Nativo |
Arquivos .p12 codificados em Base64 geralmente têm dezenas de KB a 30 KB, o que é o ponto de decisão. Se couber em 8 KB, o Parameter Store é significativamente mais barato; caso contrário, o Secrets Manager ou a abordagem híbrida com S3 descrita abaixo é mais adequada.
Lidando com arquivos acima de 8 KB
Se o tamanho for muito grande, uma configuração híbrida que armazene o .p12 criptografado no S3 e guarde apenas a chave do S3 e a senha de descriptografia no Parameter Store é praticamente limpa. Combina a simplicidade do Parameter Store com a liberdade de tamanho do S3.
Configuração no AWS
Primeiro, crie um provedor de identidade OIDC para o GitHub Actions no IAM (ignore se já criado):
- Provider URL:
https://token.actions.githubusercontent.com - Audience:
sts.amazonaws.com
Em seguida, armazene os parâmetros no Parameter Store (exemplo com AWS CLI):
# Corpo do certificado
aws ssm put-parameter \
--name /ios/yourapp/dist-cert-base64 \
--type SecureString \
--value "$(base64 -i Certificates.p12)"
# Senha do .p12
aws ssm put-parameter \
--name /ios/yourapp/p12-password \
--type SecureString \
--value "your-p12-password"
# Perfil de provisionamento
aws ssm put-parameter \
--name /ios/yourapp/provisioning-profile-base64 \
--type SecureString \
--value "$(base64 -i YourApp_AppStore.mobileprovision)"
# Chave de API do App Store Connect (.p8)
aws ssm put-parameter \
--name /ios/yourapp/asc-api-key-base64 \
--type SecureString \
--value "$(base64 -i AuthKey_XXXXXXXXXX.p8)"
Crie uma role IAM para o GitHub Actions e restrinja o Assume Role a repositórios e branches específicos na Trust Policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
},
"StringLike": {
"token.actions.githubusercontent.com:sub": "repo:your-org/your-repo:ref:refs/heads/main"
}
}
}
]
}
Política de permissões mínimas — apenas GetParameter para o caminho alvo:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["ssm:GetParameter", "ssm:GetParameters"],
"Resource": "arn:aws:ssm:ap-northeast-1:123456789012:parameter/ios/yourapp/*"
},
{
"Effect": "Allow",
"Action": "kms:Decrypt",
"Resource": "arn:aws:kms:ap-northeast-1:123456789012:key/your-key-id"
}
]
}
Workflow do GitHub Actions
Não esqueça de configurar id-token: write no bloco permissions. Sem isso, o token OIDC não pode ser obtido.
permissions:
id-token: write # Necessário para obtenção do token OIDC
contents: read
jobs:
deploy:
runs-on: macos-14
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/github-actions-ios-deploy
aws-region: ap-northeast-1
- name: Fetch secrets from Parameter Store
env:
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: |
CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
PP_PATH=$RUNNER_TEMP/build_pp.mobileprovision
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
# Buscar do Parameter Store (SecureString requer --with-decryption)
aws ssm get-parameter \
--name /ios/yourapp/dist-cert-base64 \
--with-decryption \
--query Parameter.Value --output text | base64 --decode > $CERTIFICATE_PATH
aws ssm get-parameter \
--name /ios/yourapp/provisioning-profile-base64 \
--with-decryption \
--query Parameter.Value --output text | base64 --decode > $PP_PATH
P12_PASSWORD=$(aws ssm get-parameter \
--name /ios/yourapp/p12-password \
--with-decryption \
--query Parameter.Value --output text)
# Criar keychain e importar certificado
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security import $CERTIFICATE_PATH -P "$P12_PASSWORD" \
-A -t cert -f pkcs12 -k $KEYCHAIN_PATH
security set-key-partition-list -S apple-tool:,apple: \
-k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security list-keychain -d user -s $KEYCHAIN_PATH
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles
# As etapas de archive / export / altool são iguais ao Passo 5
Vantagens
- Custo quase zero: parâmetros standard são completamente gratuitos; Advanced é extremamente barato
- Sem credenciais de longa duração com OIDC: não é necessário armazenar chaves de acesso no GitHub (maior vantagem)
- Logs de auditoria com CloudTrail: registro completo de quem acessou o certificado e quando
- Controle granular de permissões com IAM/KMS: roles separadas para ambientes dev e prod
- Compartilhável entre múltiplos sistemas CI/CD: o mesmo certificado pode ser usado do GitHub Actions, CodeBuild, Jenkins e desenvolvimento local
- Histórico de versões: possibilidade de reverter para versões anteriores
Desvantagens
- Conta AWS necessária (alto custo inicial para equipes que ainda não a usam)
- Workaround necessário se
.p12ultrapassar 8 KB (usar S3 em paralelo ou Secrets Manager) - Curva de aprendizado para configuração OIDC (apenas uma vez)
Ideal para: equipes que já usam AWS, requisitos de logs de auditoria, referenciar o mesmo certificado de múltiplas ferramentas CI/CD, gestão de qualidade enterprise a baixo custo
Opção 5: AWS Secrets Manager + Integração OIDC
Funcionamento: .p12 é armazenado diretamente como binário (SecretBinary) no AWS Secrets Manager e recuperado do GitHub Actions via OIDC. Ao contrário do Parameter Store, a característica principal é poder manipulá-lo como arquivo sem se preocupar com conversão Base64.
Diferenças em relação ao Parameter Store
| Item | Parameter Store | Secrets Manager |
|---|---|---|
| Formato de armazenamento | Apenas strings (binário requer Base64) | Binário direto OK (tipo SecretBinary) |
| Limite de tamanho | 4 KB (Standard) / 8 KB (Advanced) | 64 KB |
| Custo | Gratuito a muito barato | $0,40/mês/secret + tarifas de API |
| Rotação automática | Não | Sim (integração Lambda) |
| Direção de uso | Valores de configuração, secrets pequenos | Secrets maiores, operações em produção |
Se .p12 ultrapassar 8 KB, ou se você prioriza a facilidade de manuseio como arquivo, o Secrets Manager é mais direto. Para 1-2 certificados, o custo mensal fica abaixo de um dólar.
Configuração no AWS
A configuração do provedor de identidade OIDC é compartilhada com a Opção 4, então a omitimos aqui.
Registre no Secrets Manager como binário usando o prefixo fileb://, que indica ao AWS CLI para enviar o arquivo como está em binário:
# Corpo do certificado (pode ser registrado diretamente como binário)
aws secretsmanager create-secret \
--name ios/yourapp/dist-cert \
--secret-binary fileb://Certificates.p12
# Perfil de provisionamento como binário
aws secretsmanager create-secret \
--name ios/yourapp/provisioning-profile \
--secret-binary fileb://YourApp_AppStore.mobileprovision
# Chave de API do App Store Connect (.p8) como binário
aws secretsmanager create-secret \
--name ios/yourapp/asc-api-key \
--secret-binary fileb://AuthKey_XXXXXXXXXX.p8
# Senhas como string
aws secretsmanager create-secret \
--name ios/yourapp/p12-password \
--secret-string "your-p12-password"
Para atualizações, use update-secret:
aws secretsmanager update-secret \
--secret-id ios/yourapp/dist-cert \
--secret-binary fileb://Certificates_new.p12
A política de permissões da role IAM permite o Secrets Manager em vez do Parameter Store:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["secretsmanager:GetSecretValue"],
"Resource": "arn:aws:secretsmanager:ap-northeast-1:123456789012:secret:ios/yourapp/*"
},
{
"Effect": "Allow",
"Action": "kms:Decrypt",
"Resource": "arn:aws:kms:ap-northeast-1:123456789012:key/your-key-id"
}
]
}
Workflow do GitHub Actions
Os valores recuperados via SecretBinary são retornados codificados em Base64 na resposta da API, então decodifique com base64 --decode e escreva em um arquivo:
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: macos-14
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/github-actions-ios-deploy
aws-region: ap-northeast-1
- name: Fetch certificates from Secrets Manager
env:
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: |
CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
PP_PATH=$RUNNER_TEMP/build_pp.mobileprovision
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
# SecretBinary é retornado codificado em Base64; decodificar e escrever em arquivo
aws secretsmanager get-secret-value \
--secret-id ios/yourapp/dist-cert \
--query SecretBinary --output text | base64 --decode > $CERTIFICATE_PATH
aws secretsmanager get-secret-value \
--secret-id ios/yourapp/provisioning-profile \
--query SecretBinary --output text | base64 --decode > $PP_PATH
# SecretString pode ser recuperado diretamente
P12_PASSWORD=$(aws secretsmanager get-secret-value \
--secret-id ios/yourapp/p12-password \
--query SecretString --output text)
# Colocar a chave de API (.p8) no caminho esperado pelo altool
API_KEY_ID="${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}"
mkdir -p ~/.appstoreconnect/private_keys
aws secretsmanager get-secret-value \
--secret-id ios/yourapp/asc-api-key \
--query SecretBinary --output text | base64 --decode \
> ~/.appstoreconnect/private_keys/AuthKey_${API_KEY_ID}.p8
# Criar keychain e importar certificado
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security import $CERTIFICATE_PATH -P "$P12_PASSWORD" \
-A -t cert -f pkcs12 -k $KEYCHAIN_PATH
security set-key-partition-list -S apple-tool:,apple: \
-k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security list-keychain -d user -s $KEYCHAIN_PATH
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles
# As etapas de archive / export / altool são iguais ao Passo 5
Vantagens
.p12pode ser registrado e recuperado diretamente como arquivo (o mais próximo do Azure DevOps Secure Files)- Suporte a até 64 KB: sem preocupação com limites de tamanho
- Sem credenciais de longa duração com OIDC: mesmos benefícios de segurança da Opção 4
- Logs de auditoria com CloudTrail disponíveis nativamente
- Rotação automática: possível via Lambda (mas a integração com a API da Apple tem alto custo de construção)
- Controle granular de permissões com IAM
Desvantagens
- Gera custo: ~$0,40/secret/mês + tarifas de API; alguns dólares por mês com 3-5 certificados
- Conta AWS necessária
- Curva de aprendizado para configuração OIDC (compartilhada com Opção 4)
Ideal para: prioridade ao manuseio como arquivo, arquivos .p12 grandes, muitos arquivos de certificado em múltiplos apps, rotação automática considerada no futuro, experiência equivalente ao Azure DevOps Secure Files desejada
Opção 6: Outros gerenciadores de segredos em nuvem
Para equipes que não usam AWS, as seguintes opções funcionam com o mesmo conceito. Todas suportam integração OIDC:
| Serviço | Custo | Limite de tamanho | Suporte a binário |
|---|---|---|---|
| Azure Key Vault | $0,03/10.000 ops | 25 KB | ○ (tipo Certificate) |
| Google Secret Manager | $0,06/mês/secret | 64 KB | ○ |
| HashiCorp Vault (OSS) | Apenas custos de servidor | Sem limite | ○ |
O Azure Key Vault em particular tem um tipo Certificate com suporte oficial para importação/exportação direta no formato .p12 — ideal para equipes que usam o ecossistema Azure.
Opção 7: Proteção adicional com GitHub Environments
Esta opção pode ser combinada com qualquer uma das anteriores. Configurando GitHub Environments, você pode restringir o acesso a certificados apenas a deploys de branches específicas ou adicionar uma etapa de aprovação manual:
jobs:
deploy:
runs-on: macos-14
environment: production # ← Os Secrets vinculados a este Environment são utilizáveis
Ao isolar os Secrets por Environment, evita-se o uso acidental de certificados de distribuição de produção para builds de desenvolvimento.
Configurações recomendadas por escala
| Escala | Configuração recomendada |
|---|---|
| Desenvolvedor solo / projeto hobby | Opção 1 (Base64 + Secrets) — a simplicidade vence |
| Startup / equipe pequena (1-3 pessoas) | Opção 1 ou Opção 2 (arquivos criptografados) |
| Equipe média (vários apps ou 5+ pessoas) | Opção 3 (fastlane match) ou Opção 5 (Secrets Manager) |
| Usuários AWS, conscientes do custo | Opção 4 (Parameter Store + OIDC) |
| Usuários AWS, orientados a operações | Opção 5 (Secrets Manager + OIDC) — amigável com arquivos |
| Enterprise / requisitos de auditoria | Opção 4 ou 5 + Opção 7 (Environments) combinadas |
| Migração do Azure DevOps | Opção 2 (arquivos criptografados) ou Opção 5 (Secrets Manager) — o mais próximo de Secure Files |
Problemas comuns e soluções
No signing certificate "iOS Distribution" found
Ocorre frequentemente quando a importação do certificado no keychain falha ou quando set-key-partition-list está ausente. Adicione security find-identity -v -p codesigning $KEYCHAIN_PATH como etapa para verificar se o certificado está visível — isso facilita o isolamento do problema.
error: exportArchive: "YourApp.app" requires a provisioning profile
O perfil de provisionamento não foi colocado corretamente, ou o Bundle ID e o nome do perfil em provisioningProfiles no ExportOptions.plist não coincidem. O nome do perfil é o "nome exibido no site Apple Developer", não o nome do arquivo.
altool: Invalid API key
Geralmente causado por quebras de linha ou espaços extras misturados no arquivo .p8 codificado em Base64. Gere com base64 -i file.p8 | pbcopy e cole diretamente. Se o nome do arquivo não seguir a convenção AuthKey_<KEY_ID>.p8, o altool não o reconhecerá — construa-o com precisão usando variáveis de ambiente.
xcodebuild: error: The operation couldn't be completed. No such file or directory
Ocorre quando se usa .xcworkspace mas -project é especificado (ou vice-versa). Se você usa um workspace de CocoaPods ou Swift Package Manager, especifique -workspace.
Parameter Store: ParameterNotFound ou AccessDenied
Erros durante a integração AWS. Verifique nesta ordem:
- Há erro de digitação no caminho do parâmetro (hierarquia como
/ios/yourapp/...)? ssm:GetParameterestá permitido para o caminho alvo na política de permissões da role IAM?- Para SecureString: a flag
--with-decryptionestá incluída? - A role IAM tem permissão
kms:Decryptpara a chave KMS? - A condição
subna Trust Policy corresponde à branch de execução do workflow?
Melhorias adicionais
Construímos o pipeline com uma configuração mínima. Aqui estão algumas extensões possíveis:
- Notificações Slack: Integrar
slackapi/slack-github-actionpara notificar automaticamente quando o upload for concluído. - Release notes automáticas: Extrair mudanças do
git loge inseri-las automaticamente no campo "What to Test" do TestFlight (difícil apenas com altool; requer chamar a App Store Connect API diretamente). - Trigger por push de tag: Disparar com um push de tag
v*em vez de push nomainpara um fluxo de release mais intencional. - Executar testes unitários primeiro: Executar
xcodebuild testantes do archive e interromper o deploy se os testes falharem.
Conclusão
Uma configuração sem fastlane tem as vantagens de ser intuitiva e fácil de entender, não requerer ambiente Ruby e não ser afetada por problemas de compatibilidade do fastlane com atualizações do Xcode. Em contrapartida, coisas como o gerenciamento do número de build precisam ser feitas manualmente, e ao escalar para múltiplos apps, a carga de gerenciamento aumenta. Nesse ponto, migrar para fastlane match ou AWS Secrets Manager / Parameter Store é uma escolha pragmática.
O maior obstáculo na configuração inicial é sem dúvida o gerenciamento de certificados — esperamos que este artigo e a seção de "Opções de gerenciamento de certificados" ajudem a superar esse obstáculo.
