AndroidGitHub ActionsGoogle PlayCI/CDKotlin

Compilar, assinar e fazer deploy automático de apps Android no Google Play Console com GitHub Actions

Sloth255
Sloth255
·6 min read·1,174 words

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 --> G

Os 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) Google 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:

  1. O Play Console rejeita categoricamente AABs assinados com assinatura de debug
    → A CI precisa de um release.keystore separado
  2. 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.gradle ou build.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.

  1. Acesse Google Play Android Developer API
  2. Clique em Habilitar

3-2. Criar uma conta de serviço no Google Cloud Console

  1. Acesse o Google Cloud Console
  2. Crie um projeto (ou selecione um existente)
  3. 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)
  4. 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

  1. Abra o Play Console
  2. Abra Usuários e permissões, clique em Convidar novos usuários
  3. Insira o endereço de e-mail da conta de serviço criada na etapa 3-2
  4. 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 debug usa o debug.keystore auto-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.

  1. Crie um novo Keystore localmente

  2. Exporte o certificado no formato PEM

    keytool -export -rfc \
      -keystore release.keystore \
      -alias my-release-key \
      -file upload_certificate.pem
    
  3. No Play Console → Configuração → Integridade do app → Assinatura do app → Solicitar redefinição de chave de upload, faça upload do certificado

  4. O suporte do Google verifica (normalmente 1-2 dias úteis)

  5. 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:

  1. Play Console → app alvo → Configuração → Integridade do app → Assinatura do app

  2. Baixe a ferramenta PEPK (Play Encrypt Private Key) fornecida pelo Google

  3. 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
    
  4. Faça upload do arquivo de saída (encrypted-key.zip) para o Play Console

  5. 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: internal pode ser mudado para alpha, beta, production etc.
  • 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 / -o para saída limpa
  • Linux: Insere quebras de linha por padrão → use -w 0 para sem quebras de linha
  • Windows (certutil): Cabeçalhos, rodapés e quebras de linha todos incluídos → pós-processamento necessário; [Convert]::ToBase64String do 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

Google Play Console

GitHub Actions

Gradle