Subir una actualización a Google Play Store de forma manual es uno de los procesos más tediosos y propensos a errores en el día a día de un desarrollador Android: abrir Android Studio, compilar el bundle firmado, introducir contraseñas de keystore, abrir el navegador, loguearse en Play Console, navegar entre pantallas, arrastrar el archivo AAB, subir el fichero mapping.txt de ofuscación, escribir los textos de novedades y enviar a revisión. Repetir esto cada vez que corriges un bug o lanzas una mejora consume decenas de horas al año.
En este tutorial te enseñamos paso a paso cómo automatizamos en Walkiria Apps la compilación y subida completa a Google Play Store con un único comando por terminal, utilizando Gradle y un script en Python conectado directamente a la Google Play Developer API v3 oficial de Google.
- Velocidad: Despliegue en 2 minutos sin tocar el navegador.
- Fiabilidad: Nunca olvidarás subir el archivo de mapeo R8/ProGuard (indispensable para descifrar fallos en producción).
- Seguridad: Tus credenciales y contraseñas de firma no quedan expuestas en repositorios públicos.
- Compatible con CI/CD: Listo para ejecutarse en local o en pipelines de GitHub Actions y GitLab CI.
Arquitectura del Sistema de Publicación
El flujo automatizado se compone de tres bloques perfectamente desacoplados:
Fase 1: Compilación, Auto-Incremento de Versión y Firma con Gradle
Mediante la tarea bundleRelease de Gradle, el proyecto calcula el versionCode automáticamente, compila el código en Kotlin, ejecuta la ofuscación y optimización con R8, y firma criptográficamente el paquete Android App Bundle (.aab) utilizando las credenciales almacenadas de forma segura en un archivo local key.properties.
Fase 2: Despliegue con la Google Play Developer API
Un script en Python se autentica mediante una Cuenta de Servicio (Service Account) de Google Cloud, abre una transacción de edición (Edit) en Play Console, sube el paquete AAB en bloques reanudables, envía el archivo mapping.txt para desofuscar stacktraces, actualiza los metadatos ASO de la ficha, configura las notas de la versión en varios idiomas y confirma (commit) el lanzamiento.
Fase 3: Notificación de Éxito o Fallo a Telegram / Discord
Al concluir el despliegue, el script envía una alerta instantánea a tu canal o grupo de desarrolladores con el número de versión y el estado de la publicación.
Paso 1: Auto-Incremento de Versión y Firma Automática en Gradle
Uno de los errores más frecuentes al intentar subir una app es recibir el mensaje de error Version code X has already been used porque olvidaste subir el número de compilación. Para solucionarlo de raíz, podemos automatizar el versionCode en app/build.gradle.kts basándonos en el timestamp actual o en un fichero version.properties:
// En app/build.gradle.kts
// Genera un versionCode único automático basado en el tiempo Unix en minutos
val autoVersionCode: Int = (System.currentTimeMillis() / 60000).toInt()
Para no escribir las contraseñas del keystore a mano en cada compilación, creamos un archivo llamado key.properties en la raíz de nuestro proyecto (¡asegúrate de incluirlo en tu .gitignore!):
releaseKeyAlias=mi_alias_de_firma
releaseKeyPassword=contrasena_del_alias
releaseKeyStore=/ruta/absoluta/o/relativa/a/mi-keystore.jks
releaseStorePassword=contrasena_del_keystore
Luego, configuramos el bloque defaultConfig, signingConfigs y buildTypes en nuestro app/build.gradle.kts:
import java.io.FileInputStream
import java.util.Properties
val keystorePropertiesFile = rootProject.file("key.properties")
val keystoreProperties = Properties()
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
}
android {
defaultConfig {
applicationId = "com.walkiria.miapp"
minSdk = 24
targetSdk = 35
versionCode = autoVersionCode
versionName = "1.2.0"
}
signingConfigs {
create("release") {
keyAlias = keystoreProperties.getProperty("releaseKeyAlias")
keyPassword = keystoreProperties.getProperty("releaseKeyPassword")
storeFile = file(keystoreProperties.getProperty("releaseKeyStore"))
storePassword = keystoreProperties.getProperty("releaseStorePassword")
}
}
buildTypes {
release {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
signingConfig = signingConfigs.getByName("release")
}
}
}
A partir de este momento, ejecutar en la terminal ./gradlew bundleRelease generará automáticamente tu paquete firmado listo para publicación en app/build/outputs/bundle/release/app-release.aab junto con su archivo de ofuscación en app/build/outputs/mapping/release/mapping.txt sin pedirte ninguna clave interactiva.
Paso 2: Crear y Autorizar la Cuenta de Servicio en Google Play Console
Para que nuestro script pueda comunicarse con la API de Google Play sin interacción humana en un navegador, necesitamos una cuenta de servicio (Service Account):
- Entra en Google Cloud Console (console.cloud.google.com) en el proyecto vinculado a tu cuenta de desarrollador.
- Ve a IAM y administración > Cuentas de servicio y pulsa en Crear cuenta de servicio. Nómbrala, por ejemplo,
play-publisher. - En la pestaña Claves de esa cuenta de servicio, haz clic en Agregar clave > Crear clave nueva > Formato JSON. Guarda ese archivo
.jsonen un lugar seguro de tu ordenador. - Abre Google Play Console, dirígete a Acceso a la API (en Configuración de la cuenta).
- Verás la cuenta de servicio recién creada; pulsa en Conceder acceso y asígnale los permisos: «Lanzar versiones a producción, excluir dispositivos y usar la firma de apps de Play» y «Administrar pistas de prueba y editar listas de testers» sobre tu aplicación.
Paso 3: El Script de Python para Subida, Ficha ASO y Notificación
Instalamos en nuestro entorno local la biblioteca cliente oficial de Google para Python:
pip install google-api-python-client google-auth
A continuación tienes el script completo publish_play_store.py que usamos en producción en Walkiria Apps, con soporte para subida de AAB, archivo de mapeo R8, actualización de metadatos ASO de la ficha, selección de pista y avisos automáticos a Telegram:
import os
import sys
import socket
import urllib.request
import urllib.parse
from google.oauth2 import service_account
from googleapiclient.discovery import build
from googleapiclient.http import MediaFileUpload
# Aumentar timeout para subidas pesadas de bundles
socket.setdefaulttimeout(600)
KEY_FILE = "ruta/a/tu/cuenta-servicio.json"
PKG = "com.walkiria.miapp"
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
AAB_PATH = os.path.join(BASE_DIR, "app/build/outputs/bundle/release/app-release.aab")
MAPPING_PATH = os.path.join(BASE_DIR, "app/build/outputs/mapping/release/mapping.txt")
# Pista de despliegue: 'production', 'beta' (para los 20 testers), 'alpha' o 'internal'
TRACK = "production"
# Configuración de lanzamiento: 'completed' (100% de usuarios) o 'inProgress' (despliegue escalonado)
RELEASE_STATUS = "completed"
USER_FRACTION = 1.0 # Para escalonado: 0.1 = 10%, 0.2 = 20%, 0.5 = 50%
# Opcional: Notificación Telegram al finalizar
TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "")
TELEGRAM_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID", "")
def notify_telegram(message):
if not TELEGRAM_BOT_TOKEN or not TELEGRAM_CHAT_ID:
return
try:
url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage"
data = urllib.parse.urlencode({"chat_id": TELEGRAM_CHAT_ID, "text": message, "parse_mode": "Markdown"}).encode("utf-8")
urllib.request.urlopen(url, data=data, timeout=10)
except Exception as e:
print(f"⚠️ No se pudo enviar notificación a Telegram: {e}")
def main():
if not os.path.exists(AAB_PATH):
print(f"Error: No se encontró el bundle en {AAB_PATH}")
print("Ejecuta primero: ./gradlew bundleRelease")
sys.exit(1)
print(f"🚀 Iniciando subida a Google Play Store para {PKG}...")
# 1. Autenticación con Service Account
creds = service_account.Credentials.from_service_account_file(
KEY_FILE,
scopes=["https://www.googleapis.com/auth/androidpublisher"]
)
service = build("androidpublisher", "v3", credentials=creds)
# 2. Iniciar sesión de edición (Transacción atómica)
print("1. Creando nueva sesión de edición (edit)...")
edit = service.edits().insert(packageName=PKG, body={}).execute()
edit_id = edit["id"]
print(f" Edit ID obtenido: {edit_id}")
try:
# 3. Subir el paquete AAB en bloques (chunks) reanudables
aab_size = os.path.getsize(AAB_PATH)
print(f"2. Subiendo AAB ({aab_size / (1024*1024):.2f} MB)...")
media = MediaFileUpload(AAB_PATH, mimetype="application/octet-stream", resumable=True, chunksize=2*1024*1024)
request = service.edits().bundles().upload(
packageName=PKG,
editId=edit_id,
media_body=media
)
response = None
while response is None:
status, response = request.next_chunk()
if status:
print(f" Progreso de subida: {int(status.progress() * 100)}%")
version_code = response["versionCode"]
print(f" ✅ Bundle subido con éxito. Version Code: {version_code}")
# 4. Subir archivo de mapeo R8 / ProGuard
if os.path.exists(MAPPING_PATH):
print("3. Subiendo archivo de mapeo ProGuard/R8...")
mapping_media = MediaFileUpload(MAPPING_PATH, mimetype="application/octet-stream")
service.edits().deobfuscationfiles().upload(
packageName=PKG,
editId=edit_id,
apkVersionCode=version_code,
deobfuscationFileType="proguard",
media_body=mapping_media
).execute()
print(" ✅ Archivo de mapeo subido correctamente.")
# 5. Opcional: Actualizar Ficha de Play Store (ASO) de forma automática
try:
listing_body = {
"language": "es-ES",
"title": "Mi App: Novedades y Utilidades",
"shortDescription": "Descubre la forma más rápida y eficiente de gestionar tus tareas.",
"fullDescription": "Descripción completa de la aplicación optimizada para palabras clave ASO..."
}
service.edits().listings().update(
packageName=PKG,
editId=edit_id,
language="es-ES",
body=listing_body
).execute()
print(" ✅ Ficha de Google Play (ASO) actualizada.")
except Exception as e:
print(f" ℹ️ Omitiendo actualización de ficha: {e}")
# 6. Configurar Track, Rollout y Notas de la Versión
print(f"4. Configurando release en el track '{TRACK}'...")
release_notes = [
{
"language": "es-ES",
"text": "• Mejoras de estabilidad y optimización de rendimiento.\n• Corrección de errores reportados por la comunidad."
},
{
"language": "en-US",
"text": "• Performance improvements and stability enhancements.\n• Bug fixes and general optimizations."
}
]
release_dict = {
"name": f"Versión {version_code}",
"versionCodes": [str(version_code)],
"status": RELEASE_STATUS,
"releaseNotes": release_notes
}
# Si es lanzamiento escalonado en producción (Staged Rollout)
if RELEASE_STATUS == "inProgress":
release_dict["userFraction"] = USER_FRACTION
track_body = {
"track": TRACK,
"releases": [release_dict]
}
service.edits().tracks().update(
packageName=PKG,
editId=edit_id,
track=TRACK,
body=track_body
).execute()
print(f" ✅ Track '{TRACK}' configurado con estado '{RELEASE_STATUS}'.")
# 7. Commit atómico de la transacción
print("5. Confirmando (commit) el lanzamiento en Google Play...")
commit_resp = service.edits().commit(packageName=PKG, editId=edit_id).execute()
print(f"🎉 ¡PUBLICACIÓN COMPLETADA CON ÉXITO! Commit ID: {commit_resp.get('id', edit_id)}")
# Notificar a Telegram
notify_telegram(f"🎉 *Nueva versión {version_code} publicada en Play Store*\nApp: `{PKG}`\nTrack: `{TRACK}`\nEstado: `{RELEASE_STATUS}`")
except Exception as e:
print(f"❌ Error durante el proceso: {e}")
notify_telegram(f"❌ *Error al publicar en Play Store*\nApp: `{PKG}`\nDetalle: `{e}`")
sys.exit(1)
if __name__ == "__main__":
main()
Paso 4: El Script Maestro de un Solo Clic (deploy.sh)
Para unir la compilación y la subida en una sola orden de terminal, creamos un script bash deploy.sh en la raíz del proyecto:
#!/bin/bash
set -e
echo "🔨 1/2 Compilando Release Bundle (AAB) con Gradle..."
./gradlew clean bundleRelease
echo "📦 2/2 Subiendo a Google Play Console con Python..."
python3 publish_play_store.py
echo "🚀 ¡Todo el proceso ha finalizado con éxito!"
Le damos permisos de ejecución con chmod +x deploy.sh y listo. Ahora, cada vez que queramos lanzar una versión, simplemente ejecutamos:
./deploy.sh
Extra Pro: Automatización Total con GitHub Actions (CI/CD en la Nube)
Si trabajas en equipo o prefieres que la compilación y subida ocurra en la nube cada vez que creas una etiqueta de versión (tag) en Git, puedes crear el archivo .github/workflows/deploy.yml:
name: Despliegue Automático a Google Play
on:
push:
tags:
- 'v*'
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Descargar código fuente
uses: actions/checkout@v4
- name: Configurar JDK 17
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
- name: Configurar Keystore y Credenciales
run: |
echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 --decode > my-release-key.jks
echo "${{ secrets.PLAY_SERVICE_ACCOUNT_JSON }}" > service_account.json
echo "releaseKeyAlias=${{ secrets.KEY_ALIAS }}" > key.properties
echo "releaseKeyPassword=${{ secrets.KEY_PASSWORD }}" >> key.properties
echo "releaseKeyStore=../my-release-key.jks" >> key.properties
echo "releaseStorePassword=${{ secrets.STORE_PASSWORD }}" >> key.properties
- name: Compilar AAB
run: ./gradlew clean bundleRelease
- name: Configurar Python e Instalar Dependencias
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Subir a Google Play Store
run: |
pip install google-api-python-client google-auth
python3 publish_play_store.py
Resolución de Errores Habituales de la Google Play API
Al conectar con la API de Google Play por primera vez, es habitual encontrarse con alguno de estos códigos de error:
⚠️ Error 401 / 403: «The caller does not have permission»
Ocurre cuando la cuenta de servicio de Google Cloud no ha sido invitada formalmente como usuario dentro de tu Google Play Console (en la sección Configuración de la cuenta > Acceso a la API > Usuarios de la cuenta de servicio), o bien no le has otorgado permisos específicos sobre el paquete de la aplicación.
⚠️ Error 400: «Release notes text is too long»
Google Play impone un límite estricto de 500 caracteres por idioma en el campo releaseNotes. Asegúrate de que tus viñetas de novedades no excedan esa longitud.
⚠️ Error 400: «Changes cannot be sent for review directly»
Si tu cuenta de desarrollador tiene activada la opción de «Envío gestionado» (Managed Publishing), la API compromete la versión en estado de borrador listo para revisión. Debes entrar a Play Console y pulsar en «Enviar a revisión» si tienes esta directiva activada en los ajustes de publicación.
🚀 ¿Estás preparando tu app para Google Play?
Recuerda que para cuentas creadas a partir de noviembre de 2023, Google exige pasar una prueba cerrada de 20 testers durante 14 días antes de habilitar el botón de producción. En nuestro grupo de Telegram nos ayudamos entre desarrolladores para testear apps y compartir scripts de automatización.