La lección anterior terminó con una idea incómoda: el frontend es un cliente antiguo al que no puedes obligar a actualizarse. Ahí la consecuencia era menor —basta con que el usuario recargue la página— y el rollback costaba menos de un minuto. Ahora vamos al extremo de esa misma propiedad. Reservalia Pro es la aplicación móvil React Native con la que el profesional gestiona su agenda desde el móvil: mira los huecos del día entre cliente y cliente, confirma o cancela citas y cobra. La usan 190 de los 340 negocios de pago, y su pipeline rompe cuatro reglas que llevamos siete módulos dando por supuestas. El usuario decide cuándo actualiza, y muchos no lo hacen nunca. Una tienda revisa cada versión durante horas o días antes de publicarla, así que entre el merge y el usuario hay un tercero que no controlas. No puedes hacer rollback de una versión ya publicada: lo único que existe es publicar otra. Y conviven a la vez decenas de versiones de cliente contra la misma API, no dos durante cinco minutos. En esta lección veremos qué partes del pipeline sobreviven intactas, qué hay que reinventar —firma de código, distribución escalonada, actualizaciones over-the-air, compatibilidad de API— y cómo se leen las métricas DORA cuando "desplegar" deja de significar "que llegue al usuario".

Contenido

  1. Qué tiene este contexto que la web no tenía
  2. Reservalia Pro: producto, repositorio y lo que se reutiliza
  3. Firma de código y gestión de credenciales
  4. El pipeline de CI móvil
  5. Versionado y numeración automática
  6. Canales de distribución y despliegue escalonado
  7. El Fastfile de release, comentado
  8. Actualizaciones over-the-air y sus límites
  9. Compatibilidad con clientes viejos
  10. Crash reporting como bucle de feedback
  11. Las métricas DORA cuando desplegar no es llegar
  12. Ficha del caso
  13. Errores Comunes y Consejos
  14. Ejercicios
  15. Conclusión

  1. Qué tiene este contexto que la web no tenía

apps/web (05-01) Reservalia Pro (móvil)
Quién decide la actualización El servidor: al recargar ya está El usuario, o su configuración de auto-update
Intermediarios Ninguno App Store y Google Play, con revisión humana
Tiempo merge → usuario 12 minutos De 4 horas a 5 días, y luego semanas de adopción
Rollback Repuntar index.html: <1 min No existe: solo publicar una versión nueva
Versiones de cliente vivas 1 ó 2 Decenas, algunas de hace un año
Artefacto Carpeta de ficheros APK/AAB e IPA firmados con material criptográfico
Coste del runner Linux, barato macOS para iOS, ~10× el precio por minuto
Señal de fallo Errores 5xx en tu servidor Informes de crash de dispositivos ajenos

La fila de la firma es la que introduce el problema más operativo y la que más equipos bloquea, así que va primero. La fila del rollback es la que más cambia el diseño: si no puedes deshacer, tienes que poder detener, y de ahí sale el despliegue escalonado del apartado 6. Y la de las versiones vivas es la que obliga a llevar la disciplina de la 04-06 al contrato de la API.

Lo que no cambia conviene decirlo con la misma claridad, porque es la mayor parte: control de versiones y trunk-based (02-07), npm ci con lockfile (04-02), análisis estático (02-05), artefacto inmutable construido una vez y promocionado (02-06), secretos por referencia y mínimo privilegio (04-03), pipeline as code con acciones reutilizables (04-05). Un pipeline móvil no es otro mundo: es el mismo con tres piezas nuevas y una restricción dura.

  1. Reservalia Pro: producto, repositorio y lo que se reutiliza

Reservalia Pro vive en un repositorio propio, reservalia/pro-movil, no en el monorepo. La razón es puramente práctica y merece la pena entenderla porque se repite en la lección siguiente: la app consume packages/compartido —los tipos y la lógica de disponibilidad— pero su ciclo de construcción no tiene nada que ver con el de la API (herramientas nativas, runners macOS, builds de 18 minutos). Mantenerla en el monorepo obligaría a que cada PR de la API arrastrase la configuración móvil, y el paths de la 02-07 se volvería frágil. La conexión se hace publicando @reservalia/compartido como paquete versionado en el registro privado de la 04-02.

pro-movil/
  src/                      # React Native (TypeScript)
  android/                  # proyecto Gradle
  ios/                      # proyecto Xcode
  fastlane/
    Fastfile                # las "lanes": beta, release, screenshots
    Appfile                 # identificadores de app y cuentas
    Matchfile               # configuración de match (certificados)
  .github/workflows/
    ci-movil.yml            # en cada PR
    release-movil.yml       # en cada tag v*

Dos flujos, igual que en el monorepo: uno rápido en cada PR y otro de publicación disparado por tag. La diferencia con ci.yml/cd.yml es que aquí el segundo no termina en producción, termina en la puerta de una tienda.

  1. Firma de código y gestión de credenciales

Todo binario móvil va firmado, y la firma no es un detalle de seguridad opcional: sin ella el sistema operativo no instala nada. Las dos plataformas resuelven lo mismo de forma distinta:

