Introducción
¿Sigues publicando tu aplicación Android haciendo clic manualmente en Generate Signed Bundle en Android Studio, y luego abriendo la Play Console para arrastrar y soltar el AAB?
Yo lo hacía así durante un tiempo, pero a medida que aumentaba la frecuencia de publicaciones, encontré los siguientes problemas:
- Los builds se rompen por diferencias en el entorno local (incompatibilidades de versión de JDK, problemas con la caché de Gradle)
- La gestión del Keystore se convierte en dependencia de una persona (solo existe en el Mac de un desarrollador específico)
- El proceso de publicación depende de la documentación, lo que lleva a errores humanos
Para resolver todos estos problemas de una vez, automaticé todo el flujo —compilación, firma y carga a Google Play Console— con GitHub Actions. Aquí está el procedimiento paso a paso.
Visión general
El pipeline que construiremos luce así:
flowchart TD
A["Push de git tag v1.0.0"]
B["Se activa GitHub Actions"]
C["Configurar JDK y restaurar cach\u00e9 de Gradle"]
D["Restaurar Keystore desde Secrets"]
E["Compilar y firmar AAB con ./gradlew bundleRelease"]
F["Distribuir a pruebas internas via r0adkll/upload-google-play"]
G["Google recibe, re-firma con la clave de firma de app y distribuye a usuarios"]
A --> B --> C --> D --> E --> F --> GLos términos "AAB" y "clave de subida" se explican en la siguiente sección.
3 conceptos clave que debes entender primero
Antes de entrar en los pasos concretos, aclaremos los términos clave que aparecen en este artículo. Si estos conceptos no están claros, la parte de Play App Signing puede resultar confusa.
APK vs AAB
| APK (Android Package Kit) | AAB (Android App Bundle) | |
|---|---|---|
| Contenido | Paquete universal que combina todos los ABI, densidades de pantalla y recursos de idioma en un solo archivo | Artefacto de build antes de la optimización para dispositivos específicos |
| Tamaño del archivo | Tiende a ser grande porque incluye todo | Google Play genera Split APKs para la distribución, reduciendo el tamaño de descarga del usuario en ~15% de media |
| Uso principal | Distribución directa / distribución interna / tiendas de terceros | Distribución en Google Play Store (obligatorio para nuevas apps desde agosto de 2021) |
| Tarea Gradle | ./gradlew assembleRelease |
./gradlew bundleRelease |
En pocas palabras: "APK = producto terminado que se instala en el dispositivo del usuario" y "AAB = plano que se envía a Google Play". Google Play recibe el AAB y ensambla el APK adecuado para el dispositivo de cada usuario.
Como este artículo apunta a la distribución en Play Store, usaremos AAB (bundleRelease). Para distribución interna u otros casos de uso que requieran APK, usa assembleRelease.
Clave de subida vs clave de firma de aplicación
Play App Signing utiliza dos tipos de claves de firma:
| Tipo de clave | ¿Quién gestiona? | Propósito |
|---|---|---|
| Clave de subida (Upload Key) | Desarrollador | Firma el AAB al subirlo a la Play Console |
| Clave de firma de aplicación (App Signing Key) | Firma el APK final distribuido a los usuarios desde Play Store |
En otras palabras, la Play Console verifica si el AAB está firmado con la clave de subida correcta, por eso los desarrolladores necesitan firmar con su propia clave de subida = Keystore. Lo que Google hace por ti es solo la re-firma con la clave de firma de aplicación en el momento de la distribución.
flowchart TD
Dev["Desarrollador"] -->|"Firma con clave de subida"| AAB["AAB"]
AAB --> Play["Play Console"]
Play --> Verify["Google verifica la clave de subida"]
Verify --> Resign["Google re-firma con clave de firma de app"]
Resign --> User["Usuario"]Incluso con Play App Signing, el hecho de que "todavía necesitas crear tu propio Keystore" no ha cambiado. Lo que cambió es la red de seguridad: "puedes solicitar un restablecimiento si pierdes tu clave de subida".
Firma de depuración vs firma release
Cuando pulsaste Run por primera vez en Android Studio sin ninguna configuración, se construyó un APK y se ejecutó en tu dispositivo porque AGP (Android Gradle Plugin) genera automáticamente ~/.android/debug.keystore y firma automáticamente los builds de depuración.
| Firma de depuración | Firma release | |
|---|---|---|
| Keystore | ~/.android/debug.keystore (generado automáticamente por AGP) |
Creado y gestionado por ti (release.keystore en este artículo) |
| Contraseña | Fija: android |
Definida por ti |
| Alias | Fijo: androiddebugkey |
Definido por ti |
| Expiración | ~30 años | Definida por ti (~27 años en este artículo) |
| Uso | Pruebas de desarrollo / ./gradlew assembleDebug |
Distribución Play Store / ./gradlew bundleRelease |
| Gestión Git | No necesaria (OK si existe por máquina) | Conservar de forma segura (puede restablecerse via Play App Signing si se pierde) |
Dos propiedades importantes:
- La Play Console rechaza categóricamente los AAB firmados con firma de depuración
→ La CI necesita unrelease.keystoreseparado - La firma de depuración y la firma release son completamente independientes
→ Agregar la configuración de firma release para CI no tiene ningún efecto en los llamados locales./gradlew assembleDebug
El signingConfigs.create("release") condicional en las secciones 5 y 6 existe precisamente para aprovechar estas propiedades, creando un estado donde los desarrolladores sin la clave aún pueden ejecutar builds de depuración normalmente.
Requisitos previos
- Aplicación Android (usando
build.gradleobuild.gradle.kts) - Repositorio GitHub
- Cuenta de desarrollador de Google Play Console
- La primera publicación de la app ya debe haberse realizado manualmente en la Play Console (la creación inicial a través de la API no es posible)
- Play App Signing debe estar activado para la app objetivo
Para apps creadas después de agosto de 2021, Play App Signing se activa automáticamente, por lo que no se requieren pasos especiales. Para apps más antiguas creadas antes de eso con el método legacy, primero necesitas migrar a Play App Signing (tratado en la sección 7-4).
1. Crear la clave de subida (Keystore)
Genera el Keystore localmente para firmar el AAB. En una configuración de Play App Signing, esta clave funciona como la "clave de subida" (= tu identidad al enviar el AAB a la Play Console). La entrega final a los usuarios está firmada por la "clave de firma de aplicación" de Google, por lo que incluso si pierdes esta clave, puedes solicitar un restablecimiento desde la Play Console (detalles en 7-3).
Dicho esto, perderla en las operaciones diarias detendrá las publicaciones hasta que se complete el restablecimiento, así que guárdala de forma segura.
# macOS / Linux
keytool -genkey -v \
-keystore release.keystore \
-alias my-release-key \
-keyalg RSA \
-keysize 2048 \
-validity 10000
En Windows (PowerShell / Símbolo del sistema), la continuación de línea con backslash no funciona, así que ejecútalo en una sola línea o usa el backtick ` para los saltos de línea en PowerShell.
keytool -genkey -v `
-keystore release.keystore `
-alias my-release-key `
-keyalg RSA `
-keysize 2048 `
-validity 10000
Introduce la contraseña, nombre, organización, etc. de forma interactiva.
2. Codificar el Keystore en Base64
GitHub Secrets no puede almacenar binario directamente, así que conviértelo en una cadena Base64. El workflow elimina los saltos de línea con tr -d '\n\r' durante la decodificación, pero una salida sin saltos de línea es más 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 escribir en un archivo:
[Convert]::ToBase64String([IO.File]::ReadAllBytes("release.keystore")) `
| Out-File -Encoding ascii -NoNewline release.keystore.base64
Windows (Símbolo del sistema)
certutil funciona pero añade encabezados, pies de página y saltos de línea que deben eliminarse.
certutil -encode release.keystore release.keystore.base64
Elimina las líneas -----BEGIN CERTIFICATE----- y -----END CERTIFICATE----- y todos los saltos de línea del archivo de salida. Se recomienda usar PowerShell.
Copia la cadena del portapapeles y pégala en GitHub Secrets en el siguiente paso.
3. Crear una cuenta de servicio para Google Play Console
Las cargas automatizadas a la Play Console requieren una cuenta de servicio de Google Cloud.
3-1. Habilitar la API de Google Play Android Developer
Primero, habilita la API en el lado de GCP. Omitir este paso resultará en un error 403 más adelante, así que hazlo primero.
- Ve a Google Play Android Developer API
- Haz clic en Habilitar
3-2. Crear una cuenta de servicio en Google Cloud Console
- Ve a Google Cloud Console
- Crea un proyecto (o selecciona uno existente)
- En IAM y administración → Cuentas de servicio, crea una nueva. No asignes roles de IAM (los permisos se gestionan en el lado de la Play Console)
- Después de crearla, selecciona Agregar clave → Crear nueva clave → JSON y descarga el archivo JSON
3-3. Otorgar permisos en la Play Console
- Abre la Play Console
- Abre Usuarios y permisos, haz clic en Invitar nuevos usuarios
- Introduce la dirección de correo electrónico de la cuenta de servicio creada en el paso 3-2
- En la pestaña Permisos de la aplicación, agrega la app objetivo y otorga los siguientes permisos:
- Publicar en tracks de prueba — necesario para la distribución al track de pruebas internas
- Ver información de la app y descargar informes masivos — permiso de lectura
4. Registrar valores en GitHub Secrets
Desde Settings → Secrets and variables → Actions del repositorio, registra lo siguiente:
| Nombre del Secret | Valor |
|---|---|
KEYSTORE_BASE64 |
Cadena Base64 del paso 2 |
KEYSTORE_PASSWORD |
Contraseña del Keystore |
KEY_ALIAS |
Ej.: my-release-key |
KEY_PASSWORD |
Contraseña de la clave |
SERVICE_ACCOUNT_JSON |
Pega directamente el contenido del archivo JSON descargado en el paso 3-2 |
5. Configurar build.gradle para leer la información de firma desde variables de entorno
Configúralo para que la CI lea desde variables de entorno y el desarrollo local lea desde keystore.properties, y que incluso los entornos sin Keystore puedan ejecutar builds de depuración normalmente.
Edita app/build.gradle.kts de la siguiente manera:
import java.util.Properties
import java.io.FileInputStream
// --- Resolver información de firma ---
// Prioridad: variables de entorno (CI) > keystore.properties (local) > ninguna (solo 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") {
// Agregar applicationIdSuffix para que los builds release y debug
// puedan coexistir en el mismo dispositivo
applicationIdSuffix = ".debug"
versionNameSuffix = "-debug"
// signingConfig usa el debug.keystore auto-generado por AGP
}
getByName("release") {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
// Solo asignar release signingConfig si la información de firma está disponible
// (= los desarrolladores sin la clave aún pueden ejecutar assembleDebug)
if (hasReleaseSigning) {
signingConfig = signingConfigs.getByName("release")
}
}
}
}
Puntos clave de este código:
signingConfigs.create("release")está protegido, por lo que la configuración release no se crea en entornos sin información de firma- El tipo de build
debugusa eldebug.keystoreauto-generado por AGP y no se ve afectado en absoluto por la configuración de firma release - Las variables de entorno tienen la mayor prioridad, por lo que no se necesita ningún archivo
keystore.propertiesen CI
6. Configuración adicional para no romper el desarrollo local
"Modifiqué build.gradle para la CI y ahora los builds de depuración locales están rotos" es un accidente común en configuraciones Android × CI. Para evitar esto, configura lo siguiente:
6-1. Crear keystore.properties (solo local)
local.properties está destinado a rutas SDK, y mezclar información de firma ahí hace las cosas confusas. Crear un archivo keystore.properties separado para la información de firma es el enfoque recomendado en la documentación oficial de Android.
Crea keystore.properties en la raíz del proyecto (un nivel por encima de app/):
storeFile=/Users/yourname/keys/release.keystore
storePassword=your-store-password
keyAlias=my-release-key
keyPassword=your-key-password
Este archivo nunca debe ir a Git, así que agrégalo al .gitignore (ver abajo).
6-2. Qué agregar al .gitignore
Las entradas relacionadas con firma a menudo se olvidan ya que el .gitignore generado por Android Studio no las incluye. Agrégalas explícitamente:
# Firma - nunca commitear
*.keystore
*.jks
keystore.properties
release.keystore.base64
# JSON de cuenta de servicio
*-service-account*.json
play-publisher.json
# Archivos de autenticación temporales generados por google-github-actions/auth (usando WIF)
gha-creds-*.json
# Creado por Android Studio pero verificar de todas formas
local.properties
local.properties generalmente lo agrega Android Studio, pero verifica si falta en proyectos más antiguos.
6-3. Coexistencia de Release y Debug con applicationIdSuffix
El snippet build.gradle.kts anterior incluye:
debug {
applicationIdSuffix = ".debug"
}
Esto permite instalar simultáneamente la versión release de Play Store y una versión debug construida localmente en el mismo dispositivo. Sin esto, instalar un build de depuración sobreescribiría la versión de Play Store en tu dispositivo de prueba.
No está directamente relacionado con CI, pero una vez que empiezas a automatizar las publicaciones, ejecutarás con frecuencia la versión Play Store en tu dispositivo de prueba, así que recomiendo encarecidamente agregarlo.
6-4. Usar ~/.gradle/gradle.properties
Otra opción es escribir la información de firma en las propiedades Gradle del directorio 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
En build.gradle.kts, léelo con findProperty("MYAPP_KEYSTORE_FILE") as String?.
La ventaja es que no hay información secreta en el directorio del proyecto en absoluto. La desventaja es que comunicar los pasos de configuración a nuevos desarrolladores requiere un poco más de esfuerzo. Mi recomendación: ~/.gradle/gradle.properties para proyectos en solitario, keystore.properties protegido por .gitignore para proyectos en equipo.
6-5. Probar builds release sin firmar en Android Studio
Aunque ./gradlew assembleDebug siempre funciona, es posible que quieras ejecutar ./gradlew assembleRelease sin firma (por ejemplo, para verificar la configuración de ProGuard para builds release).
En ese caso, cuando hasReleaseSigning es false, el tipo de build release no tendrá signingConfig, por lo que ejecutar assembleRelease produce un APK sin firmar (no se puede instalar en un dispositivo, pero se puede verificar el tamaño y mapping.txt).
7. Notas operacionales sobre Play App Signing
Con los pasos 1–6 completados, la configuración de Play App Signing ya está terminada. El release.keystore creado en el paso 1 funciona como clave de subida en el contexto de Play App Signing.
Esta sección contiene notas adicionales útiles en la operación.
7-1. Qué ocurre en la primera subida
Para una nueva app, cuando subes el AAB a la Play Console por primera vez:
- El certificado usado en ese AAB se registra automáticamente como "certificado de clave de subida"
- Google genera automáticamente una nueva "clave de firma de aplicación"
- Play App Signing se activa
Por lo tanto, no se necesita configuración adicional de la Play Console – seguir los pasos de este artículo tal cual establecerá la configuración de Play App Signing.
flowchart TD
CI["CI"] -->|"Firma con release.keystore"| AAB["AAB"]
AAB --> Play["Play Console"]
Play --> Verify["Google verifica el certificado de clave de subida"]
Verify --> Resign["Google re-firma con clave de firma de app"]
Resign --> Device["Dispositivo del usuario"]7-2. Ir un paso más allá: Crear una clave de subida dedicada
Incluso para nuevas apps, puedes elegir "registrar la clave de subida por separado" en la Play Console.
- Durante la primera subida, selecciona "Usar clave de firma de app generada por Google" en la Play Console
- En la misma pantalla, sube el certificado de clave de subida por separado
En este caso, release.keystore contiene una "clave completamente dedicada solo a la subida".
| Patrón estándar (7-1) | Enfoque de claves separadas (7-2) | |
|---|---|---|
Contenido de release.keystore |
Clave de subida = clave que firmó el primer AAB | Clave de subida pura |
| Si se compromete | Puede recuperarse con solicitud de restablecimiento | Puede recuperarse con solicitud de restablecimiento |
| Nivel de seguridad | Suficiente | Más estricto |
Los pasos de CI son idénticos para ambos patrones. La única diferencia es cómo creas release.keystore.
7-3. Qué hacer si pierdes la clave de subida
Esta es la mayor ventaja de Play App Signing. Puedes solicitar un restablecimiento desde la Play Console.
-
Crea un nuevo Keystore localmente
-
Exporta el certificado en formato PEM
keytool -export -rfc \ -keystore release.keystore \ -alias my-release-key \ -file upload_certificate.pem -
En Play Console → Configuración → Integridad de la app → Firma de la app → Solicitar restablecimiento de clave de subida, sube el certificado
-
El soporte de Google verifica (normalmente 1-2 días hábiles)
-
Tras la aprobación, los AAB firmados con la nueva clave serán aceptados
Con el método legacy, "perder tu clave significaba que nunca podrías actualizar el mismo nombre de paquete"—un callejón sin salida permanente. Esta opción de recuperación por sí sola hace que valga la pena adoptar Play App Signing.
7-4. Migrar una app existente (legacy)
Para apps creadas antes de agosto de 2021 que aún no han migrado a Play App Signing, debes entregar la clave de firma actual a Google:
-
Play Console → app objetivo → Configuración → Integridad de la app → Firma de la app
-
Descarga la herramienta PEPK (Play Encrypt Private Key) proporcionada por Google
-
Usa PEPK para exportar el Keystore actual en forma cifrada
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 -
Sube el archivo de salida (
encrypted-key.zip) a la Play Console -
Después de la migración, crea y registra una nueva clave de subida si es necesario (continuar usando la clave existente como clave de subida también es posible)
public-key.pem es la clave pública que aparece en la pantalla de migración en la Play Console, guardada como archivo.
Después de la migración, los pasos 1–6 de este artículo se aplican directamente.
8. Escribir el workflow de GitHub Actions
Ahora el tema principal. Crea .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
Puntos clave
- El trigger de tag (
v*) evita el despliegue accidental en cada push a main - La caché de Gradle reduce el tiempo de compilación (de ~6 minutos a ~2 minutos en ejecuciones posteriores)
- La subida de artefacto sirve como respaldo — si la subida a la Play Console falla, el AAB puede descargarse manualmente desde la interfaz de Actions
tracks: internalpuede cambiarse aalpha,beta,production, etc.whatsNewDirectoryes el directorio para notas de versión por idioma, ej.distribution/whatsnew/whatsnew-ja-JP
8-2. Configuración más segura: Usar Workload Identity Federation
En lugar de almacenar claves JSON de larga duración en GitHub Secrets, Workload Identity Federation (WIF) permite a GitHub Actions obtener credenciales temporales para acceder a la Play Console. Esto elimina el riesgo de filtración de claves JSON y se recomienda para producción.
Requisitos previos (lado GCP)
La configuración de WIF se realiza una sola vez. Sigue los pasos en google-github-actions/auth. Con este enfoque, no se genera ninguna clave JSON para la cuenta de servicio, por lo que el paso 4 en la sección 3-2 (crear/descargar clave) y registrar SERVICE_ACCOUNT_JSON en la sección 4 no son necesarios.
Configura con los siguientes comandos de gcloud. Reemplaza cada placeholder con tus valores reales:
| Placeholder | Descripción |
|---|---|
${PROJECT_ID} |
ID del proyecto GCP |
${GITHUB_ORG} |
Nombre de la organización GitHub o nombre de usuario |
${REPO} |
Nombre del repositorio en formato org/repo |
${SERVICE_ACCOUNT} |
Nombre de la cuenta de servicio del paso 3-2 (la parte antes de @ en el correo) |
${WORKLOAD_IDENTITY_POOL_ID} |
ID completo del Pool obtenido en el paso 2 |
# 1. Crear Workload Identity Pool
gcloud iam workload-identity-pools create "github" \
--project="${PROJECT_ID}" \
--location="global" \
--display-name="GitHub Actions Pool"
# 2. Obtener el ID completo del Pool
gcloud iam workload-identity-pools describe "github" \
--project="${PROJECT_ID}" \
--location="global" \
--format="value(name)"
# Ejemplo: projects/123456789/locations/global/workloadIdentityPools/github
# 3. Crear Workload Identity Provider
# --attribute-condition restringe a tokens solo de esta organización
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. Otorgar roles/iam.workloadIdentityUser a la cuenta de servicio
# principalSet's attribute.repository restringe a este repositorio específico
# ${WORKLOAD_IDENTITY_POOL_ID} = ID completo obtenido en el paso 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}"
Después de la configuración, anota el ID completo del Provider (projects/.../providers/...). Se usa en el campo workload_identity_provider del YAML de GitHub Actions.
YAML del workflow (versión WIF)
Agrega el permiso id-token: write al repositorio y un paso google-github-actions/auth:
name: Android Release
on:
push:
tags:
- 'v*'
permissions:
contents: read
id-token: write # Requerido para la adquisición del 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
Reemplaza workload_identity_provider y service_account con los valores confirmados en la consola de GCP después de la configuración de WIF. La única diferencia con la versión de clave JSON es pasar la ruta del archivo de credenciales a serviceAccountJson en lugar de serviceAccountJsonPlainText — todo lo demás es idéntico.
9. Crear el directorio de notas de versión
Crea distribution/whatsnew/ en la raíz del proyecto y coloca los archivos:
distribution/whatsnew/
├── whatsnew-en-US
└── whatsnew-ja-JP
Cada archivo contiene notas de versión en el idioma respectivo (hasta 500 caracteres).
10. Vamos a probarlo
Una vez aquí, solo queda hacer push de un tag:
git tag v1.0.0
git push origin v1.0.0
Abre la pestaña Actions en GitHub y el workflow comenzará a ejecutarse. Si tiene éxito, aparecerá un nuevo release en Pruebas internas en la Play Console.
11. Trampas comunes y soluciones
Algunos escollos en los que caí durante la operación real:
Olvidar incrementar versionCode
La Play Console no permite valores versionCode duplicados. Configurar la numeración automática usando el número de ejecución de GitHub Actions (GITHUB_RUN_NUMBER) previene accidentes. Usa el nombre del tag sin v para versionName.
android {
defaultConfig {
// En CI, usar GITHUB_RUN_NUMBER como versionCode (fallback a 1 en local)
versionCode = (System.getenv("GITHUB_RUN_NUMBER")?.toInt() ?: 1) + 1000
// En CI, usar el tag git (v1.0.0 → 1.0.0) como versionName
versionName = System.getenv("GITHUB_REF_NAME")?.removePrefix("v") ?: "1.0.0-local"
}
}
Retraso en la propagación de permisos de cuenta de servicio
Ejecutar Actions inmediatamente después de otorgar permisos puede resultar en 403 The caller does not have permission. Los cambios de permisos tardan algo de tiempo en propagarse, así que sé paciente en la primera ejecución.
Saltos de línea mezclados en la decodificación Base64
El comando base64 se comporta de manera diferente en distintos sistemas operativos:
- macOS: Inserta automáticamente saltos de línea cada 76 caracteres → usa
-i/-opara salida limpia - Linux: Inserta saltos de línea por defecto → usa
-w 0para sin saltos de línea - Windows (
certutil): Encabezados, pies de página y saltos de línea todos incluidos → se requiere post-procesamiento;[Convert]::ToBase64Stringde PowerShell es más seguro
Siempre verifica que no haya saltos de línea antes de pegar en GitHub Secrets. Agregar tr -d '\n\r' al decodificar en CI es práctica estándar.
- name: Decode Keystore
run: |
echo "${{ secrets.KEYSTORE_BASE64 }}" | tr -d '\n\r' | base64 --decode > release.keystore
Conclusión
Con GitHub Actions, el proceso de publicación se convierte en:
"Solo crear un tag y hacer push"
Esto permite aumentar la frecuencia de publicaciones con una carga operacional casi nula.
Para equipos con varios desarrolladores, resolver únicamente el problema de "solo ese Mac puede hacer publicaciones" ya vale la pena adoptarlo. Si tienes las mismas dificultades en el desarrollo de apps Android, pruébalo.
Referencias
Android / Firma
- Firmar tu aplicación | Android Developers
- Acerca de los Android App Bundles | Android Developers
- APK versus AAB | Android Developers
- Configurar tu build | Android Developers
- Consejos y recetas de Gradle (excluir información de firma de Git) | Android Developers
Google Play Console
- Usar Play App Signing | Ayuda de Play Console
- Google Play Developer API
- Empezar con la Google Play Developer API | Android Developers
GitHub Actions
- actions/checkout
- actions/setup-java
- actions/cache
- actions/upload-artifact
- r0adkll/upload-google-play
- Secretos cifrados | GitHub Docs
