AppgeousGuias práticos sobre aplicativos e inovação
Desenvolvimento Mobile

Automatize a Play Store com GitHub Actions e Keystore Segura

Elimine o processo manual de upload e transforme cada push no branch principal em uma versão pública na faixa de lançamento interno da Play Store.

Bruno Carvalho Ramos
Bruno Carvalho RamosEditor Chefe de Desenvolvimento Mobile6 min de leitura
Imagem editorial ilustrando Automatize a Play Store com GitHub Actions e Keystore Segura

Sexta-feira às 17h55. Você queria apenas fechar a semana, mas surgiu um bug crítico no login. Corrige, roda o gradlew assembleRelease, vai na pasta app/build/outputs/apk/release, abre o navegador, faz login na Play Console, sobe o APK, preenche o changelog... cansativo só de ler. Agora imagine que seu time faz isso três vezes por dia. É o caminho mais curto para o burnout e, inevitavelmente, para o erro humano — seja esquecer de atualizar o version code ou subir o build de debug com assinatura de release (sim, isso acontece).

A automação desse pipeline não é "coisa para amanhã", é um requisito de higiene em 2026. O foco aqui não é apenas fazer o build, mas garantir que a assinatura esteja correta e que o binário caia na track certa da loja sem toque humano. Vamos botar a mão na massa.

A anatomia da segurança: Keystore e Secretos

O maior calo no processo de automação Android é a Keystore. Se você comita o arquivo .jks no repositório, pode começar a atualizar o LinkedIn porque sua chave privada está exposta. A solução padrão da indústria é transformar esse arquivo binário em uma string codificada em Base64 e guardá-la nos secrets do repositório.

Primeiro, no seu terminal local, converta a Keystore:

base64 -i app-release-keystore.jks | pbcopy

Isso copia um bloco de texto gigante para a área de transferência. Acesse seu repositório no GitHub, vá em Settings > Secrets and variables > Actions e crie uma nova secret chamada KEYSTORE_FILE. Cole o texto ali. Repita o processo para a senha da keystore (KEYSTORE_PASSWORD) e para o alias (KEY_ALIAS_PASSWORD).

Um detalhe técnico que很多人 ignoram: a Play Store para apps novos exige o formato Android App Bundle (.aab), não o APK clássico. A configuração da Keystore é idêntica, mas o comando de build muda ligeiramente no final. Mantenha esses nomes de secret padronizados no seu build.gradle (ou build.gradle.kts) para que o script de assinatura saiba onde buscar as informações durante o CI.

O arquivo de serviço da Google Cloud

Para que o GitHub Actions possa "falar" com a Play Console e subir o arquivo, ele precisa de credenciais de API. Não use sua conta pessoal. Crie uma conta de serviço (Service Account) no Google Cloud Console.

  1. Acesse o Google Cloud Console.
  2. Crie um novo projeto ou selecione o existente vinculado ao seu app.
  3. Vá em APIs & Services > Credentials.
  4. Crie uma Service Account.
  5. Depois de criada, clique nela e vá em "Keys". Gere uma chave JSON. Baixe esse arquivo.

Agora, o passo que trava 80% dos devs: a permissão. Pegue o e-mail dessa conta de serviço (algo como [email protected]) e vá na Play Console. Em Configurações > Acesso à API > Usuários e permissões, convide esse e-mail. Dê a ele permissão de "Release Manager" ou "Admin".

Pegue o conteúdo desse arquivo JSON, converta para Base64 da mesma forma que fez com a Keystore e salve como secret GOOGLE_PLAY_SERVICE_ACCOUNT_JSON.

Detalhe fotográfico relacionado a Automatize a Play Store com GitHub Actions e Keystore Segura

Escrevendo o Workflow YAML

Agora vamos para o coração da automação. Crie o arquivo .github/workflows/deploy-play-store.yml. A ideia aqui é rodar em todo push na branch main, mas você pode adaptar para tags específicas se preferir mais controle.

Não use actions genéricas demais. Para 2026, a configuração abaixo utilizando Java 21 (padrão atual) e a action oficial da Google para setup de credenciais é a mais estável:

name: Deploy Play Store