Android iOS
Material Un keystore (.jks) con una clave y su contraseña Certificado de distribución + perfil de aprovisionamiento
Quién lo emite Tú mismo Apple, ligado a tu cuenta de desarrollador
Caduca No (pero perderlo es fatal) Sí: certificado a 1 año, perfil a 1 año
Si lo pierdes No puedes publicar actualizaciones de esa app nunca más (salvo con firma gestionada por Play) Se regenera; molesto pero recuperable
Rotación Prácticamente nunca Anual, y siempre pilla por sorpresa

La primera fila de "si lo pierdes" es la que hay que interiorizar: el keystore de Android es el activo más crítico del proyecto. Reservalia tiene una copia en el gestor de secretos de la empresa, otra cifrada fuera de línea, y usa además la firma gestionada por Google Play como red de seguridad. Es una decisión de negocio, no técnica.

Fastlane match resuelve el problema de iOS de una forma elegante: guarda los certificados y perfiles cifrados en un repositorio git privado, y cada máquina —el portátil de Nuria o el runner de CI— los descarga y descifra con una única contraseña. En vez de N máquinas con material distinto, hay una fuente de verdad versionada.

# fastlane/Matchfile
git_url("[email protected]:reservalia/certificados-ios.git")   # 1 · repo privado y separado
storage_mode("git")
type("appstore")                                            # 2
readonly(true)                                              # 3
  1. Un repositorio aparte del código: quien contribuye a la app no necesita acceso al material de firma.
  2. En CI solo se usa el tipo appstore; los perfiles de desarrollo se quedan en los portátiles.
  3. readonly(true) en CI es fundamental: el runner descarga lo que hay, nunca regenera certificados. Un CI con permiso de escritura puede revocar los certificados del equipo entero en un job mal escrito, y ese incidente deja a todo el mundo sin poder publicar.

En el workflow, el material llega por secretos y nunca toca el repositorio en claro:

  construir-ios:
    runs-on: macos-14                                   # 1
    timeout-minutes: 45
    steps:
      - uses: actions/checkout@v4
      - uses: ./.github/actions/preparar-node           # reutilizada de 04-05

      - name: Preparar llavero temporal                 # 2
        run: |
          security create-keychain -p "$KEYCHAIN_PASS" build.keychain
          security default-keychain -s build.keychain
          security unlock-keychain -p "$KEYCHAIN_PASS" build.keychain
        env:
          KEYCHAIN_PASS: ${{ secrets.KEYCHAIN_PASS }}

      - name: Descargar certificados con match
        run: bundle exec fastlane match appstore --readonly
        env:
          MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}          # 3
          MATCH_GIT_BASIC_AUTHORIZATION: ${{ secrets.MATCH_GIT_TOKEN }}

      - name: Compilar y subir a TestFlight
        run: bundle exec fastlane beta
        env:
          APP_STORE_CONNECT_API_KEY: ${{ secrets.ASC_API_KEY }}  # 4
  1. macos-14 es obligatorio para compilar iOS: Xcode solo existe en macOS. En GitHub Actions, un minuto de macOS cuesta diez veces un minuto de Linux, así que este job es el que domina la factura de CI de la app. De ahí la política del apartado 4: los builds de iOS no se ejecutan en cada PR.
  2. Un llavero temporal por job, creado y destruido en la misma ejecución. En un runner alojado esto es casi ceremonial porque la máquina se destruye; en un runner autoalojado (02-02) es imprescindible, porque si no los certificados quedan en la máquina para el siguiente job, que puede ser de otro repositorio.
  3. MATCH_PASSWORD es la llave maestra de todo el material de firma. Vive en el gestor de secretos del entorno de release (03-02), con revisores, y no en los secretos del repositorio accesibles a cualquier workflow.
  4. Autenticación por clave de API de App Store Connect, no con usuario y contraseña de un humano. Es el equivalente móvil del OIDC de la 03-02: credencial de servicio, con permisos acotados y revocable sin afectar a ninguna persona.

  1. El pipeline de CI móvil

En cada pull request, y con el coste del runner macOS muy presente:

name: ci-movil
on: { pull_request: { branches: [main] } }
concurrency: { group: ci-movil-${{ github.ref }}, cancel-in-progress: true }

