La lliçó anterior va acabar amb una idea incòmoda: el frontend és un client antic que no pots obligar a actualitzar-se. Allà la conseqüència era menor —n'hi ha prou que l'usuari recarregui la pàgina— i el rollback costava menys d'un minut. Ara anem a l'extrem d'aquesta mateixa propietat. Reservalia Pro és l'aplicació mòbil React Native amb què el professional gestiona la seva agenda des del mòbil: mira els forats del dia entre client i client, confirma o cancel·la cites i cobra. La fan servir 190 dels 340 negocis de pagament, i el seu pipeline trenca quatre regles que fa set mòduls que donem per descomptades. L'usuari decideix quan actualitza, i molts no ho fan mai. Una botiga revisa cada versió durant hores o dies abans de publicar-la, així que entre el merge i l'usuari hi ha un tercer que no controles. No pots fer rollback d'una versió ja publicada: l'única cosa que existeix és publicar-ne una altra. I conviuen alhora desenes de versions de client contra la mateixa API, no pas dues durant cinc minuts. En aquesta lliçó veurem quines parts del pipeline sobreviuen intactes, què cal reinventar —signatura de codi, distribució esglaonada, actualitzacions over-the-air, compatibilitat d'API— i com es llegeixen les mètriques DORA quan «desplegar» deixa de significar «que arribi a l'usuari».

Contingut

  1. Què té aquest context que la web no tenia
  2. Reservalia Pro: producte, repositori i el que es reutilitza
  3. Signatura de codi i gestió de credencials
  4. El pipeline de CI mòbil
  5. Versionat i numeració automàtica
  6. Canals de distribució i desplegament esglaonat
  7. El Fastfile de release, comentat
  8. Actualitzacions over-the-air i els seus límits
  9. Compatibilitat amb clients vells
  10. Crash reporting com a bucle de retroalimentació
  11. Les mètriques DORA quan desplegar no és arribar
  12. Fitxa del cas
  13. Errors Comuns i Consells
  14. Exercicis
  15. Conclusió

  1. Què té aquest context que la web no tenia

apps/web (05-01) Reservalia Pro (mòbil)
Qui decideix l'actualització El servidor: en recarregar ja està L'usuari, o la seva configuració d'auto-update
Intermediaris Cap App Store i Google Play, amb revisió humana
Temps merge → usuari 12 minuts De 4 hores a 5 dies, i després setmanes d'adopció
Rollback Reapuntar index.html: <1 min No existeix: només publicar una versió nova
Versions de client vives 1 o 2 Desenes, algunes de fa un any
Artefacte Carpeta de fitxers APK/AAB i IPA signats amb material criptogràfic
Cost del runner Linux, barat macOS per a iOS, ~10× el preu per minut
Senyal de fallada Errors 5xx al teu servidor Informes de crash de dispositius aliens

La fila de la signatura és la que introdueix el problema més operatiu i la que bloqueja més equips, així que va primer. La fila del rollback és la que més canvia el disseny: si no pots desfer, has de poder aturar, i d'aquí surt el desplegament esglaonat de l'apartat 6. I la de les versions vives és la que obliga a portar la disciplina de la 04-06 al contracte de l'API.

El que no canvia convé dir-ho amb la mateixa claredat, perquè és la major part: control de versions i trunk-based (02-07), npm ci amb lockfile (04-02), anàlisi estàtica (02-05), artefacte immutable construït una vegada i promocionat (02-06), secrets per referència i mínim privilegi (04-03), pipeline as code amb accions reutilitzables (04-05). Un pipeline mòbil no és un altre món: és el mateix amb tres peces noves i una restricció dura.

  1. Reservalia Pro: producte, repositori i el que es reutilitza

Reservalia Pro viu en un repositori propi, reservalia/pro-movil, no pas al monorepo. La raó és purament pràctica i val la pena entendre-la perquè es repeteix a la lliçó següent: l'app consumeix packages/compartit —els tipus i la lògica de disponibilitat— però el seu cicle de construcció no té res a veure amb el de l'API (eines natives, runners macOS, construccions de 18 minuts). Mantenir-la al monorepo obligaria que cada PR de l'API arrossegués la configuració mòbil, i el paths de la 02-07 es tornaria fràgil. La connexió es fa publicant @reservalia/compartit com a paquet versionat al registre privat de la 04-02.

pro-movil/
  src/                      # React Native (TypeScript)
  android/                  # projecte Gradle
  ios/                      # projecte Xcode
  fastlane/
    Fastfile                # les "lanes": beta, release, screenshots
    Appfile                 # identificadors d'app i comptes
    Matchfile               # configuració de match (certificats)
  .github/workflows/
    ci-movil.yml            # a cada PR
    release-movil.yml       # a cada tag v*

Dos fluxos, igual que al monorepo: un de ràpid a cada PR i un altre de publicació disparat per tag. La diferència amb ci.yml/cd.yml és que aquí el segon no acaba en producció, acaba a la porta d'una botiga.

  1. Signatura de codi i gestió de credencials

