Introdução
Você ainda publica seu app Android clicando manualmente em Generate Signed Bundle no Android Studio, e depois abrindo o Play Console para arrastar e soltar o AAB?
Eu fazia isso por um tempo, mas à medida que a frequência de publicações aumentava, encontrei os seguintes problemas:
- Builds quebram por diferenças no ambiente local (incompatibilidades de versão do JDK, problemas com cache do Gradle)
- O gerenciamento do Keystore vira dependência de uma pessoa (existe apenas no Mac de um desenvolvedor específico)
- O processo de publicação depende de documentação, levando a erros humanos
Para resolver todos esses problemas de uma vez, automatizei todo o fluxo — build, assinatura e upload para o Google Play Console — com GitHub Actions. Aqui está o procedimento detalhado.
Visão geral
O pipeline que vamos construir fica assim:
flowchart TD
A["Push da git tag v1.0.0"]
B["GitHub Actions \u00e9 acionado"]
C["Configurar JDK e restaurar cache do Gradle"]
D["Restaurar Keystore dos Secrets"]
E["Compilar e assinar AAB com ./gradlew bundleRelease"]
F["Distribuir para testes internos via r0adkll/upload-google-play"]
G["Google recebe, re-assina com a chave de assinatura do app e distribui aos usu\u00e1rios"]
A --> B --> C --> D --> E --> F --> GOs termos "AAB" e "chave de upload" são explicados na seção seguinte.
3 conceitos-chave para entender primeiro
Antes de entrar nas etapas concretas, vamos esclarecer os termos-chave usados neste artigo. Se esses conceitos não estiverem claros, a parte do Play App Signing pode rapidamente ficar confusa.
APK vs AAB
| APK (Android Package Kit) | AAB (Android App Bundle) | |
|---|---|---|
| Conteúdo | Pacote universal combinando todos os ABIs, densidades de tela e recursos de idioma em um único arquivo | Artefato de build antes da otimização para dispositivos específicos |
| Tamanho do arquivo | Tende a ser grande pois inclui tudo | O Google Play gera Split APKs para distribuição, reduzindo o tamanho de download do usuário em ~15% em média |
| Uso principal | Distribuição direta / distribuição interna / lojas de terceiros | Distribuição no Google Play Store (obrigatório para novos apps desde agosto de 2021) |
| Task Gradle | ./gradlew assembleRelease |
./gradlew bundleRelease |
Em resumo: "APK = produto final que é instalado no dispositivo do usuário" e "AAB = blueprint enviado ao Google Play". O Google Play recebe o AAB e monta o APK adequado para o dispositivo de cada usuário.
Como este artigo visa a distribuição no Play Store, usaremos AAB (bundleRelease). Para distribuição interna ou outros casos de uso que precisam de APK, use assembleRelease.
Chave de upload vs chave de assinatura do aplicativo
O Play App Signing usa dois tipos de chaves de assinatura:
| Tipo de chave | Quem gerencia? | Finalidade |
|---|---|---|
| Chave de upload (Upload Key) | Desenvolvedor | Assina o AAB ao fazer upload para o Play Console |
| Chave de assinatura do aplicativo (App Signing Key) | Assina o APK final distribuído aos usuários pelo Play Store |
Em outras palavras, o Play Console verifica se o AAB está assinado com a chave de upload correta, por isso os desenvolvedores precisam assinar com sua própria chave de upload = Keystore. O que o Google faz por você é apenas a re-assinatura com a chave de assinatura do aplicativo no momento da distribuição.
flowchart TD
Dev["Desenvolvedor"] -->|"Assina com chave de upload"| AAB["AAB"]
AAB --> Play["Play Console"]
Play --> Verify["Google verifica a chave de upload"]
Verify --> Resign["Google re-assina com chave de assinatura do app"]
Resign --> User["Usu\u00e1rio"]Mesmo com o Play App Signing, o fato de que "você ainda precisa criar seu próprio Keystore" não mudou. O que mudou é a rede de segurança: "você pode solicitar uma redefinição se perder sua chave de upload".
Assinatura de debug vs assinatura release
Quando você pressionou Run pela primeira vez no Android Studio sem nenhuma configuração, um APK foi construído e executado no seu dispositivo porque o AGP (Android Gradle Plugin) gera automaticamente ~/.android/debug.keystore e assina automaticamente os builds de debug.
| Assinatura de debug | Assinatura release | |
|---|---|---|
| Keystore | ~/.android/debug.keystore (gerado automaticamente pelo AGP) |
Criado e gerenciado por você (release.keystore neste artigo) |
| Senha | Fixa: android |
Definida por você |
| Alias | Fixo: androiddebugkey |
Definido por você |
| Expiração | ~30 anos | Definida por você (~27 anos neste artigo) |
| Uso | Testes de desenvolvimento / ./gradlew assembleDebug |
Distribuição Play Store / ./gradlew bundleRelease |
| Gerenciamento Git | Não necessário (OK se existir por máquina) | Manter com segurança (pode ser redefinido via Play App Signing se perdido) |
Duas propriedades importantes:
- O Play Console rejeita categoricamente AABs assinados com assinatura de debug
→ A CI precisa de umrelease.keystoreseparado - A assinatura de debug e a assinatura release são completamente independentes
→ Adicionar configuração de assinatura release para a CI não tem nenhum efeito nas chamadas locais./gradlew assembleDebug
O signingConfigs.create("release") condicional nas seções 5 e 6 existe exatamente para aproveitar essas propriedades — criando um estado onde desenvolvedores sem a chave ainda podem executar builds de debug normalmente.
Pré-requisitos
- Aplicativo Android (usando
build.gradleoubuild.gradle.kts) - Repositório GitHub
- Conta de desenvolvedor no Google Play Console
- A primeira publicação do app já deve ter sido feita manualmente no Play Console (a criação inicial via API não é possível)
- O Play App Signing deve estar ativado para o app alvo
Para apps criados após agosto de 2021, o Play App Signing é ativado automaticamente, então nenhuma etapa especial é necessária. Para apps mais antigos criados antes disso com o método legado, você precisa primeiro migrar para o Play App Signing (abordado na seção 7-4).
1. Criar a chave de upload (Keystore)
Gere o Keystore localmente para assinar o AAB. Em uma configuração de Play App Signing, essa chave funciona como a "chave de upload" (= sua identidade ao enviar o AAB para o Play Console). A entrega final aos usuários é assinada pela "chave de assinatura do aplicativo" do Google, então mesmo se você perder essa chave, pode solicitar uma redefinição pelo Play Console (detalhes em 7-3).
Dito isso, perdê-la nas operações diárias bloqueará publicações até que a redefinição seja concluída, então guarde-a com segurança.
# macOS / Linux
keytool -genkey -v \
-keystore release.keystore \
-alias my-release-key \
-keyalg RSA \
-keysize 2048 \
-validity 10000
No Windows (PowerShell / Prompt de Comando), a continuação de linha com backslash não funciona, então execute em uma única linha ou use o backtick ` para quebras de linha no PowerShell.
keytool -genkey -v `
-keystore release.keystore `
-alias my-release-key `
-keyalg RSA `
-keysize 2048 `
-validity 10000
Insira a senha, nome, organização etc. interativamente.
2. Codificar o Keystore em Base64
O GitHub Secrets não pode armazenar binário diretamente, então converta-o em uma string Base64. O workflow remove quebras de linha com tr -d '\n\r' durante a decodificação, mas uma saída sem quebras de linha é mais segura.
macOS
base64 -i release.keystore -o release.keystore.base64
cat release.keystore.base64 | pbcopy
Linux
base64 -w 0 release.keystore > release.keystore.base64
cat release.keystore.base64 | xclip -selection clipboard
Windows (PowerShell)
[Convert]::ToBase64String([IO.File]::ReadAllBytes("release.keystore")) `
| Set-Clipboard
Para gravar em arquivo:
[Convert]::ToBase64String([IO.File]::ReadAllBytes("release.keystore")) `
| Out-File -Encoding ascii -NoNewline release.keystore.base64
Windows (Prompt de Comando)
certutil funciona mas adiciona cabeçalhos, rodapés e quebras de linha que precisam ser removidos.
certutil -encode release.keystore release.keystore.base64
Remova as linhas -----BEGIN CERTIFICATE----- e -----END CERTIFICATE----- e todas as quebras de linha do arquivo de saída. Usar o PowerShell é recomendado.
Copie a string para a área de transferência e cole no GitHub Secrets na próxima etapa.
3. Criar uma conta de serviço para o Google Play Console
Uploads automatizados para o Play Console requerem uma conta de serviço do Google Cloud.
3-1. Habilitar a API Google Play Android Developer
Primeiro, habilite a API no lado do GCP. Pular esta etapa resultará em um erro 403 mais tarde, então faça primeiro.
- Acesse Google Play Android Developer API
- Clique em Habilitar
3-2. Criar uma conta de serviço no Google Cloud Console
- Acesse o Google Cloud Console
- Crie um projeto (ou selecione um existente)
- Em IAM e administração → Contas de serviço, crie uma nova. Não atribua funções IAM (as permissões são gerenciadas no lado do Play Console)
- Após criar, selecione Adicionar chave → Criar nova chave → JSON e faça download do arquivo JSON
3-3. Conceder permissões no Play Console
- Abra o Play Console
- Abra Usuários e permissões, clique em Convidar novos usuários
- Insira o endereço de e-mail da conta de serviço criada na etapa 3-2
- Na aba Permissões do aplicativo, adicione o app alvo e conceda as seguintes permissões:
- Publicar em faixas de teste — necessário para distribuição na faixa de testes internos
- Visualizar informações do app e baixar relatórios em massa — permissão de leitura
4. Registrar valores nos GitHub Secrets
Em Settings → Secrets and variables → Actions do repositório, registre o seguinte:
| Nome do Secret | Valor |
|---|---|
KEYSTORE_BASE64 |
String Base64 da etapa 2 |
KEYSTORE_PASSWORD |
Senha do Keystore |
KEY_ALIAS |
Ex.: my-release-key |
KEY_PASSWORD |
Senha da chave |
SERVICE_ACCOUNT_JSON |
Cole diretamente o conteúdo do arquivo JSON baixado na etapa 3-2 |
5. Configurar build.gradle para ler informações de assinatura de variáveis de ambiente
Configure para que a CI leia de variáveis de ambiente e o desenvolvimento local leia de keystore.properties, e mesmo ambientes sem Keystore possam executar builds de debug normalmente.
Edite app/build.gradle.kts da seguinte forma:
import java.util.Properties
import java.io.FileInputStream
// --- Resolver informações de assinatura ---
// Prioridade: variáveis de ambiente (CI) > keystore.properties (local) > nenhuma (apenas build debug)
val keystorePropertiesFile = rootProject.file("keystore.properties")
val keystoreProperties = Properties().apply {
if (keystorePropertiesFile.exists()) {
load(FileInputStream(keystorePropertiesFile))
}
}
fun resolveSigning(key: String, envKey: String): String? =
System.getenv(envKey) ?: keystoreProperties.getProperty(key)
val releaseStoreFile = resolveSigning("storeFile", "KEYSTORE_FILE")
val releaseStorePassword = resolveSigning("storePassword", "KEYSTORE_PASSWORD")
val releaseKeyAlias = resolveSigning("keyAlias", "KEY_ALIAS")
val releaseKeyPassword = resolveSigning("keyPassword", "KEY_PASSWORD")
val hasReleaseSigning = listOf(
releaseStoreFile, releaseStorePassword, releaseKeyAlias, releaseKeyPassword
).all { !it.isNullOrBlank() }
android {
signingConfigs {
if (hasReleaseSigning) {
create("release") {
storeFile = file(releaseStoreFile!!)
storePassword = releaseStorePassword
keyAlias = releaseKeyAlias
keyPassword = releaseKeyPassword
}
}
}
buildTypes {
getByName("debug") {
// Adicionar applicationIdSuffix para que builds release e debug
// possam coexistir no mesmo dispositivo
applicationIdSuffix = ".debug"
versionNameSuffix = "-debug"
// signingConfig usa o debug.keystore auto-gerado pelo AGP
}
getByName("release") {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
// Só atribuir release signingConfig se as informações de assinatura estiverem disponíveis
// (= desenvolvedores sem a chave ainda podem executar assembleDebug)
if (hasReleaseSigning) {
signingConfig = signingConfigs.getByName("release")
}
}
}
}
Pontos-chave deste código:
signingConfigs.create("release")está protegido, então a configuração release não é criada em ambientes sem informações de assinatura- O tipo de build
debugusa odebug.keystoreauto-gerado pelo AGP e não é afetado de forma alguma pelas configurações de assinatura release - Variáveis de ambiente têm a maior prioridade, então nenhum arquivo
keystore.propertiesé necessário na CI
6. Configuração adicional para não quebrar o desenvolvimento local
"Modifiquei o build.gradle para a CI e agora os builds de debug locais estão quebrados" é um acidente comum em configurações Android × CI. Para evitar isso, configure o seguinte:
6-1. Criar keystore.properties (somente local)
local.properties é destinado a caminhos do SDK, e misturar informações de assinatura lá torna as coisas confusas. Criar um arquivo keystore.properties separado para informações de assinatura é a abordagem recomendada na documentação oficial do Android.
Crie keystore.properties na raiz do projeto (um nível acima de app/):
storeFile=/Users/yourname/keys/release.keystore
storePassword=your-store-password
keyAlias=my-release-key
keyPassword=your-key-password
Este arquivo nunca deve ir ao Git, então adicione-o ao .gitignore (veja abaixo).
6-2. O que adicionar ao .gitignore
Entradas relacionadas à assinatura são frequentemente esquecidas pois o .gitignore gerado pelo Android Studio não as inclui. Adicione-as explicitamente:
# Assinatura - nunca commitar
*.keystore
*.jks
keystore.properties
release.keystore.base64
# JSON da conta de serviço
*-service-account*.json
play-publisher.json
# Arquivos de autenticação temporários gerados pelo google-github-actions/auth (usando WIF)
gha-creds-*.json
# Criado pelo Android Studio mas verificar mesmo assim
local.properties
local.properties geralmente é adicionado pelo Android Studio, mas verifique se está faltando em projetos mais antigos.
6-3. Coexistência de Release e Debug com applicationIdSuffix
O snippet build.gradle.kts acima inclui:
debug {
applicationIdSuffix = ".debug"
}
Isso permite instalar simultaneamente a versão release do Play Store e uma versão debug construída localmente no mesmo dispositivo. Sem isso, instalar um build de debug sobrescreveria a versão do Play Store no seu dispositivo de teste.
Não está diretamente relacionado à CI, mas uma vez que você começa a automatizar publicações, executará frequentemente a versão do Play Store no seu dispositivo de teste, então recomendo fortemente adicioná-lo.
6-4. Usar ~/.gradle/gradle.properties
Outra opção é escrever as informações de assinatura nas propriedades Gradle do diretório home:
# macOS/Linux: ~/.gradle/gradle.properties
# Windows: %USERPROFILE%\.gradle\gradle.properties
MYAPP_KEYSTORE_FILE=/Users/yourname/keys/release.keystore
MYAPP_KEYSTORE_PASSWORD=xxxx
MYAPP_KEY_ALIAS=my-release-key
MYAPP_KEY_PASSWORD=xxxx
Em build.gradle.kts, leia com findProperty("MYAPP_KEYSTORE_FILE") as String?.
A vantagem é que nenhuma informação secreta fica no diretório do projeto em absoluto. A desvantagem é que comunicar as etapas de configuração para novos desenvolvedores requer um pouco mais de esforço. Minha recomendação: ~/.gradle/gradle.properties para projetos solo, keystore.properties protegido por .gitignore para projetos em equipe.
6-5. Testar builds release sem assinar no Android Studio
Embora ./gradlew assembleDebug sempre funcione, você pode querer executar ./gradlew assembleRelease sem assinatura (por exemplo, para verificar as configurações do ProGuard para builds release).
Nesse caso, quando hasReleaseSigning é false, o tipo de build release não terá signingConfig, então executar assembleRelease produz um APK não assinado (não pode ser instalado em um dispositivo, mas o tamanho e mapping.txt podem ser verificados).
7. Notas operacionais sobre Play App Signing
Com as etapas 1–6 concluídas, a configuração do Play App Signing já está terminada. O release.keystore criado na etapa 1 funciona como chave de upload no contexto do Play App Signing.
Esta seção contém notas adicionais úteis na operação.
7-1. O que acontece no primeiro upload
Para um novo app, quando você faz upload do AAB para o Play Console pela primeira vez:
- O certificado usado nesse AAB é automaticamente registrado como "certificado de chave de upload"
- O Google gera automaticamente uma nova "chave de assinatura do aplicativo"
- O Play App Signing é ativado
Portanto, nenhuma configuração adicional do Play Console é necessária — seguir as etapas deste artigo como estão estabelecerá a configuração do Play App Signing.
flowchart TD
CI["CI"] -->|"Assina com release.keystore"| AAB["AAB"]
AAB --> Play["Play Console"]
Play --> Verify["Google verifica o certificado de chave de upload"]
Verify --> Resign["Google re-assina com chave de assinatura do app"]
Resign --> Device["Dispositivo do usu\u00e1rio"]7-2. Indo além: Criar uma chave de upload dedicada
Mesmo para novos apps, você pode escolher "registrar a chave de upload separadamente" no Play Console.
- Durante o primeiro upload, selecione "Usar chave de assinatura do app gerada pelo Google" no Play Console
- Na mesma tela, faça upload do certificado de chave de upload separadamente
Neste caso, release.keystore contém uma "chave completamente dedicada apenas ao upload".
| Padrão comum (7-1) | Abordagem de chaves separadas (7-2) | |
|---|---|---|
Conteúdo do release.keystore |
Chave de upload = chave que assinou o primeiro AAB | Chave de upload pura |
| Se comprometida | Recuperável com solicitação de redefinição | Recuperável com solicitação de redefinição |
| Nível de segurança | Suficiente | Mais rigoroso |
As etapas da CI são idênticas para ambos os padrões. A única diferença é como você cria release.keystore.
7-3. O que fazer se você perder a chave de upload
Esta é a maior vantagem do Play App Signing. Você pode solicitar uma redefinição pelo Play Console.
-
Crie um novo Keystore localmente
-
Exporte o certificado no formato PEM
keytool -export -rfc \ -keystore release.keystore \ -alias my-release-key \ -file upload_certificate.pem -
No Play Console → Configuração → Integridade do app → Assinatura do app → Solicitar redefinição de chave de upload, faça upload do certificado
-
O suporte do Google verifica (normalmente 1-2 dias úteis)
-
Após aprovação, AABs assinados com a nova chave serão aceitos
Com o método legado, "perder sua chave significava que você nunca mais poderia atualizar o mesmo nome de pacote" — um beco sem saída permanente. Essa opção de recuperação por si só já vale a pena adotar o Play App Signing.
7-4. Migrar um app existente (legado)
Para apps criados antes de agosto de 2021 que ainda não migraram para o Play App Signing, você precisa entregar a chave de assinatura atual ao Google:
-
Play Console → app alvo → Configuração → Integridade do app → Assinatura do app
-
Baixe a ferramenta PEPK (Play Encrypt Private Key) fornecida pelo Google
-
Use o PEPK para exportar o Keystore atual de forma criptografada
java -jar pepk.jar \ --keystore=existing-release.keystore \ --alias=my-release-key \ --output=encrypted-key.zip \ --include-cert \ --rsa-aes-encryption \ --encryption-key-path=public-key.pem -
Faça upload do arquivo de saída (
encrypted-key.zip) para o Play Console -
Após a migração, crie e registre uma nova chave de upload se necessário (continuar usando a chave existente como chave de upload também é possível)
public-key.pem é a chave pública exibida na tela de migração no Play Console, salva como arquivo.
Após a migração, as etapas 1–6 deste artigo se aplicam diretamente.
8. Escrever o workflow do GitHub Actions
Agora o ponto principal. Crie .github/workflows/release.yml:
name: Android Release
on:
push:
tags:
- 'v*'
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
- name: Cache Gradle packages
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Grant execute permission for gradlew
run: chmod +x ./gradlew
- name: Decode Keystore
run: |
echo "${{ secrets.KEYSTORE_BASE64 }}" | tr -d '\n\r' | base64 --decode > ${{ github.workspace }}/release.keystore
- name: Build Release AAB
env:
KEYSTORE_FILE: ${{ github.workspace }}/release.keystore
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
run: ./gradlew bundleRelease --no-daemon
- name: Upload AAB as artifact
uses: actions/upload-artifact@v4
with:
name: release-aab
path: app/build/outputs/bundle/release/app-release.aab
- name: Deploy to Play Store (Internal Track)
uses: r0adkll/upload-google-play@v1
with:
serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
packageName: com.example.myapp
releaseFiles: app/build/outputs/bundle/release/app-release.aab
tracks: internal
status: completed
whatsNewDirectory: distribution/whatsnew
Pontos-chave
- O trigger de tag (
v*) evita deploy acidental a cada push para main - O cache do Gradle reduz o tempo de build (de ~6 minutos para ~2 minutos em execuções subsequentes)
- O upload de artefato serve como backup — se o upload para o Play Console falhar, o AAB pode ser baixado manualmente pela interface do Actions
tracks: internalpode ser mudado paraalpha,beta,productionetc.whatsNewDirectoryé o diretório para notas de versão por idioma, ex.distribution/whatsnew/whatsnew-pt-BR
8-2. Configuração mais segura: Usar Workload Identity Federation
Em vez de armazenar chaves JSON de longa duração no GitHub Secrets, o Workload Identity Federation (WIF) permite que o GitHub Actions obtenha credenciais temporárias para acessar o Play Console. Isso elimina o risco de vazamento de chave JSON e é recomendado para produção.
Pré-requisitos (lado GCP)
A configuração do WIF é feita uma única vez. Siga as etapas em google-github-actions/auth. Com essa abordagem, nenhuma chave JSON é gerada para a conta de serviço, então a etapa 4 na seção 3-2 (criar/baixar chave) e registrar SERVICE_ACCOUNT_JSON na seção 4 não são necessários.
Configure com os seguintes comandos gcloud. Substitua cada placeholder pelos seus valores reais:
| Placeholder | Descrição |
|---|---|
${PROJECT_ID} |
ID do projeto GCP |
${GITHUB_ORG} |
Nome da organização GitHub ou nome de usuário |
${REPO} |
Nome do repositório no formato org/repo |
${SERVICE_ACCOUNT} |
Nome da conta de serviço da etapa 3-2 (a parte antes de @ no e-mail) |
${WORKLOAD_IDENTITY_POOL_ID} |
ID completo do Pool obtido na etapa 2 |
# 1. Criar Workload Identity Pool
gcloud iam workload-identity-pools create "github" \
--project="${PROJECT_ID}" \
--location="global" \
--display-name="GitHub Actions Pool"
# 2. Obter o ID completo do Pool
gcloud iam workload-identity-pools describe "github" \
--project="${PROJECT_ID}" \
--location="global" \
--format="value(name)"
# Exemplo: projects/123456789/locations/global/workloadIdentityPools/github
# 3. Criar Workload Identity Provider
# --attribute-condition restringe a tokens apenas desta organização
gcloud iam workload-identity-pools providers create-oidc "my-repo" \
--project="${PROJECT_ID}" \
--location="global" \
--workload-identity-pool="github" \
--issuer-uri="https://token.actions.githubusercontent.com" \
--attribute-mapping="google.subject=assertion.sub,attribute.repository=assertion.repository,attribute.repository_owner=assertion.repository_owner" \
--attribute-condition="assertion.repository_owner == '${GITHUB_ORG}'"
# 4. Conceder roles/iam.workloadIdentityUser à conta de serviço
# principalSet's attribute.repository restringe a este repositório específico
# ${WORKLOAD_IDENTITY_POOL_ID} = ID completo obtido na etapa 2
gcloud iam service-accounts add-iam-policy-binding \
"${SERVICE_ACCOUNT}@${PROJECT_ID}.iam.gserviceaccount.com" \
--project="${PROJECT_ID}" \
--role="roles/iam.workloadIdentityUser" \
--member="principalSet://iam.googleapis.com/${WORKLOAD_IDENTITY_POOL_ID}/attribute.repository/${REPO}"
Após a configuração, anote o ID completo do Provider (projects/.../providers/...). Ele é usado no campo workload_identity_provider do YAML do GitHub Actions.
YAML do workflow (versão WIF)
Adicione a permissão id-token: write ao repositório e uma etapa google-github-actions/auth:
name: Android Release
on:
push:
tags:
- 'v*'
permissions:
contents: read
id-token: write # Necessário para aquisição do token WIF
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
- name: Cache Gradle packages
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Grant execute permission for gradlew
run: chmod +x ./gradlew
- name: Decode Keystore
run: |
echo "${{ secrets.KEYSTORE_BASE64 }}" | tr -d '\n\r' | base64 --decode > ${{ github.workspace }}/release.keystore
- name: Build Release AAB
env:
KEYSTORE_FILE: ${{ github.workspace }}/release.keystore
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
run: ./gradlew bundleRelease --no-daemon
- name: Upload AAB as artifact
uses: actions/upload-artifact@v4
with:
name: release-aab
path: app/build/outputs/bundle/release/app-release.aab
- name: Authenticate to Google Cloud (WIF)
uses: google-github-actions/auth@v3
id: auth
with:
workload_identity_provider: projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/POOL_ID/providers/PROVIDER_ID
service_account: SERVICE_ACCOUNT@PROJECT_ID.iam.gserviceaccount.com
- name: Deploy to Play Store (Internal Track)
uses: r0adkll/upload-google-play@v1
with:
serviceAccountJson: ${{ steps.auth.outputs.credentials_file_path }}
packageName: com.example.myapp
releaseFiles: app/build/outputs/bundle/release/app-release.aab
tracks: internal
status: completed
whatsNewDirectory: distribution/whatsnew
Substitua workload_identity_provider e service_account pelos valores confirmados no console do GCP após a configuração do WIF. A única diferença da versão com chave JSON é passar o caminho do arquivo de credenciais para serviceAccountJson em vez de serviceAccountJsonPlainText — todo o resto é idêntico.
9. Criar o diretório de notas de versão
Crie distribution/whatsnew/ na raiz do projeto e coloque os arquivos:
distribution/whatsnew/
├── whatsnew-en-US
└── whatsnew-pt-BR
Cada arquivo contém notas de versão no idioma respectivo (até 500 caracteres).
10. Vamos tentar
Uma vez aqui, basta fazer push de uma tag:
git tag v1.0.0
git push origin v1.0.0
Abra a aba Actions no GitHub e o workflow começará a executar. Se tiver sucesso, um novo release aparecerá em Testes internos no Play Console.
11. Armadilhas comuns e soluções
Algumas armadilhas em que caí durante a operação real:
Esquecer de incrementar versionCode
O Play Console não permite valores versionCode duplicados. Configurar numeração automática usando o número de execução do GitHub Actions (GITHUB_RUN_NUMBER) evita acidentes. Use o nome da tag sem v para versionName.
android {
defaultConfig {
// Na CI, usar GITHUB_RUN_NUMBER como versionCode (fallback para 1 localmente)
versionCode = (System.getenv("GITHUB_RUN_NUMBER")?.toInt() ?: 1) + 1000
// Na CI, usar a git tag (v1.0.0 → 1.0.0) como versionName
versionName = System.getenv("GITHUB_REF_NAME")?.removePrefix("v") ?: "1.0.0-local"
}
}
Atraso na propagação de permissões da conta de serviço
Executar Actions imediatamente após conceder permissões pode resultar em 403 The caller does not have permission. As mudanças de permissão levam algum tempo para propagar, então tenha paciência na primeira execução.
Quebras de linha mistas na decodificação Base64
O comando base64 se comporta diferentemente em diferentes SOs:
- macOS: Insere automaticamente quebras de linha a cada 76 caracteres → use
-i/-opara saída limpa - Linux: Insere quebras de linha por padrão → use
-w 0para sem quebras de linha - Windows (
certutil): Cabeçalhos, rodapés e quebras de linha todos incluídos → pós-processamento necessário;[Convert]::ToBase64Stringdo PowerShell é mais seguro
Sempre verifique se não há quebras de linha antes de colar no GitHub Secrets. Adicionar tr -d '\n\r' ao decodificar na CI é prática padrão.
- name: Decode Keystore
run: |
echo "${{ secrets.KEYSTORE_BASE64 }}" | tr -d '\n\r' | base64 --decode > release.keystore
Conclusão
Com o GitHub Actions, o processo de publicação se torna:
"Simplesmente criar uma tag e fazer push"
Isso permite aumentar a frequência de publicações com carga operacional quase nula.
Para equipes com vários desenvolvedores, resolver apenas o problema de "só aquele Mac pode fazer publicações" já vale a pena adotá-lo. Se você tem as mesmas dificuldades no desenvolvimento de apps Android, experimente.
Referências
Android / Assinatura
- Assinar seu aplicativo | Android Developers
- Sobre Android App Bundles | Android Developers
- APK versus AAB | Android Developers
- Configurar seu build | Android Developers
- Dicas e receitas Gradle (excluir informações de assinatura do Git) | Android Developers
Google Play Console
- Usar o Play App Signing | Ajuda do Play Console
- Google Play Developer API
- Introdução à Google Play Developer API | Android Developers
GitHub Actions
- actions/checkout
- actions/setup-java
- actions/cache
- actions/upload-artifact
- r0adkll/upload-google-play
- Segredos criptografados | GitHub Docs
