Cómo automatizar la compilación y subida de las apps al PlayStore

🏠 Curso Android Gratis > Lección 18 de 18
Módulo 6 · Nivel Práctico

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.

⚡ Ventajas de la automatización desatendida:
  • 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):

  1. Entra en Google Cloud Console (console.cloud.google.com) en el proyecto vinculado a tu cuenta de desarrollador.
  2. Ve a IAM y administración > Cuentas de servicio y pulsa en Crear cuenta de servicio. Nómbrala, por ejemplo, play-publisher.
  3. En la pestaña Claves de esa cuenta de servicio, haz clic en Agregar clave > Crear clave nueva > Formato JSON. Guarda ese archivo .json en un lugar seguro de tu ordenador.
  4. Abre Google Play Console, dirígete a Acceso a la API (en Configuración de la cuenta).
  5. 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.

Víctor Alonso - Desarrollador Android Senior

Creado y tutorizado por Víctor Alonso

Ingeniero en Informática · Desarrollador Android Senior & Fundador de Walkiria Apps

Especialista en desarrollo nativo para Android (Kotlin, Jetpack Compose, Arquitectura MVVM/Clean) con apps en Google Play que superan cientos de miles de descargas. Formador y divulgador técnico.

Deja un comentario