iOSGitHub ActionsTestFlightXcodeCI/CDCode Signing

Construir, assinar e fazer upload automático de apps iOS para o TestFlight com GitHub Actions (sem fastlane)

Sloth255
Sloth255
·14 min read·3,012 words

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:

  1. O workflow é acionado por push no main ou manualmente
  2. O Xcode é preparado no runner macOS do GitHub Actions
  3. O certificado e o perfil de provisionamento são importados temporariamente no keychain
  4. O número de build é atualizado
  5. O .xcarchive é gerado com xcodebuild archive
  6. O .ipa é exportado com xcodebuild -exportArchive
  7. O upload para o TestFlight é feito com xcrun altool
  8. 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.

  1. Faça login no App Store Connect
  2. Selecione "Usuários e acesso" → "Integrations" → "App Store Connect API"
  3. Gere uma chave com o botão "+" (função App Manager ou superior necessária)
  4. Salve o arquivo AuthKey_XXXXXXXXXX.p8 baixado (só pode ser baixado uma vez — guarde com cuidado)
  5. 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 SettingsSecrets and variablesActions 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).

ios/ExportOptions.plist
<?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.

.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
  • .p12 geralmente 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 .gpg no 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:

trust-policy.json
{
  "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:

permission-policy.json
{
  "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.

.github/workflows/ios-testflight.yml
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 .p12 ultrapassar 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:

permission-policy.json
{
  "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:

.github/workflows/ios-testflight.yml
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

  • .p12 pode 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:

  1. Há erro de digitação no caminho do parâmetro (hierarquia como /ios/yourapp/...)?
  2. ssm:GetParameter está permitido para o caminho alvo na política de permissões da role IAM?
  3. Para SecureString: a flag --with-decryption está incluída?
  4. A role IAM tem permissão kms:Decrypt para a chave KMS?
  5. A condição sub na 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-action para notificar automaticamente quando o upload for concluído.
  • Release notes automáticas: Extrair mudanças do git log e 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 no main para um fluxo de release mais intencional.
  • Executar testes unitários primeiro: Executar xcodebuild test antes 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.