on:
  push:
    branches:
      - main

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up JDK 21
        uses: actions/setup-java@v4
        with:
          java-version: '21'
          distribution: 'temurin'

      - name: Setup Gradle
        uses: gradle/actions/setup-gradle@v3

      - name: Decode Keystore
        run: |
          echo "${{ secrets.KEYSTORE_FILE }}" | base64 --decode > app/release-keystore.jks

      - name: Grant execute permission for gradlew
        run: chmod +x gradlew

      - name: Build Release Bundle
        run: ./gradlew bundleRelease --no-daemon

      - name: Decode Service Account JSON
        run: |
          echo "${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON }}" | base64 --decode > service-account.json

      - name: Deploy to Play Store (Internal Track)
        uses: r0adkll/upload-google-play@v1
        with:
          serviceAccountJsonPlainText: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON }}
          packageName: com.seuapp.exemplo
          releaseFiles: app/build/outputs/bundle/release/app-release.aab
          track: internal
          status: completed

Observe o bloco Decode Keystore. O GitHub não mantém arquivos binários entre as execuções de forma persistente na workspace da mesma forma que sua máquina local. Você precisa recriar o arquivo .jks a cada run usando o comando base64 --decode. O caminho app/release-keystore.jks deve bater exatamente com o que você definiu no seu arquivo de assinatura dentro do Gradle.

Na etapa final, a action r0adkll/upload-google-play continua sendo a opção mais robusta da comunidade, sendo mais simples que configurar o gcloud CLI manualmente dentro do YAML. Definimos a track como internal. Isso é crucial. Se você apontar direto para production, um commit com um bug trivial vai direto para os seus usuários finais. A faixa interna permite testar a distribuição via URL de lista de teste antes de qualquer promoção.

Ajustando a lógica de versionamento

O maior risco de um CI/CD agressivo é o travamento da versão. Se você subiu a versão 1.0.1 (versionCode 2) e rodar o pipeline novamente sem mudar o versionCode, a Play Store rejeitará o upload. O erro no log será algo como "APK specifies a version code that has already been used".

Você tem duas saídas honestas. A primeira é incrementar o versionCode manualmente no build.gradle antes de cada commit. A segunda, mais elegante, é automatizar isso via script. Uma abordagem comum em times que usam Diferença Prática entre 'View' e 'Jetpack Compose' no Manuseio de Estado da UI e arquiteturas modernas é ter um task do Gradle que lê a data atual ou um contador de commits.

Se quiser manter simples (recomendo), use o CalVer (Calendar Versioning) no versionCode, ex: 2026042701 (ANO MES DIA BUILD DO DIA). Isso evita conflitos. No app/build.gradle:

android {
    defaultConfig {
        def date = new Date()
        def formattedDate = date.format('yyyyMMddHH')
        versionCode formattedDate.toInteger()
        versionName "1.0.$formattedDate"
    }
}

Assim, você pode rodar o workflow quantas vezes quiser no mesmo dia que ele nunca vai bater exatamente na hora, a menos que faça dois builds no mesmo minuto (o que, admito, é possível se você for muito distraído).

Quando a automação quebra

Não se iluda: a primeira vez vai falhar. Geralmente é erro de permissão da conta de serviço. A mensagem "The caller does not have permission" é genérica e chata. Verifique se você esperou os 10 a 15 minutos necessários para a propagação da permissão da Service Account na Play Console após clicar em "Convidar". Outro ponto comum é o nome do pacote (packageName) errado no YAML. Confira character por character. Um espaço a mais no final da string no arquivo build.gradle já quebra o checksum do build.

Se seu projeto é pesado e o build está demorando mais de 15 minutos, considere usar caches no Gradle. A action gradle/actions/setup-gradle já faz isso nativamente, mas se você usa muitas dependências locais ou arquivos grandes, talvez valha a pena ajustar a estratégia de cache no actions/checkout.

Automatizar não é sobre preguiça, é sobre consistência. Se você migrou sua stack recentemente, como discutimos na migração da nossa codebase Flutter para Rust, sabe que a estabilidade do build é sagrada. Um pipeline confiável libera sua cabeça para pensar em features, e não em cliques de interface.

O próximo passo após o upload

Depois que o pipeline ficar verde e você vir o bundle aparecer na track "Internal" da Play Console, pare. Não promova para produção imediatamente.

A melhor prática de 2026 para times ágeis é usar essa automação interna como um gate de qualidade. O CI subiu? Ótimo. Agora o time de QA (ou você mesmo) pega o link de opt-in da faixa interna e testa em um device físico real. Se o app for de Realidade Aumentada ou algo que exige performance bruta, como questionamos se o React Native é maduro o suficiente para AR, esse teste manual na faixa interna é inegociável.

O processo manual morreu, mas a responsabilidade continuou. A automação deixa o processo chato de "empurrar arquivo" invisível. Você ganha tempo para focar onde realmente importa: no código que roda no celular do usuário.

Leia em seguida