wandres.dev
CI/CD · automatizar el ciclo

Publicar en cada merge: la API de edición, Fastlane y el canal interno

Subir un artefacto a la tienda no es una llamada sino una transacción con varias fases que hay que abrir, poblar y confirmar, y entender ese modelo transaccional explica la mayoría de los fallos que la gente atribuye a la herramienta. Esta lección desarrolla la API de edición del portal de desarrollo, el papel de la cuenta de servicio y sus permisos mínimos, la disciplina del número de versión monótono, la diferencia entre canales y despliegue por fases, la automatización con Fastlane frente a la invocación directa y qué significa realmente publicar en cada fusión a la rama principal.

⏱ 22 min

La publicación automática es la frontera donde la integración continua se convierte en entrega continua, y es también donde más equipos se detienen porque la palabra publicar les suena a irreversible. Conviene deshacer esa asociación desde el principio: publicar en un canal de prueba cerrado no es enviar nada a los usuarios, es poner el artefacto donde un grupo definido de personas puede instalarlo con un toque en lugar de descargando un fichero de un chat. La diferencia entre un equipo que hace esto y uno que no se mide en horas de fricción por semana, pero también en algo más difícil de cuantificar: en el primero, cada fusión a la rama principal produce algo que existe de verdad, y esa propiedad cambia la manera en que la gente escribe código.

🎯 Al terminar esta lección sabrás
  • Describir el modelo transaccional de la API de edición y por qué sus fallos típicos son de secuencia.
  • Configurar una cuenta de servicio con el permiso mínimo suficiente para subir sin poder publicar.
  • Garantizar la monotonía del número de versión sin depender de que nadie se acuerde.
  • Elegir entre invocación directa y Fastlane según la complejidad real de los metadatos que se gestionan.

El modelo transaccional: la edición como unidad de cambio

La interfaz de programación del portal de desarrollo no expone operaciones sueltas del tipo sube este fichero. Expone un objeto llamado edición que funciona como una transacción: se abre, se le aplican cambios que no tienen ningún efecto observable mientras siga abierta, y se confirma en un único acto que los hace efectivos todos a la vez o ninguno. Ese diseño existe porque una publicación real no consiste solo en un binario, sino en un binario más un canal más unas notas de versión más un porcentaje de despliegue, y liberar esas piezas por separado dejaría estados intermedios incoherentes visibles para los usuarios.

Comprender esto resuelve de golpe una familia entera de errores desconcertantes. Los mensajes que hablan de una edición que ya no existe suelen significar que el proceso tardó demasiado entre la apertura y la confirmación, porque las ediciones caducan. Los que hablan de un conflicto suelen significar que hay dos ediciones abiertas a la vez, típicamente porque una ejecución anterior falló a mitad y nadie la cerró, o porque alguien tiene la consola web abierta con cambios sin guardar. Y los que informan de que el paquete no puede procesarse casi nunca se refieren al paquete, sino a que el número de versión ya fue usado en algún canal, incluido uno que el equipo había olvidado.

La secuencia canónica tiene cuatro pasos y el orden importa. Se abre la edición; se sube el artefacto, que queda asociado a la edición pero sin canal; se asigna ese artefacto a una pista concreta declarando su estado y, si procede, su fracción de despliegue; y se confirma. Un quinto paso opcional, a menudo el más útil, consiste en indicar que los cambios no se envían a revisión, lo que permite acumular varias subidas y decidir después.

📝

Edición

Transacción con caducidad. Todo cambio es invisible hasta la confirmación y se descarta si expira.

🎚️

Canal

Interno, cerrado, abierto o producción. Determina quién puede instalar y cuánta revisión hay antes.

📈

Despliegue por fases

Una fracción de la base de usuarios recibe la versión. Se amplía o se detiene sin publicar nada nuevo.

🔢

Número de versión

Entero estrictamente creciente y jamás reutilizable. Es la única clave que la tienda no perdona.

La cuenta de servicio y el privilegio mínimo

La automatización se autentica con una cuenta de servicio del proveedor de nube, vinculada al portal de desarrollo y con una clave en formato de texto estructurado que se guarda como secreto igual que cualquier otra credencial. Aquí aparece la primera decisión de diseño que casi nadie toma conscientemente: qué permisos concederle. La respuesta cómoda es concederle administración completa; la respuesta correcta es concederle exactamente la capacidad de gestionar publicaciones en las pistas de prueba y nada más.

La diferencia es material. Una cuenta que solo puede subir a la pista interna no puede publicar en producción aunque alguien modifique el flujo, no puede alterar la ficha de la tienda, no puede cambiar precios ni responder reseñas y no puede retirar la aplicación. Como esa credencial vive en un almacén de secretos al que tienen acceso todos los flujos del repositorio, limitar su alcance es la única defensa real contra un error de configuración o contra una acción de terceros comprometida.