Tot binari mòbil va signat, i la signatura no és un detall de seguretat opcional: sense ella el sistema operatiu no instal·la res. Les dues plataformes resolen el mateix de manera diferent:

Android iOS
Material Un keystore (.jks) amb una clau i la seva contrasenya Certificat de distribució + perfil d'aprovisionament
Qui l'emet Tu mateix Apple, lligat al teu compte de desenvolupador
Caduca No (però perdre'l és fatal) Sí: certificat a 1 any, perfil a 1 any
Si el perds No pots publicar actualitzacions d'aquesta app mai més (llevat que la signatura la gestioni Play) Es regenera; molest però recuperable
Rotació Pràcticament mai Anual, i sempre agafa per sorpresa

La primera fila de «si el perds» és la que cal interioritzar: el keystore d'Android és l'actiu més crític del projecte. Reservalia en té una còpia al gestor de secrets de l'empresa, una altra xifrada fora de línia, i fa servir a més la signatura gestionada per Google Play com a xarxa de seguretat. És una decisió de negoci, no tècnica.

Fastlane match resol el problema d'iOS d'una manera elegant: desa els certificats i perfils xifrats en un repositori git privat, i cada màquina —el portàtil de la Nuria o el runner de CI— els descarrega i desxifra amb una única contrasenya. En comptes de N màquines amb material diferent, hi ha una font de veritat versionada.

# fastlane/Matchfile
git_url("[email protected]:reservalia/certificats-ios.git")    # 1 · repo privat i separat
storage_mode("git")
type("appstore")                                            # 2
readonly(true)                                              # 3
  1. Un repositori a part del codi: qui contribueix a l'app no necessita accés al material de signatura.
  2. A CI només es fa servir el tipus appstore; els perfils de desenvolupament es queden als portàtils.
  3. readonly(true) a CI és fonamental: el runner descarrega el que hi ha, mai no regenera certificats. Un CI amb permís d'escriptura pot revocar els certificats de l'equip sencer en un job mal escrit, i aquest incident deixa tothom sense poder publicar.

Al workflow, el material arriba per secrets i mai no toca el repositori en clar:

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

      - name: Preparar clauer 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: Descarregar certificats amb 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 i pujar a TestFlight
        run: bundle exec fastlane beta
        env:
          APP_STORE_CONNECT_API_KEY: ${{ secrets.ASC_API_KEY }}  # 4
  1. macos-14 és obligatori per compilar iOS: Xcode només existeix a macOS. A GitHub Actions, un minut de macOS costa deu vegades un minut de Linux, així que aquest job és el que domina la factura de CI de l'app. D'aquí ve la política de l'apartat 4: les construccions d'iOS no s'executen a cada PR.
  2. Un clauer temporal per job, creat i destruït a la mateixa execució. En un runner allotjat això és gairebé cerimonial perquè la màquina es destrueix; en un runner autoallotjat (02-02) és imprescindible, perquè si no els certificats queden a la màquina per al job següent, que pot ser d'un altre repositori.
  3. MATCH_PASSWORD és la clau mestra de tot el material de signatura. Viu al gestor de secrets de l'entorn de release (03-02), amb revisors, i no pas als secrets del repositori accessibles a qualsevol workflow.
  4. Autenticació per clau d'API d'App Store Connect, no amb usuari i contrasenya d'un humà. És l'equivalent mòbil de l'OIDC de la 03-02: credencial de servei, amb permisos acotats i revocable sense afectar cap persona.

  1. El pipeline de CI mòbil

A cada pull request, i amb el cost del runner macOS molt present:

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

jobs:
  estatic:                               # 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: [estatic]
    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 · sense signatura de release
      - uses: actions/upload-artifact@v4
        with: { name: apk-debug-${{ github.sha }}, path: android/app/build/outputs/apk/debug/*.apk }

  instrumentats:                         # 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, només sota condició
    if: contains(github.event.pull_request.labels.*.name, 'ios') || github.event.pull_request.base.ref == 'release'
    runs-on: macos-14
    needs: [estatic]
    steps:
      - uses: actions/checkout@v4
      - run: xcodebuild -workspace ios/Pro.xcworkspace -scheme Pro -sdk iphonesimulator build
  1. El job barat va primer i bloqueja els altres. És el fail fast de la 04-01, i aquí té un benefici econòmic directe: un error de tipus detectat en dos minuts de Linux evita gastar-ne quaranta de macOS.
  2. L'APK de debug es construeix a cada PR i es puja com a artefacte. La Nuria l'instal·la en un dispositiu real per revisar el canvi: és l'equivalent mòbil de l'entorn de previsualització per PR de la 02-07. Un comentari automàtic amb un enllaç de descàrrega i un QR completa el circuit.
  3. assembleDebug fa servir la signatura de depuració, que no necessita el keystore de release. Això permet que aquest job corri en qualsevol PR, fins i tot d'una branca d'un col·laborador extern, sense exposar material sensible. És la mateixa lògica de mínim privilegi de la 04-03.
  4. Les proves instrumentades s'executen en un emulador dins del runner. Són lentes i més fràgils que les unitàries, així que la piràmide de la 02-04 s'aplica amb severitat: unes vuit, sobre els fluxos que donen diners. Les inestables van a quarantena amb la mateixa política de sempre.
  5. iOS només es compila sota demanda, mitjançant una etiqueta ios al PR o si el PR va contra la branca de release. La justificació és de cost: compilar iOS als 60 PR mensuals de Reservalia costaria unes 25 hores de macOS; amb aquesta condició es queda en cinc. El risc assumit és real —un canvi que trenca només iOS es detecta més tard— i s'acota fixant una compilació nocturna programada.

Diego: «Quant ens costa això al mes?» La resposta, amb números: 60 PR × 20 min de Linux, més 12 construccions d'iOS × 40 min de macOS. Al voltant de 20 h de Linux i 8 h de macOS, i la meitat de la factura ve de les 8 hores. Sense la condició del punt 5, serien tres vegades més.

  1. Versionat i numeració automàtica

Tres números conviuen i confondre'ls causa releases rebutjades:

Concepte Android iOS Per a què serveix
Versió visible versionName (1.14.0) CFBundleShortVersionString El que veu l'usuari; SemVer de la 02-06
Número intern versionCode (enter) CFBundleVersion (build) Ordena les pujades; ha de créixer sempre
Identitat applicationId bundleIdentifier Identifica l'app; no canvia mai

La regla dura: el número intern ha de ser estrictament creixent i no es pot reutilitzar. Si puges la build 412 a TestFlight i després vols tornar a pujar aquesta mateixa versió amb un arranjament, ha de ser la 413 encara que el versionName continuï sent 1.14.0. Derivar-lo a mà garanteix que algun dia algú el repetirà i la pujada es rebutjarà després de quaranta minuts de construcció.

# fastlane/Fastfile (fragment)
lane :numerar do
  versio = sh("node -p \"require('./package.json').version\"").strip   # 1
  build  = latest_testflight_build_number(version: versio) + 1         # 2
  increment_version_number(version_number: versio)
  increment_build_number(build_number: build)
  UI.message("Publicant #{versio} (build #{build})")
end
  1. La versió visible surt del package.json, que al seu torn la fixa semantic-release des dels commits convencionals de la 02-06. L'app hereta així el versionat de la resta del sistema, sense un mecanisme paral·lel.
  2. El número de build es consulta a la botiga i s'incrementa. És l'única font que no es pot desincronitzar, millor que github.run_number: si migres de CI, el comptador es reinicia i les pujades comencen a rebutjar-se. Preguntar a qui té la veritat és més robust que portar el compte.

  1. Canals de distribució i desplegament esglaonat

Aquí hi ha la substitució conceptual més important de la lliçó. El canari de la 03-04 dirigia el 10 % del trànsit a la versió nova mitjançant el pes de l'ALB, i si les mètriques empitjoraven es retirava en segons. En mòbil no hi ha balancejador i no hi ha retirada: la versió ja està instal·lada al dispositiu de l'usuari. El que sí que existeix és controlar a quants se'ls ofereix.

flowchart LR
    B["Build signada"] --> I["Intern<br/>l equip · minuts"]
    I --> A["Alpha / TestFlight<br/>~30 negocis voluntaris"]
    A --> C["Beta oberta<br/>~200 usuaris"]
    C --> R1["Produccio 5%"]
    R1 --> R2["20%"]
    R2 --> R3["50%"]
    R3 --> R4["100%"]
    R1 -.->|"crash-free < 99,5%"| H["Aturar desplegament"]
    R2 -.->|"crash-free < 99,5%"| H
    H --> F["Publicar versio correctiva"]

Els canals, amb el que aporten i el que costen:

Canal Qui hi entra Revisió de la botiga Per a què serveix
Intern Fins a 100 correus; l'equip Cap o mínima Verificar que el binari signat arrenca
Alpha / TestFlight intern Negocis voluntaris (~30) Lleugera Ús real, retroalimentació qualitativa
Beta oberta Qui s'hi apunti (~200) Cobertura de dispositius i versions de SO
Producció esglaonada Percentatge creixent Ja passada Limitar el dany d'una fallada

El staged rollout funciona així: publiques al 5 %, i la botiga ofereix l'actualització només a aquesta fracció d'usuaris triada a l'atzar. Observes 24 hores la taxa de sessions sense fallada, puges al 20 %, i així fins al 100 % en quatre o cinc dies. Les seves dues diferències amb el canari són les que cal tenir clares:

  • Aturar no és revertir. Si al 20 % detectes una fallada, pots pausar l'escalat —ningú més no rebrà la versió— però els qui ja la van instal·lar s'hi queden. Google Play permet a més «halt», que retira l'oferta; iOS permet pausar la publicació esglaonada. En cap cas no torna el binari anterior als dispositius.
  • L'únic arranjament és cap endavant. És el roll-forward de la 03-05, però aquí no és una opció entre dues: és l'única. I porta de la mà una revisió de la botiga, que en un cas urgent es pot accelerar sol·licitant una revisió expedita, sense garanties.

D'aquí ve la conclusió operativa: com que no pots revertir, has de detectar abans i amb menys usuaris. Un 5 % de 190 negocis són 9 o 10; suficient perquè una fallada d'arrencada aparegui al crash reporting en hores, i prou petit perquè només 10 clients la pateixin.

  1. El Fastfile de release, comentat

# fastlane/Fastfile
default_platform(:android)

platform :android do
  desc "Puja a Play al canal indicat i amb 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ó es resol primer, perquè un rebuig per número repetit passi abans de compilar i no pas després.
  2. bundleRelease genera un AAB, no pas un APK. Google Play el fa servir per generar APK optimitzats per dispositiu, amb la qual cosa la descàrrega de l'usuari baixa bastant. Les credencials de signatura entren per variables d'entorn que el workflow omple des de secrets: mai no apareixen a build.gradle, que està versionat.
  3. El canal és un paràmetre, no un valor fix. El mateix lane serveix per a intern, beta i producció; el que canvia és com s'invoca. És la mateixa idea del workflow reutilitzable de la 04-05.
  4. rollout: 0.05 arrenca al 5 %. Els increments posteriors no tornen a pujar el binari: es fan des de la consola o amb un lane que només canvia el percentatge. L'artefacte és el mateix —construir una vegada, desplegar moltes vegades (02-06)—; el que es mou és l'exposició.
  5. El fitxer de mapping es puja amb el release. Sense ell, els informes de crash de l'apartat 10 arriben ofuscats i són inútils. És el pas que més s'oblida i el que surt més car a les tres de la matinada.

Al workflow, això es dispara amb un tag i amb aprovació humana:

  publicar-android:
    if: startsWith(github.ref, 'refs/tags/v')
    environment: botigues                      # revisors obligatoris, com a 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. Actualitzacions over-the-air i els seus límits

Com que React Native executa un bundle de JavaScript sobre una carcassa nativa, existeix la possibilitat de substituir aquest bundle sense passar per la botiga: l'app descarrega la versió nova en arrencar i l'aplica en l'inici següent. És el que fan CodePush o EAS Update, i retorna una cosa semblant al desplegament continu que teníem a web.

Actualització OTA Release per botiga
Què pot canviar Només JavaScript i recursos Qualsevol cosa, inclòs codi natiu
Temps fins a l'usuari Minuts o hores Hores o dies + adopció
Revisió Cap
Reversió : tornar al bundle anterior No
Risc Un bundle trencat deixa l'app inservible Menor, hi ha revisió pel mig

Els límits cal dir-los sense adorns. Tècnics: si el canvi toca una dependència nativa, canvia permisos o actualitza la versió de React Native, l'OTA no serveix i cal passar per la botiga; i el bundle OTA ha de ser compatible amb la carcassa nativa que ja està instal·lada, cosa que obliga a lligar-lo a una versió d'app concreta. De política de botiga: les regles d'Apple permeten actualitzar codi interpretat sempre que no canviï el propòsit principal de l'app ni introdueixi funcionalitat substancialment diferent de la revisada; fer servir OTA per saltar-se la revisió és motiu de retirada. La lectura pràctica de Reservalia:

  • Sí OTA per a correccions urgents de JavaScript, textos, ajustos d'estil i activar o desactivar un feature flag (03-05) que ja venia al binari.
  • No OTA per a funcionalitat nova visible, canvis de permisos, dependències natives ni res que alteri el que la botiga va revisar.
  • Sempre esglaonat també a l'OTA, amb la mateixa disciplina de percentatges, perquè un bundle trencat distribuït al 100 % en deu minuts és pitjor que qualsevol fallada de botiga.

Marta: «L'OTA és l'extintor, no l'ascensor.» Es fa servir per apagar un foc, no com a via habitual de lliurament. Un equip que ho lliura tot per OTA acaba amb una versió de botiga desactualitzada i una diferència creixent entre el que van revisar i el que executa l'usuari.

  1. Compatibilitat amb clients vells

Aquest és l'equivalent mòbil d'expand and contract (04-06), i és la part que afecta l'equip de l'API encara que no toqui l'app. Les dades de Reservalia, preses als trenta dies d'un release:

Versió de Pro Usuaris actius
1.14.x (última) 61 %
1.13.x 22 %
1.12.x 9 %
1.9.x – 1.11.x 6 %
Anteriors a 1.9 2 %

El 39 % dels usuaris executa codi de fa mesos. Això converteix qualsevol canvi de l'API en un canvi de contracte amb desenes de clients que no pots actualitzar. Quatre tàctiques, en ordre de preferència:

(1) Canvis additius i compatibles cap enrere, sempre que sigui possible. Un camp nou a la resposta no trenca ningú; reanomenar-ne un, sí. És literalment el patró de la 04-06 aplicat al JSON en lloc de a l'esquema: s'afegeix el camp nou, tots dos conviuen durant mesos, i el vell es retira quan l'ús baixa a zero.

(2) Versionat explícit de l'API quan el canvi no pot ser additiu: /v1/cites i /v2/cites convivint, amb una política escrita de quant temps es manté cada versió —a Reservalia, dotze mesos des que es publica la següent—.

(3) Proves de contracte al pipeline de l'API. L'app publica la seva expectativa i l'API verifica a CI que no la trenca. És la tècnica que la lliçó següent desenvolupa a fons per a microserveis; aquí n'hi ha prou amb el mecanisme: un conjunt de contractes versionats per versió d'app viva, executats com a proves de l'API.

(4) Versió mínima suportada, com a últim recurs. L'API respon amb un codi específic i l'app mostra una pantalla bloquejant que convida a actualitzar:

// apps/api/src/plugins/versio-client.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: 'versio_no_suportada',
      minima: MINIMA,
      missatge: 'Actualitza Reservalia Pro per continuar fent servir l\'aplicació.',
    });
  }
});
  1. L'app envia la seva versió a cada petició. Sense aquesta capçalera no hi ha manera de saber qui t'està cridant, i és la dada que alimenta la taula de dalt.
  2. Si no ve la capçalera, no es bloqueja: pot ser la web, un script intern o una versió antiquíssima. Bloquejar per absència trenca integracions legítimes.
  3. 426 Upgrade Required amb un cos que l'app sap interpretar. La pantalla bloquejant ha d'existir abans que es necessiti: si la versió 1.8 no sap gestionar el 426, forçar-la només produeix errors incomprensibles. Aquesta és la trampa clàssica d'aquesta tècnica, i la raó que la gestió del 426 s'implementi aviat encara que no es faci servir en anys.

Forçar l'actualització té un cost de negoci: un professional al mig de la jornada a qui bloqueges l'agenda no està content. Es reserva per a motius seriosos —una vulnerabilitat, un canvi incompatible inevitable— i s'avisa abans amb una pantalla no bloquejant durant setmanes.

  1. Crash reporting com a bucle de retroalimentació

A la 03-06 el bucle de retroalimentació era observabilitat del servidor: mètriques, logs i traces de màquines pròpies. Aquí el codi corre en dispositius aliens, i el senyal equivalent són els informes de fallada. La mètrica que governa és la taxa de sessions sense fallada:

Mètrica Objectiu a Reservalia Pro Què decideix
Sessions sense fallada ≥ 99,5 % Si l'escalat continua o s'atura
Usuaris sense fallada ≥ 99,8 % Si una fallada és massiva o afecta un dispositiu rar
ANR (Android) < 0,3 % Bloquejos d'interfície que l'usuari viu com «s'ha penjat»
Errors 4xx/5xx per versió Correlaciona una fallada amb un canvi de l'API

Dos requisits perquè això funcioni. Primer, pujar els símbols al release —el mapping.txt d'Android i els dSYM d'iOS—, o els informes arriben sense noms de funció. Segon, marcar la versió i l'esglaó: cada informe porta versionName, build i el percentatge de rollout actiu, per poder respondre a «això ho va portar la 1.14 o ja passava abans?». És l'equivalent de les marques de desplegament al panell de la 03-06.

L'automatització que Reservalia sí que té: un job programat consulta cada hora la taxa de sessions sense fallada de la versió en escalat i, si baixa del llindar, pausa el rollout automàticament i avisa per Slack. És el descendent directe del rollback automàtic per mètriques de la 03-05, amb la diferència que aquí no reverteix res: atura l'expansió.

  1. Les mètriques DORA quan desplegar no és arribar

Aplicar les definicions de la 01-05 sense traduir-les dóna resultats absurds: «desplegament» passaria a significar «pujar a la botiga» i el lead time diria cinc dies sense distingir on se'n va el temps. La traducció que Reservalia fa servir:

Mètrica Definició a l'API Definició en mòbil Valor
Freqüència de desplegament Desplegaments a prod Releases publicades a la botiga 1 cada 2 setmanes
(auxiliar) construccions a intern/beta 8 per setmana
Lead time Commit → prod Commit → disponible a la botiga 3,5 dies
(auxiliar) commit → beta 4 hores
Change failure rate Desplegaments amb incident Releases amb caiguda de sessions sense fallada o correcció urgent 12 %
Time to restore Rollback: 9 min Correcció publicada i adoptada al 50 % 2,5 dies

Tres lectures. La primera: el time to restore de 2,5 dies no és un fracàs de l'equip, és una propietat del canal de distribució, i per això l'esforç es desplaça de «recuperar ràpid» a «no publicar la fallada» —més beta, escalat més lent al principi, feature flags per desactivar sense publicar—. La segona: les mètriques auxiliars són les que l'equip pot millorar de debò; si commit→beta són quatre hores, el pipeline funciona, encara que la botiga hi afegeixi tres dies a sobre. La tercera: el change failure rate del 12 % és tres vegades el 3,8 % de l'API, i té una explicació estructural —cada release acumula dues setmanes de canvis, davant dels desplegaments petits i freqüents de l'API—. La lliçó de la 01-02 es confirma a l'inrevés: lots grans, més fallades. I com que aquí el lot no es pot reduir a voluntat, es compensa amb la beta i l'escalat.

  1. Fitxa del cas

Context React Native, repositori propi, 190 negocis, iOS + Android, publicació quinzenal
Què continua valent tal qual Trunk-based (02-07), lockfile (04-02), estàtic (02-05), artefacte únic (02-06), secrets i mínim privilegi (04-03), accions reutilitzables (04-05)
Decisió 1 Signatura amb match en repositori separat, readonly a CI i clauer temporal per job
Decisió 2 iOS només es compila amb etiqueta o branca de release, més construcció nocturna; estalvi ~20 h de macOS/mes
Decisió 3 Número de build consultat a la botiga, versió visible heretada de semantic-release
Decisió 4 Escalat 5 → 20 → 50 → 100 % amb pausa automàtica si les sessions sense fallada baixen de 99,5 %
Decisió 5 OTA només per a correccions de JS i flags; mai per a funcionalitat nova
Decisió 6 Capçalera x-app-version, contractes per versió viva i 426 implementat anys abans de fer-lo servir
Efecte en DORA Lead time real 3,5 dies (auxiliar a beta: 4 h); CFR 12 %; restore 2,5 dies
Què t'endús a qualsevol projecte Quan no pots revertir, la inversió es mou a detectar abans i a exposar menys gent

Errors Comuns i Consells

Error 1: desar el keystore al repositori, encara que sigui «temporalment» o en una branca privada. Qui cloni el repo té la capacitat de signar com tu. Error 2: donar permís d'escriptura a match a CI, amb la qual cosa un job pot regenerar i revocar els certificats de tot l'equip. Error 3: derivar el número de build d'un comptador del CI; en canviar d'eina es reinicia i les pujades comencen a rebutjar-se.

Error 4: compilar iOS a cada PR sense necessitar-ho: és la meitat de la factura de CI per un benefici marginal. Error 5: oblidar pujar el mapping o els dSYM, cosa que deixa els informes de fallada il·legibles justament quan calen. Error 6: publicar al 100 % de cop perquè «aquesta versió és petita»; l'escalat no costa res i és l'única xarxa que hi ha.

Error 7: fer servir OTA per a funcionalitat nova i saltar-se la revisió, amb risc de retirada de l'app. Error 8: implementar el bloqueig per versió mínima sense que les versions antigues sàpiguen interpretar-lo, amb la qual cosa l'usuari veu un error incomprensible en comptes d'una invitació a actualitzar. Error 9: mesurar el lead time només fins a la botiga i concloure que el pipeline és lent quan el temps se l'emporta la revisió.

Consell 1: tracta el keystore com l'actiu crític que és, amb còpies en dos llocs i signatura gestionada per Play com a xarxa. Consell 2: publica l'APK de debug com a artefacte del PR amb un enllaç i un QR: és el teu entorn de previsualització. Consell 3: envia x-app-version des del primer dia encara que no la facis servir; sense aquesta dada no sabràs mai a qui trencaries. Consell 4: escriu el procediment de release —qui aprova, què es mira a cada esglaó, quan es pausa— abans de la primera publicació urgent.

Exercicis

Exercici 1

Reservalia Pro 1.14.0 es publica el dimarts al 5 %. El dimecres a les 09:40, la taxa de sessions sense fallada d'aquesta versió és del 97,1 % davant del 99,7 % de la 1.13.4; els informes apunten a una fallada en obrir l'agenda en dispositius Android 11. Descriu el procediment complet de resposta, indicant què es pot i què no es pot fer, i compara cada pas amb el que hauria fet l'equip si la mateixa fallada hagués ocorregut a apps/api.

Exercici 2

L'equip de l'API vol reanomenar el camp horaInici a iniciUtc a la resposta de GET /cites, coherent amb la migració de la 04-06. La taula de versions vives de l'app mostra que el 39 % dels usuaris executa versions anteriors a la 1.14. Dissenya el pla complet, amb fases i criteris d'avanç, i digues en quin moment exacte es pot retirar el camp antic.

Exercici 3

En Diego proposa eliminar la beta oberta: «ningú no reporta res per allà, són 200 usuaris que no diuen ni piu, i ens endarrereix cada release dos dies». Avalua la proposta amb arguments a favor i en contra, i proposa una decisió raonada amb el que mesuraries per validar-la.

Solucions

Solució 1. El primer és acceptar la restricció: no hi ha rollback. Els usuaris que ja van instal·lar la 1.14.0 la tenen, i cap acció teva no la substitueix per la 1.13.4. El que sí que es pot fer, per ordre:

(1) Aturar l'escalat, en minuts. A Google Play es pausa el rollout —i, si la fallada és greu, es fa servir «halt», que retira l'oferta a qui encara no ha actualitzat—. Cost: zero. Efecte: el dany queda acotat als ~10 negocis que ja la tenen. (2) Contenir amb el que ja està desplegat. Si la vista d'agenda nova era darrere d'un feature flag (03-05), es desactiva des del servidor i la fallada desapareix sense publicar res; aquesta és la raó per la qual les funcionalitats mòbils s'emboliquen en flags encara que l'app no sigui un servei. Si a més el problema fos al costat servidor —un camp nou que l'app no tolera— es pot arreglar a l'API en minuts. (3) Avisar els afectats. Deu negocis són identificables: suport els truca i els ofereix una alternativa (fer servir la web) mentre duri. Això no és un pas tècnic, però en un producte de pagament és el que decideix si l'incident es converteix en una baixa. (4) Corregir cap endavant. S'arregla, es publica 1.14.1 i, si l'impacte ho justifica, se sol·licita revisió expedita. Si la fallada és de JavaScript i no toca codi natiu, una actualització OTA arriba en hores en lloc de dies: és exactament el cas d'ús legítim de l'extintor. (5) Reprendre l'escalat des del 5 %, no des d'on es va quedar, i amb vigilància específica d'Android 11. (6) Post-mortem sense culpables (03-05), amb una pregunta concreta: per què la beta de 200 usuaris no cobria Android 11.

Comparació amb la mateixa fallada a apps/api: el pas 1 seria rollback.yml tornant el digest anterior en quatre minuts, amb tots els usuaris recuperats, no només els futurs; el pas 3 no existiria perquè no hi hauria afectats residuals; el pas 4 deixaria de ser urgent, perquè el sistema ja és sa i es corregeix amb calma. La diferència de fons és una de sola: a l'API el temps de recuperació depèn de tu; en mòbil depèn del canal de distribució i de la voluntat de l'usuari. Per això tot l'esforç es trasllada cap a l'esquerra —beta, escalat, flags—, i per això el time to restore de 2,5 dies de la taula de l'apartat 11 és una propietat del mitjà i no un defecte de l'equip.

Solució 2. El pla és expand and contract, amb la particularitat que la fase de convivència no dura una setmana sinó més d'un any, perquè no controles quan actualitza el client.

Fase 1 — Expandir (API). GET /cites retorna els dos camps, horaInici i iniciUtc, amb el mateix valor. És additiu: cap versió de l'app no es trenca, ni tan sols la 1.8. Es desplega amb el pipeline normal, sense cerimònia. Fase 2 — Instrumentar. Afegir a l'API una mètrica que compti, per versió d'app, quantes peticions vénen de clients que encara consumeixen el camp antic. Com que el servidor no sap quin camp llegeix el client, s'aproxima amb la capçalera x-app-version: es registra quines versions són vives i es creua amb la dada coneguda de quina versió de l'app va començar a llegir iniciUtc. Sense aquest pas, la fase 4 es decideix per intuïció. Fase 3 — App. La versió 1.15.0 de Reservalia Pro llegeix iniciUtc i deixa de fer servir horaInici; es publica amb l'escalat habitual. Aquí el criteri d'avanç no és temporal sinó d'adopció: s'espera que la 1.15 o superior superi el 95 % d'usuaris actius, cosa que a Reservalia són uns quatre mesos. Fase 4 — Contraure. Només quan es compleixen tres condicions alhora: les versions que llegeixen horaInici són per sota de l'1 % d'usuaris actius; ha passat el període de compatibilitat publicat (dotze mesos des de la 1.15); i s'ha comprovat que cap altre consumidor —la web, un script intern, una integració d'un client gran— fa servir el camp.

El moment exacte de retirada és, per tant, el màxim d'aquestes tres condicions, i a la pràctica el determina la tercera o la segona, no la primera. Dos reforços que abarateixen l'espera: (a) per a l'1 % residual, la tècnica del 426 de l'apartat 9 permet forçar l'actualització d'aquests usuaris concrets en lloc de mantenir el camp per sempre; i (b) mentre duri la convivència, un contracte al pipeline de l'API per cada versió d'app viva impedeix que un refactor futur elimini horaInici per descuit. I una observació de fons: el cost de mantenir un camp duplicat durant un any és baixíssim comparat amb trencar el 39 % dels usuaris. L'asimetria de costos és la que decideix, no l'elegància del contracte.

Solució 3. Els arguments d'en Diego són certs però mesuren el que no toca. A favor d'eliminar-la: la beta afegeix dos dies al lead time; la retroalimentació qualitativa espontània és gairebé nul·la —la gent no reporta, desinstal·la—; i mantenir notes de versió i un canal de comunicació per a 200 usuaris costa temps d'algú. En contra: el valor de la beta no és la retroalimentació que la gent escriu, és la telemetria que genera sense escriure res. Dos-cents usuaris reals durant dos dies produeixen sessions en desenes de models de dispositiu i versions de sistema operatiu que ni l'equip ni l'emulador cobreixen, i és aquí on apareixen les fallades que no es veuen en desenvolupament: fabricants amb gestió agressiva de memòria, versions antigues d'Android, mides de pantalla rares, ajustos d'accessibilitat activats. La fallada d'Android 11 de l'exercici 1 és literalment el cas que una beta ben poblada detecta.

La decisió raonada: no eliminar-la, sinó canviar-li l'objectiu i mesurar-la. Concretament: (1) deixar d'esperar informes escrits i considerar-la una fase de telemetria, amb un criteri d'avanç automàtic —X hores d'ús acumulat, N models diferents, sessions sense fallada ≥ 99,5 %—, de manera que si la cobertura s'assoleix en dotze hores, el release avança en dotze hores i no en dos dies; (2) reclutar la beta amb criteri, buscant diversitat de dispositius en lloc de número, perquè 60 usuaris en 40 models valen més que 200 en 6; (3) mesurar-ne el rendiment real durant sis mesos amb una mètrica concreta: quantes fallades es van detectar en beta i no haurien passat la beta fins a producció. Si en sis mesos la beta ha atrapat tres fallades que haurien arribat a usuaris de pagament, dos dies de retard estan justificats de sobres —el cost d'una fallada en producció inclou suport, correcció urgent, revisió expedita i risc de baixa d'un client—. Si no n'ha atrapat cap, en Diego té raó i s'elimina. El que no és defensable és decidir-ho per sensació en cap de les dues direccions: és exactament el tipus de pregunta que la 01-05 va ensenyar a respondre amb dades.

Conclusió

Reservalia Pro ha posat a prova el pipeline construït als mòduls anteriors i ha confirmat dues coses. La primera és quant en sobreviu: el model de branques, el lockfile, l'anàlisi estàtica, l'artefacte únic construït una vegada i promocionat, la gestió de secrets per referència amb mínim privilegi i les accions reutilitzables funcionen igual aquí que a l'API. La segona és què es trenca i per què. Apareix material criptogràfic —keystore, certificats, perfils— que cal custodiar, rotar i prestar al CI en mode només lectura i en un clauer temporal. Apareix un intermediari amb revisió humana que converteix un lead time d'hores en un de dies, i un cost real de runners macOS que obliga a decidir què es compila i quan. Desapareix el rollback, i amb ell el mecanisme que sostenia la tranquil·litat del mòdul 3; al seu lloc queden l'escalat per percentatge amb pausa automàtica, els feature flags que apaguen sense publicar, l'OTA com a extintor amb límits tècnics i de política, i el roll-forward com a única sortida. I apareixen desenes de versions de client vives alhora, cosa que converteix cada canvi de l'API en un exercici de compatibilitat cap enrere que dura mesos: camps additius, versions convivents, contractes per versió viva i el 426 implementat molt abans de necessitar-lo. Al final, les mètriques DORA les vam haver de traduir, separant el que l'equip controla —commit a beta, quatre hores— del que imposa el canal —commit a usuari, tres dies i mig—, i la conclusió operativa va ser que quan no pots revertir, tota la inversió es desplaça a detectar abans i a exposar menys gent.

Fixa't en el patró que es repeteix: a la web hi havia un client antic; aquí n'hi ha desenes. El que en tots dos casos sosté el sistema és un contracte explícit i una convivència llarga entre versions. La lliçó següent, Cas d'Estudi: Microserveis, agafa exactament aquest problema i el multiplica en una altra direcció. Reservalia creix i es parteix en cinc serveis —cites, negocis, notificacions, pagaments, disponibilitat— amb equips diferents que despleguen quan volen. Ja no hi ha una app client i una API, sinó un graf en què cada servei és alhora client antic i proveïdor d'un altre. Allà el contracte deixa de ser una taula de versions vigilada a mà i es converteix en proves de contracte executables amb una porta de desplegament automàtica, el pipeline es multiplica per cinc i cal decidir què s'estandarditza i què es deixa a cada equip.

Curs de CI/CD: Integració i Desplegament Continu

Mòdul 1: Introducció al CI/CD

Mòdul 2: Integració Contínua (CI)

Mòdul 3: Desplegament Continu (CD)

Mòdul 4: Pràctiques Avançades de CI/CD

Mòdul 5: Implementació de CI/CD en Projectes Reals

Mòdul 6: Eines i Tecnologies

Mòdul 7: Exercicis Pràctics

Mòdul 8: Recursos Addicionals

© Copyright 2026. Tots els drets reservats