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.


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.
- Acesse o Google Cloud Console.
- Crie um novo projeto ou selecione o existente vinculado ao seu app.
- Vá em APIs & Services > Credentials.
- Crie uma Service Account.
- 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.

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.