Conviene también separar credenciales por propósito cuando la organización lo permite: una cuenta para subir a pistas internas desde el pipeline automático y otra, con permisos mayores, reservada a un flujo manual que exige aprobación humana explícita. Es el mismo principio que separa la clave de subida de la clave de firma de aplicación, aplicado al plano de la distribución.

- name: Restaurar la credencial de la cuenta de servicio
  run: |
    printf '%s' "${{ secrets.PLAY_SERVICE_ACCOUNT_JSON }}" > "$RUNNER_TEMP/play.json"
    echo "PLAY_JSON=$RUNNER_TEMP/play.json" >> "$GITHUB_ENV"

- name: Subir a la pista interna
  run: bundle exec fastlane interna
  env:
    SUPPLY_JSON_KEY: ${{ env.PLAY_JSON }}

- name: Limpiar la credencial
  if: always()
  run: rm -f "$RUNNER_TEMP/play.json"
⚠️
El número de versión es la única decisión irreversible barata

La tienda rechaza cualquier artefacto cuyo número de versión ya se haya usado en cualquier canal, incluso si esa subida se descartó después. Un flujo que deriva ese número de un contador manual acabará, con certeza matemática, produciendo colisiones el día en que dos ramas se fusionen cerca en el tiempo. La solución robusta es derivarlo de algo estrictamente creciente y ajeno a la voluntad humana: el número de ejecución del flujo, o el recuento de commits de la rama principal, o un sello temporal en minutos desde una fecha base. Y conviene reservar espacio en el rango desde el principio, porque el valor es un entero con techo y quemar miles de números al mes con un sello temporal por segundos es una forma silenciosa de quedarse sin espacio en unos años.

Fastlane frente a la invocación directa

Existen dos caminos y la elección depende menos de la elegancia que del alcance de lo que se automatiza. Invocar la interfaz de programación directamente, con la biblioteca cliente del proveedor o con peticiones a mano desde un script, es perfectamente razonable cuando lo único que se hace es subir un paquete a una pista interna y confirmar. Son unas pocas llamadas, sin dependencias ajenas al proyecto, con control total sobre el manejo de errores y sin un intérprete adicional que instalar y mantener en el runner.

Fastlane compensa cuando el alcance crece. Su acción de aprovisionamiento conoce el modelo transaccional completo, gestiona la caducidad y los conflictos de edición, y añade lo que resulta tedioso hacer a mano: sincronizar notas de versión desde una estructura de directorios por idioma, subir capturas de pantalla y textos de la ficha, gestionar el mapa de símbolos para desofuscar informes de fallo, y expresar despliegues por fases con una sintaxis legible. Su coste es una dependencia en ruby que hay que fijar por versión y cachear, y una capa de abstracción más entre tu intención y el error que devuelve el servidor.

La regla práctica es reconocible: si tus notas de versión viven en el repositorio y cambian con cada publicación, Fastlane te ahorra trabajo real. Si solo subes binarios a una pista interna y las notas las escribe alguien en la consola cuando toca promover, la invocación directa es más simple y más fácil de depurar.

# fastlane/Fastfile
default_platform(:android)

platform :android do
  desc "Compilar y subir a la pista interna sin enviar a revision"
  lane :interna do
    gradle(task: "bundle", build_type: "Release")
    upload_to_play_store(
      track: "internal",
      release_status: "draft",
      changes_not_sent_for_review: true,
      skip_upload_apk: true,
      skip_upload_metadata: true,
      skip_upload_images: true,
      skip_upload_screenshots: true
    )
  end

  desc "Promover lo ya subido a la pista cerrada con despliegue parcial"
  lane :promover do
    upload_to_play_store(
      track: "internal",
      track_promote_to: "beta",
      rollout: "0.1",
      skip_upload_aab: true
    )
  end
end
flowchart TD
A[Merge en la rama principal] --> B[Calcular numero de version desde el contador]
B --> C[Compilar el paquete de aplicacion firmado]
C --> D[Abrir edicion en el portal]
D --> E[Subir el artefacto a la edicion]
E --> F[Asignar a la pista interna]
F --> G[Confirmar la edicion]
G --> H[Disponible para el equipo en minutos]
H --> I{Se promociona}
I -- Si --> J[Pista cerrada con despliegue al diez por ciento]
I -- No --> K[Se descarta y se espera al siguiente merge]
J --> L[Ampliar o detener segun metricas]
style G fill:#a6e3a1,color:#11111b
style L fill:#f9e2af,color:#11111b

Publicar en cada fusión: qué significa de verdad

La formulación publicar en cada merge se malinterpreta con facilidad. No significa que cada fusión llegue a los usuarios, sino que cada fusión produce un artefacto real, firmado, instalable y localizable, disponible para un grupo interno en cuestión de minutos. La promoción a canales mayores sigue siendo una decisión, tomada por personas, sobre un artefacto que ya existe y que ya ha sido usado por alguien. Lo que se automatiza es la producción; lo que se conserva humano es la distribución.