jobs:
  estatico:                              # 1 · Linux, 2 min
    runs-on: ubuntu-22.04
    steps:
      - uses: actions/checkout@v4
      - uses: ./.github/actions/preparar-node
      - run: npm run lint && npx tsc --noEmit
      - run: npm test -- --coverage

  android-debug:                         # 2 · Linux, 7 min
    runs-on: ubuntu-22.04
    needs: [estatico]
    steps:
      - uses: actions/checkout@v4
      - uses: ./.github/actions/preparar-node
      - uses: actions/setup-java@v4
        with: { distribution: temurin, java-version: '17', cache: gradle }
      - run: cd android && ./gradlew assembleDebug           # 3 · sin firma de release
      - uses: actions/upload-artifact@v4
        with: { name: apk-debug-${{ github.sha }}, path: android/app/build/outputs/apk/debug/*.apk }

  instrumentados:                        # 4 · emulador, 11 min
    runs-on: ubuntu-22.04
    needs: [android-debug]
    steps:
      - uses: actions/checkout@v4
      - uses: reactivecircus/android-emulator-runner@v2
        with:
          api-level: 33
          script: cd android && ./gradlew connectedDebugAndroidTest

  ios-debug:                             # 5 · macOS, solo bajo condición
    if: contains(github.event.pull_request.labels.*.name, 'ios') || github.event.pull_request.base.ref == 'release'
    runs-on: macos-14
    needs: [estatico]
    steps:
      - uses: actions/checkout@v4
      - run: xcodebuild -workspace ios/Pro.xcworkspace -scheme Pro -sdk iphonesimulator build
  1. El job barato va primero y bloquea a los demás. Es el fail fast de la 04-01, y aquí tiene un beneficio económico directo: un error de tipos detectado en dos minutos de Linux evita gastar cuarenta de macOS.
  2. El APK de debug se construye en cada PR y se sube como artefacto. Nuria lo instala en un dispositivo real para revisar el cambio: es el equivalente móvil del entorno de previsualización por PR de la 02-07. Un comentario automático con un enlace de descarga y un QR completa el circuito.
  3. assembleDebug usa la firma de depuración, que no necesita el keystore de release. Eso permite que este job corra en cualquier PR, incluso de una rama de un colaborador externo, sin exponer material sensible. Es la misma lógica de mínimo privilegio de la 04-03.
  4. Las pruebas instrumentadas se ejecutan en un emulador dentro del runner. Son lentas y más frágiles que las unitarias, así que la pirámide de la 02-04 se aplica con severidad: unas ocho, sobre los flujos que dan dinero. Las inestables van a cuarentena con la misma política de siempre.
  5. iOS solo se compila bajo demanda, mediante una etiqueta ios en el PR o si el PR va contra la rama de release. La justificación es de coste: compilar iOS en los 60 PR mensuales de Reservalia costaría unas 25 horas de macOS; con esta condición se queda en cinco. El riesgo asumido es real —un cambio que rompe solo iOS se detecta más tarde— y se acota fijando una compilación nocturna programada.

Diego: "¿Cuánto nos cuesta esto al mes?" La respuesta, con números: 60 PR × 20 min de Linux, más 12 builds de iOS × 40 min de macOS. Alrededor de 20 h de Linux y 8 h de macOS, y la mitad de la factura viene de las 8 horas. Sin la condición del punto 5, serían tres veces más.

  1. Versionado y numeración automática

Tres números conviven y confundirlos causa releases rechazadas:

Concepto Android iOS Para qué sirve
Versión visible versionName (1.14.0) CFBundleShortVersionString Lo que ve el usuario; SemVer de la 02-06
Número interno versionCode (entero) CFBundleVersion (build) Ordena las subidas; debe crecer siempre
Identidad applicationId bundleIdentifier Identifica la app; no cambia jamás

La regla dura: el número interno tiene que ser estrictamente creciente y no se puede reutilizar. Si subes el build 412 a TestFlight y luego quieres subir otra vez esa misma versión con un arreglo, tiene que ser el 413 aunque el versionName siga siendo 1.14.0. Derivarlo a mano garantiza que algún día alguien lo repita y la subida se rechace tras cuarenta minutos de build.

# fastlane/Fastfile (fragmento)
lane :numerar do
  version = sh("node -p \"require('./package.json').version\"").strip   # 1
  build   = latest_testflight_build_number(version: version) + 1        # 2
  increment_version_number(version_number: version)
  increment_build_number(build_number: build)
  UI.message("Publicando #{version} (build #{build})")
end
  1. La versión visible sale del package.json, que a su vez la fija semantic-release desde los commits convencionales de la 02-06. La app hereda así el versionado del resto del sistema, sin un mecanismo paralelo.
  2. El número de build se consulta a la tienda y se incrementa. Es la única fuente que no se puede desincronizar, mejor que github.run_number: si migras de CI, el contador se reinicia y las subidas empiezan a rechazarse. Preguntar a quien tiene la verdad es más robusto que llevar la cuenta.

  1. Canales de distribución y despliegue escalonado

Aquí está la sustitución conceptual más importante de la lección. El canary de la 03-04 dirigía el 10 % del tráfico a la versión nueva mediante el peso del ALB, y si las métricas empeoraban se retiraba en segundos. En móvil no hay balanceador y no hay retirada: la versión ya está instalada en el dispositivo del usuario. Lo que sí existe es controlar a cuántos se les ofrece.

flowchart LR
    B["Build firmado"] --> I["Interno<br/>el equipo · minutos"]
    I --> A["Alpha / TestFlight<br/>~30 negocios voluntarios"]
    A --> C["Beta abierta<br/>~200 usuarios"]
    C --> R1["Produccion 5%"]
    R1 --> R2["20%"]
    R2 --> R3["50%"]
    R3 --> R4["100%"]
    R1 -.->|"crash-free < 99,5%"| H["Detener despliegue"]
    R2 -.->|"crash-free < 99,5%"| H
    H --> F["Publicar version correctiva"]

Los canales, con lo que aportan y lo que cuestan:

Canal Quién entra Revisión de la tienda Para qué sirve
Interno Hasta 100 correos; el equipo Ninguna o mínima Verificar que el binario firmado arranca
Alpha / TestFlight interno Negocios voluntarios (~30) Ligera Uso real, feedback cualitativo
Beta abierta Quien se apunte (~200) Cobertura de dispositivos y versiones de SO
Producción escalonada Porcentaje creciente Ya pasada Limitar el daño de un fallo

El staged rollout funciona así: publicas al 5 %, y la tienda ofrece la actualización solo a esa fracción de usuarios elegida al azar. Observas 24 horas la tasa de sesiones sin fallo, subes al 20 %, y así hasta el 100 % en cuatro o cinco días. Sus dos diferencias con el canary son las que hay que tener claras:

  • Detener no es revertir. Si al 20 % detectas un fallo, puedes pausar el escalado —nadie más recibirá la versión— pero los que ya la instalaron se quedan con ella. Google Play permite además "halt", que retira la oferta; iOS permite pausar la publicación escalonada. En ningún caso vuelve el binario anterior a los dispositivos.
  • El único arreglo es hacia delante. Es el roll-forward de la 03-05, pero aquí no es una opción entre dos: es la única. Y trae de la mano una revisión de la tienda, que en un caso urgente se puede acelerar solicitando una revisión expedita, sin garantías.

De ahí la conclusión operativa: como no puedes revertir, tienes que detectar antes y con menos usuarios. Un 5 % de 190 negocios son 9 o 10; suficiente para que un fallo de arranque aparezca en el crash reporting en horas, y pequeño para que solo 10 clientes lo sufran.

  1. El Fastfile de release, comentado

# fastlane/Fastfile
default_platform(:android)

platform :android do
  desc "Sube a Play en el canal indicado y con rollout parcial"
  lane :publicar do |options|
    numerar                                                    # 1

    gradle(task: "clean bundleRelease",                        # 2
           properties: {
             "android.injected.signing.store.file"     => ENV["KEYSTORE_PATH"],
             "android.injected.signing.store.password" => ENV["KEYSTORE_PASS"],
             "android.injected.signing.key.alias"      => ENV["KEY_ALIAS"],
             "android.injected.signing.key.password"   => ENV["KEY_PASS"],
           })

    upload_to_play_store(
      track: options[:track] || "internal",                    # 3
      rollout: options[:rollout] || "0.05",                    # 4
      release_status: "inProgress",
      skip_upload_screenshots: true,
      skip_upload_images: true,
      mapping_paths: ["android/app/build/outputs/mapping/release/mapping.txt"]  # 5
    )
  end
end
  1. La numeración se resuelve primero, para que un rechazo por número repetido ocurra antes de compilar y no después.
  2. bundleRelease genera un AAB, no un APK. Google Play lo usa para generar APK optimizados por dispositivo, con lo que la descarga del usuario baja bastante. Las credenciales de firma entran por variables de entorno que el workflow rellena desde secretos: nunca aparecen en build.gradle, que está versionado.
  3. El canal es un parámetro, no un valor fijo. El mismo lane sirve para interno, beta y producción; lo que cambia es cómo se invoca. Es la misma idea del workflow reutilizable de la 04-05.
  4. rollout: 0.05 arranca en el 5 %. Los incrementos posteriores no vuelven a subir el binario: se hacen desde la consola o con un lane que solo cambia el porcentaje. El artefacto es el mismo —construir una vez, desplegar muchas veces (02-06)—; lo que se mueve es la exposición.
  5. El fichero de mapping se sube con el release. Sin él, los informes de crash del apartado 10 llegan ofuscados y son inútiles. Es el paso que más se olvida y el que más caro sale a las tres de la madrugada.

En el workflow, esto se dispara con un tag y con aprobación humana:

  publicar-android:
    if: startsWith(github.ref, 'refs/tags/v')
    environment: tiendas                       # revisores obligatorios, como en 03-02
    runs-on: ubuntu-22.04
    steps:
      - uses: actions/checkout@v4
      - run: bundle exec fastlane android publicar track:production rollout:0.05
        env:
          KEYSTORE_PATH: ${{ runner.temp }}/release.jks
          KEYSTORE_PASS: ${{ secrets.KEYSTORE_PASS }}
          SUPPLY_JSON_KEY_DATA: ${{ secrets.PLAY_SERVICE_ACCOUNT }}

  1. Actualizaciones over-the-air y sus límites

Como React Native ejecuta un bundle de JavaScript sobre una carcasa nativa, existe la posibilidad de sustituir ese bundle sin pasar por la tienda: la app descarga la versión nueva al arrancar y la aplica en el siguiente inicio. Es lo que hacen CodePush o EAS Update, y devuelve algo parecido al despliegue continuo que teníamos en web.

Actualización OTA Release por tienda
Qué puede cambiar Solo JavaScript y recursos Cualquier cosa, incluido código nativo
Tiempo hasta el usuario Minutos u horas Horas o días + adopción
Revisión Ninguna
Reversión : volver al bundle anterior No
Riesgo Un bundle roto deja la app inservible Menor, hay revisión de por medio

Los límites hay que decirlos sin adornos. Técnicos: si el cambio toca una dependencia nativa, cambia permisos o actualiza la versión de React Native, la OTA no sirve y hay que pasar por la tienda; y el bundle OTA tiene que ser compatible con la carcasa nativa que ya está instalada, lo que obliga a atarlo a una versión de app concreta. De política de tienda: las reglas de Apple permiten actualizar código interpretado siempre que no cambie el propósito principal de la app ni introduzca funcionalidad sustancialmente distinta de la revisada; usar OTA para saltarse la revisión es motivo de retirada. La lectura práctica de Reservalia:

  • Sí OTA para correcciones urgentes de JavaScript, textos, ajustes de estilo y activar o desactivar un feature flag (03-05) que ya venía en el binario.
  • No OTA para funcionalidad nueva visible, cambios de permisos, dependencias nativas ni nada que altere lo que la tienda revisó.
  • Siempre escalonado también en OTA, con la misma disciplina de porcentajes, porque un bundle roto distribuido al 100 % en diez minutos es peor que cualquier fallo de tienda.

Marta: "La OTA es el extintor, no el ascensor." Se usa para apagar un fuego, no como vía habitual de entrega. Un equipo que entrega todo por OTA acaba con una versión de tienda desactualizada y una diferencia creciente entre lo que revisaron y lo que ejecuta el usuario.

  1. Compatibilidad con clientes viejos

Este es el equivalente móvil de expand and contract (04-06), y es la parte que afecta al equipo de la API aunque no toque la app. Los datos de Reservalia, tomados a los treinta días de una release:

Versión de Pro Usuarios activos
1.14.x (última) 61 %
1.13.x 22 %
1.12.x 9 %
1.9.x – 1.11.x 6 %
Anteriores a 1.9 2 %

El 39 % de los usuarios ejecuta código de hace meses. Eso convierte cualquier cambio de la API en un cambio de contrato con decenas de clientes que no puedes actualizar. Cuatro tácticas, en orden de preferencia:

(1) Cambios aditivos y compatibles hacia atrás, siempre que sea posible. Un campo nuevo en la respuesta no rompe a nadie; renombrar uno, sí. Es literalmente el patrón de la 04-06 aplicado al JSON en lugar de al esquema: se añade el campo nuevo, ambos conviven durante meses, y el viejo se retira cuando el uso baja a cero.

(2) Versionado explícito de la API cuando el cambio no puede ser aditivo: /v1/citas y /v2/citas conviviendo, con una política escrita de cuánto tiempo se mantiene cada versión —en Reservalia, doce meses desde que se publica la siguiente—.

(3) Pruebas de contrato en el pipeline de la API. La app publica su expectativa y la API verifica en CI que no la rompe. Es la técnica que la lección siguiente desarrolla a fondo para microservicios; aquí basta con el mecanismo: un conjunto de contratos versionados por versión de app viva, ejecutados como pruebas de la API.

(4) Versión mínima soportada, como último recurso. La API responde con un código específico y la app muestra una pantalla bloqueante que invita a actualizar:

// apps/api/src/plugins/version-cliente.ts
const MINIMA = '1.9.0';

app.addHook('onRequest', async (req, reply) => {
  const v = req.headers['x-app-version'] as string | undefined;   // 1
  if (!v) return;                                                 // 2
  if (compararSemver(v, MINIMA) < 0) {
    return reply.code(426).send({                                 // 3
      error: 'version_no_soportada',
      minima: MINIMA,
      mensaje: 'Actualiza Reservalia Pro para seguir usando la aplicación.',
    });
  }
});
  1. La app envía su versión en cada petición. Sin esa cabecera no hay forma de saber quién te está llamando, y es el dato que alimenta la tabla de arriba.
  2. Si no viene la cabecera, no se bloquea: puede ser la web, un script interno o una versión antiquísima. Bloquear por ausencia rompe integraciones legítimas.
  3. 426 Upgrade Required con un cuerpo que la app sabe interpretar. La pantalla bloqueante debe existir antes de que se necesite: si la versión 1.8 no sabe manejar el 426, forzarla solo produce errores incomprensibles. Esa es la trampa clásica de esta técnica, y la razón de que el manejo del 426 se implemente pronto aunque no se use en años.

Forzar la actualización tiene un coste de negocio: un profesional en mitad de la jornada al que le bloqueas la agenda no está contento. Se reserva para motivos serios —una vulnerabilidad, un cambio incompatible inevitable— y se avisa antes con una pantalla no bloqueante durante semanas.

  1. Crash reporting como bucle de feedback

En la 03-06 el bucle de retroalimentación era observabilidad del servidor: métricas, logs y trazas de máquinas propias. Aquí el código corre en dispositivos ajenos, y la señal equivalente son los informes de fallo. La métrica que gobierna es la tasa de sesiones sin fallo:

Métrica Objetivo en Reservalia Pro Qué decide
Sesiones sin fallo ≥ 99,5 % Si el escalado sigue o se detiene
Usuarios sin fallo ≥ 99,8 % Si un fallo es masivo o afecta a un dispositivo raro
ANR (Android) < 0,3 % Bloqueos de interfaz que el usuario vive como "se ha colgado"
Errores 4xx/5xx por versión Correlaciona un fallo con un cambio de la API

Dos requisitos para que esto funcione. Primero, subir los símbolos en el release —el mapping.txt de Android y los dSYM de iOS—, o los informes llegan sin nombres de función. Segundo, marcar la versión y el escalón: cada informe lleva versionName, build y el porcentaje de rollout activo, para poder responder a "¿esto lo trajo la 1.14 o ya pasaba antes?". Es el equivalente de las marcas de despliegue en el panel de la 03-06.

La automatización que Reservalia sí tiene: un job programado consulta cada hora la tasa de sesiones sin fallo de la versión en escalado y, si baja del umbral, pausa el rollout automáticamente y avisa por Slack. Es el descendiente directo del rollback automático por métricas de la 03-05, con la diferencia de que aquí no revierte nada: detiene la expansión.

  1. Las métricas DORA cuando desplegar no es llegar

Aplicar las definiciones de la 01-05 sin traducirlas da resultados absurdos: "despliegue" pasaría a significar "subir a la tienda" y el lead time diría cinco días sin distinguir dónde se va el tiempo. La traducción que Reservalia usa:

Métrica Definición en la API Definición en móvil Valor
Frecuencia de despliegue Despliegues a prod Releases publicadas en tienda 1 cada 2 semanas
(auxiliar) builds a interno/beta 8 por semana
Lead time Commit → prod Commit → disponible en tienda 3,5 días
(auxiliar) commit → beta 4 horas
Change failure rate Despliegues con incidente Releases con caída de sesiones sin fallo o corrección urgente 12 %
Time to restore Rollback: 9 min Corrección publicada y adoptada al 50 % 2,5 días

Tres lecturas. La primera: el time to restore de 2,5 días no es un fracaso del equipo, es una propiedad del canal de distribución, y por eso el esfuerzo se desplaza de "recuperar rápido" a "no publicar el fallo" —más beta, escalado más lento al principio, feature flags para desactivar sin publicar—. La segunda: las métricas auxiliares son las que el equipo puede mejorar de verdad; si commit→beta son cuatro horas, el pipeline funciona, aunque la tienda añada tres días encima. La tercera: el change failure rate del 12 % es tres veces el 3,8 % de la API, y tiene una explicación estructural —cada release acumula dos semanas de cambios, frente a los despliegues pequeños y frecuentes de la API—. La lección de la 01-02 se confirma al revés: lotes grandes, más fallos. Y como aquí el lote no se puede reducir a voluntad, se compensa con la beta y el escalado.

  1. Ficha del caso

Contexto React Native, repositorio propio, 190 negocios, iOS + Android, publicación quincenal
Qué sigue valiendo tal cual Trunk-based (02-07), lockfile (04-02), estático (02-05), artefacto único (02-06), secretos y mínimo privilegio (04-03), acciones reutilizables (04-05)
Decisión 1 Firma con match en repositorio separado, readonly en CI y llavero temporal por job
Decisión 2 iOS solo se compila con etiqueta o rama de release, más build nocturno; ahorro ~20 h de macOS/mes
Decisión 3 Número de build consultado a la tienda, versión visible heredada de semantic-release
Decisión 4 Escalado 5 → 20 → 50 → 100 % con pausa automática si las sesiones sin fallo bajan de 99,5 %
Decisión 5 OTA solo para correcciones de JS y flags; nunca para funcionalidad nueva
Decisión 6 Cabecera x-app-version, contratos por versión viva y 426 implementado años antes de usarlo
Efecto en DORA Lead time real 3,5 días (auxiliar a beta: 4 h); CFR 12 %; restore 2,5 días
Qué te llevas a cualquier proyecto Cuando no puedes revertir, la inversión se mueve a detectar antes y a exponer a menos gente

Errores Comunes y Consejos

Error 1: guardar el keystore en el repositorio, aunque sea "temporalmente" o en una rama privada. Quien clone el repo tiene la capacidad de firmar como tú. Error 2: dar permiso de escritura a match en CI, con lo que un job puede regenerar y revocar los certificados de todo el equipo. Error 3: derivar el número de build de un contador del CI; al cambiar de herramienta se reinicia y las subidas empiezan a rechazarse.

Error 4: compilar iOS en cada PR sin necesitarlo: es la mitad de la factura de CI por un beneficio marginal. Error 5: olvidar subir el mapping o los dSYM, lo que deja los informes de fallo ilegibles justo cuando hacen falta. Error 6: publicar al 100 % de golpe porque "esta versión es pequeña"; el escalado no cuesta nada y es la única red que hay.

Error 7: usar OTA para funcionalidad nueva y saltarse la revisión, con riesgo de retirada de la app. Error 8: implementar el bloqueo por versión mínima sin que las versiones antiguas sepan interpretarlo, con lo que el usuario ve un error incomprensible en vez de una invitación a actualizar. Error 9: medir el lead time solo hasta la tienda y concluir que el pipeline es lento cuando el tiempo se lo lleva la revisión.

Consejo 1: trata el keystore como el activo crítico que es, con copias en dos lugares y firma gestionada por Play como red. Consejo 2: publica el APK de debug como artefacto del PR con un enlace y un QR: es tu entorno de previsualización. Consejo 3: envía x-app-version desde el primer día aunque no la uses; sin ese dato no sabrás nunca a quién romperías. Consejo 4: escribe el procedimiento de release —quién aprueba, qué se mira en cada escalón, cuándo se pausa— antes de la primera publicación urgente.

Ejercicios

Ejercicio 1

Reservalia Pro 1.14.0 se publica el martes al 5 %. El miércoles a las 09:40, la tasa de sesiones sin fallo de esa versión es del 97,1 % frente al 99,7 % de la 1.13.4; los informes apuntan a un fallo al abrir la agenda en dispositivos Android 11. Describe el procedimiento completo de respuesta, indicando qué se puede y qué no se puede hacer, y compara cada paso con lo que habría hecho el equipo si el mismo fallo hubiera ocurrido en apps/api.

Ejercicio 2

El equipo de la API quiere renombrar el campo horaInicio a inicioUtc en la respuesta de GET /citas, coherente con la migración de la 04-06. La tabla de versiones vivas de la app muestra que el 39 % de los usuarios ejecuta versiones anteriores a la 1.14. Diseña el plan completo, con fases y criterios de avance, y di en qué momento exacto se puede retirar el campo antiguo.

Ejercicio 3

Diego propone eliminar la beta abierta: "nadie reporta nada por ahí, son 200 usuarios que no dicen ni mu, y nos retrasa cada release dos días". Evalúa la propuesta con argumentos a favor y en contra, y propón una decisión razonada con lo que medirías para validarla.

Soluciones

Solución 1. Lo primero es aceptar la restricción: no hay rollback. Los usuarios que ya instalaron la 1.14.0 la tienen, y ninguna acción tuya la sustituye por la 1.13.4. Lo que sí se puede hacer, por orden:

(1) Detener el escalado, en minutos. En Google Play se pausa el rollout —y, si el fallo es grave, se usa "halt", que retira la oferta a quien aún no ha actualizado—. Coste: cero. Efecto: el daño queda acotado a los ~10 negocios que ya la tienen. (2) Contener con lo que ya está desplegado. Si la vista de agenda nueva estaba tras un feature flag (03-05), se desactiva desde el servidor y el fallo desaparece sin publicar nada; esta es la razón por la que las funcionalidades móviles se envuelven en flags aunque la app no sea un servicio. Si además el problema estuviera en el lado servidor —un campo nuevo que la app no tolera— se puede arreglar en la API en minutos. (3) Avisar a los afectados. Diez negocios son identificables: soporte los llama y les ofrece una alternativa (usar la web) mientras dure. Esto no es un paso técnico, pero en un producto de pago es el que decide si el incidente se convierte en una baja. (4) Corregir hacia delante. Se arregla, se publica 1.14.1 y, si el impacto lo justifica, se solicita revisión expedita. Si el fallo es de JavaScript y no toca código nativo, una actualización OTA llega en horas en lugar de días: es exactamente el caso de uso legítimo del extintor. (5) Reanudar el escalado desde el 5 %, no desde donde se quedó, y con vigilancia específica de Android 11. (6) Post-mortem sin culpables (03-05), con una pregunta concreta: por qué la beta de 200 usuarios no cubría Android 11.

Comparación con el mismo fallo en apps/api: el paso 1 sería rollback.yml devolviendo el digest anterior en cuatro minutos, con todos los usuarios recuperados, no solo los futuros; el paso 3 no existiría porque no habría afectados residuales; el paso 4 dejaría de ser urgente, porque el sistema ya está sano y se corrige con calma. La diferencia de fondo es una sola: en la API el tiempo de recuperación depende de ti; en móvil depende del canal de distribución y de la voluntad del usuario. Por eso todo el esfuerzo se traslada hacia la izquierda —beta, escalado, flags—, y por eso el time to restore de 2,5 días de la tabla del apartado 11 es una propiedad del medio y no un defecto del equipo.

Solución 2. El plan es expand and contract, con la particularidad de que la fase de convivencia no dura una semana sino más de un año, porque no controlas cuándo actualiza el cliente.

Fase 1 — Expandir (API). GET /citas devuelve los dos campos, horaInicio e inicioUtc, con el mismo valor. Es aditivo: ninguna versión de la app se rompe, ni siquiera la 1.8. Se despliega con el pipeline normal, sin ceremonia. Fase 2 — Instrumentar. Añadir a la API una métrica que cuente, por versión de app, cuántas peticiones vienen de clientes que aún consumen el campo antiguo. Como el servidor no sabe qué campo lee el cliente, se aproxima con la cabecera x-app-version: se registra qué versiones están vivas y se cruza con el dato conocido de qué versión de la app empezó a leer inicioUtc. Sin este paso, la fase 4 se decide por intuición. Fase 3 — App. La versión 1.15.0 de Reservalia Pro lee inicioUtc y deja de usar horaInicio; se publica con el escalado habitual. Aquí el criterio de avance no es temporal sino de adopción: se espera a que la 1.15 o superior supere el 95 % de usuarios activos, lo que en Reservalia son unos cuatro meses. Fase 4 — Contraer. Solo cuando se cumplen tres condiciones a la vez: las versiones que leen horaInicio están por debajo del 1 % de usuarios activos; ha pasado el período de compatibilidad publicado (doce meses desde la 1.15); y se ha comprobado que ningún otro consumidor —la web, un script interno, una integración de un cliente grande— usa el campo.

El momento exacto de retirada es, por tanto, el máximo de esas tres condiciones, y en la práctica lo determina la tercera o la segunda, no la primera. Dos refuerzos que abaratan la espera: (a) para el 1 % residual, la técnica del 426 del apartado 9 permite forzar la actualización de esos usuarios concretos en lugar de mantener el campo para siempre; y (b) mientras dure la convivencia, un contrato en el pipeline de la API por cada versión de app viva impide que un refactor futuro elimine horaInicio por descuido. Y una observación de fondo: el coste de mantener un campo duplicado durante un año es bajísimo comparado con romper al 39 % de los usuarios. La asimetría de costes es la que decide, no la elegancia del contrato.

Solución 3. Los argumentos de Diego son ciertos pero miden lo que no toca. A favor de eliminarla: la beta añade dos días al lead time; el feedback cualitativo espontáneo es casi nulo —la gente no reporta, desinstala—; y mantener notas de versión y un canal de comunicación para 200 usuarios cuesta tiempo de alguien. En contra: el valor de la beta no es el feedback que la gente escribe, es la telemetría que genera sin escribir nada. Doscientos usuarios reales durante dos días producen sesiones en decenas de modelos de dispositivo y versiones de sistema operativo que ni el equipo ni el emulador cubren, y es ahí donde aparecen los fallos que no se ven en desarrollo: fabricantes con gestión agresiva de memoria, versiones antiguas de Android, tamaños de pantalla raros, ajustes de accesibilidad activados. El fallo de Android 11 del ejercicio 1 es literalmente el caso que una beta bien poblada detecta.

La decisión razonada: no eliminarla, sino cambiarle el objetivo y medirla. Concretamente: (1) dejar de esperar reportes escritos y considerarla una fase de telemetría, con un criterio de avance automático —X horas de uso acumulado, N modelos distintos, sesiones sin fallo ≥ 99,5 %—, de modo que si la cobertura se alcanza en doce horas, el release avanza en doce horas y no en dos días; (2) reclutar la beta con criterio, buscando diversidad de dispositivos en lugar de número, porque 60 usuarios en 40 modelos valen más que 200 en 6; (3) medir su rendimiento real durante seis meses con una métrica concreta: cuántos fallos se detectaron en beta y no habrían pasado la beta hasta producción. Si en seis meses la beta ha atrapado tres fallos que habrían llegado a usuarios de pago, dos días de retraso están justificados con creces —el coste de un fallo en producción incluye soporte, corrección urgente, revisión expedita y riesgo de baja de un cliente—. Si no ha atrapado ninguno, Diego tiene razón y se elimina. Lo que no es defendible es decidirlo por sensación en ninguna de las dos direcciones: es exactamente el tipo de pregunta que la 01-05 enseñó a responder con datos.

Conclusión

Reservalia Pro ha puesto a prueba el pipeline construido en los módulos anteriores y ha confirmado dos cosas. La primera es cuánto sobrevive: el modelo de ramas, el lockfile, el análisis estático, el artefacto único construido una vez y promocionado, la gestión de secretos por referencia con mínimo privilegio y las acciones reutilizables funcionan igual aquí que en la API. La segunda es qué se rompe y por qué. Aparece material criptográfico —keystore, certificados, perfiles— que hay que custodiar, rotar y prestar al CI en modo solo lectura y en un llavero temporal. Aparece un intermediario con revisión humana que convierte un lead time de horas en uno de días, y un coste real de runners macOS que obliga a decidir qué se compila y cuándo. Desaparece el rollback, y con él el mecanismo que sostenía la tranquilidad del módulo 3; en su lugar quedan el escalado por porcentaje con pausa automática, los feature flags que apagan sin publicar, la OTA como extintor con límites técnicos y de política, y el roll-forward como única salida. Y aparecen decenas de versiones de cliente vivas a la vez, lo que convierte cada cambio de la API en un ejercicio de compatibilidad hacia atrás que dura meses: campos aditivos, versiones convivientes, contratos por versión viva y el 426 implementado mucho antes de necesitarlo. Al final, las métricas DORA hubo que traducirlas, separando lo que el equipo controla —commit a beta, cuatro horas— de lo que impone el canal —commit a usuario, tres días y medio—, y la conclusión operativa fue que cuando no puedes revertir, toda la inversión se desplaza a detectar antes y a exponer a menos gente.

Fíjate en el patrón que se repite: en la web había un cliente antiguo; aquí hay decenas. Lo que en ambos casos sostiene el sistema es un contrato explícito y una convivencia larga entre versiones. La siguiente lección, Caso de Estudio: Microservicios, coge exactamente ese problema y lo multiplica en otra dirección. Reservalia crece y se parte en cinco servicios —citas, negocios, notificaciones, pagos, disponibilidad— con equipos distintos que despliegan cuando quieren. Ya no hay una app cliente y una API, sino un grafo en el que cada servicio es a la vez cliente antiguo y proveedor de otro. Allí el contrato deja de ser una tabla de versiones vigilada a mano y se convierte en pruebas de contrato ejecutables con una puerta de despliegue automática, el pipeline se multiplica por cinco y hay que decidir qué se estandariza y qué se deja a cada equipo.

Curso de CI/CD: Integración y Despliegue Continuo

Módulo 1: Introducción a CI/CD

Módulo 2: Integración Continua (CI)

Módulo 3: Despliegue Continuo (CD)

Módulo 4: Prácticas Avanzadas de CI/CD

Módulo 5: Implementación de CI/CD en Proyectos Reales

Módulo 6: Herramientas y Tecnologías

Módulo 7: Ejercicios Prácticos

Módulo 8: Recursos Adicionales

© Copyright 2026. Todos los derechos reservados