El beneficio operativo más evidente es que la pregunta funciona esto en un dispositivo real deja de requerir que nadie compile nada. El menos evidente, y más valioso, es que separa la construcción de la decisión de publicar. Un equipo que compila solo cuando decide publicar descubre los problemas de la variante de publicación en el peor momento posible, con la fecha encima; un equipo que la construye en cada fusión los descubre el mismo día en que se introducen, cuando el cambio culpable ocupa una sola pantalla.

Hay dos disciplinas que hacen sostenible el esquema. La primera es la idempotencia: el flujo debe poder ejecutarse dos veces sobre el mismo commit sin dejar estados a medias, lo que implica cerrar o descartar ediciones huérfanas antes de abrir una nueva y tolerar el caso de un número de versión ya subido sin marcar la ejecución como fallida por ello. La segunda es la trazabilidad: cada artefacto debe llevar en sus notas internas el identificador exacto del commit que lo produjo, para que un informe de fallo de un probador se pueda convertir en un diagnóstico sin arqueología.

# Numero de version derivado de algo estrictamente creciente
VERSION_CODE=$(git rev-list --count HEAD)
./gradlew bundleRelease -PversionCode="$VERSION_CODE" -PgitSha="$(git rev-parse --short HEAD)"

# Detener un despliegue por fases sin publicar nada nuevo
bundle exec fastlane run upload_to_play_store track:production rollout:0 skip_upload_aab:true
ℹ️
El canal interno no pasa por revisión, los demás sí

La pista interna entrega en minutos porque no atraviesa el proceso de revisión completo; las pistas cerradas, abiertas y de producción sí, con tiempos que varían de horas a días y que aumentan cuando la aplicación declara permisos sensibles o cambia su público objetivo. Diseñar el flujo suponiendo que la promoción es instantánea produce planes de publicación que se rompen el primer viernes. La consecuencia práctica es que el despliegue por fases debe iniciarse con margen y que detenerlo, que sí es inmediato, es la única palanca verdaderamente rápida que tienes ante un incidente.

Automatizar la publicación cambia qué es un cambio

Hay un efecto de segundo orden en la entrega continua que rara vez se enuncia y que es más importante que todo el ahorro de tiempo junto: cuando publicar deja de costar esfuerzo, el tamaño óptimo de un cambio se desploma. En un equipo donde cada publicación exige una tarde de trabajo manual, la economía empuja en una sola dirección, hacia acumular. Se agrupan diez funcionalidades porque el coste fijo de publicar hay que amortizarlo, y esa acumulación produce exactamente las condiciones en que los fallos son difíciles de diagnosticar: muchos cambios simultáneos, mucho tiempo transcurrido desde que se escribió cada uno, un artefacto que nadie ha usado antes de que lo usen todos. Cuando el coste marginal de publicar tiende a cero, la economía se invierte y lo racional pasa a ser publicar lo más pequeño posible, porque un artefacto que contiene un solo cambio convierte cualquier regresión en un diagnóstico trivial. Esto es una transformación de la práctica de ingeniería disfrazada de mejora de infraestructura. Modifica cómo se descompone el trabajo, cómo se diseñan las funcionalidades para poder llegar apagadas al usuario, cómo se negocia con quien define el producto y hasta qué significa terminar algo. Los equipos que instalan la automatización pero conservan el ritmo antiguo obtienen una fracción minúscula del beneficio, porque siguen agrupando por costumbre un coste que ya no existe. Y hay una consecuencia final que suele sorprender: la fiabilidad de la publicación deja de ser un atributo del proceso y pasa a ser un atributo del sistema. Cuando un despliegue es un evento raro y manual, su éxito depende de que la persona correcta recuerde los pasos correctos; cuando ocurre veinte veces por semana, cualquier fragilidad se manifiesta enseguida y se corrige, y el resultado es una máquina de publicar en la que se puede confiar precisamente porque se usa constantemente.

⚔️ Convierte cada fusión en un artefacto real
  1. Crea una cuenta de servicio con permiso exclusivo sobre pistas de prueba y comprueba, intentándolo, que no puede publicar en producción.
  2. Sustituye tu número de versión manual por uno derivado del recuento de commits y verifica que crece de forma estricta en tres fusiones seguidas.
  3. Automatiza la subida a la pista interna en cada fusión a la rama principal e incluye el identificador del commit en las notas internas.
  4. Ejecuta el mismo flujo dos veces sobre el mismo commit y arregla todo lo que quede en un estado intermedio.
  5. Practica un despliegue por fases al uno por ciento y deténlo, midiendo cuánto tarda de verdad la detención en surtir efecto.