Reservalia ja té quatre jobs, proves de debò, anàlisi estàtica i un artefacte publicat a ECR. I tanmateix, tot això pot continuar sense funcionar per una raó que no té res a veure amb el YAML: si les branques viuen tres setmanes, no hi ha Integració Contínua per molt bo que sigui el pipeline. Aquesta darrera lliçó del mòdul tanca el cercle connectant l'eina amb la manera de treballar de l'equip. Compararem els tres models de branques més estesos i veurem quin encaixa amb CI/CD i quin no; configurarem a GitHub les regles que converteixen els acords de la lliçó 02-01 en una cosa que no depèn de la bona voluntat; aprendrem a evitar execucions inútils amb paths i concurrency; analitzarem quin efecte té cada estratègia de merge sobre la traçabilitat de l'artefacte i sobre el càlcul del lead time; i acabarem amb el ci.yml complet de Reservalia, amb els seus quatre jobs i les seves dependències, i amb el balanç del que s'ha aconseguit al mòdul.

Contingut

  1. Per què el model de branques decideix el destí de la teva CI
  2. Trunk-based, GitHub Flow i Git Flow cara a cara
  3. Branques curtes i mida del pull request
  4. Protegir main: checks obligatoris, CODEOWNERS i merge queue
  5. Evitar execucions inútils: paths i concurrency
  6. Estratègies de merge, traçabilitat i lead time
  7. Entorns efímers de previsualització per PR
  8. El ci.yml complet de Reservalia
  9. Errors Comuns i Consells
  10. Exercicis
  11. Conclusió

  1. Per què el model de branques decideix el destí de la teva CI

Torna a l'integration hell de la lliçó 01-01: el cost de fusionar creix de manera no lineal amb el temps que les branques viuen separades. Aquest fet, tot sol, ja determina quins models de branques són compatibles amb la CI.

Una branca de vida llarga és un lot gran disfressat. Mentre viu, no s'està integrant res —el pipeline verifica aquella branca contra un main que s'allunya cada dia—; el verd és enganyós, perquè "la meva branca passa" no vol dir "passarà quan es fusioni amb el que hi ha entrat mentrestant"; i el risc s'acumula: deu canvis petits fusionats al llarg de dues setmanes es diagnostiquen un a un, mentre que els mateixos deu fusionats de cop formen una única fallada amb deu causes possibles.

La regla que resumeix l'apartat. La freqüència d'integració és una variable del procés, no de l'eina. Cap pipeline no pot compensar branques de tres setmanes.

  1. Trunk-based, GitHub Flow i Git Flow cara a cara

Trunk-based GitHub Flow Git Flow
Branques permanents main main main + develop
Branques temporals Molt curtes o cap Una per funcionalitat feature/, release/, hotfix/
Vida de la branca Hores, màx. 1 dia 1-3 dies Setmanes
Freqüència d'integració Diverses vegades al dia Diària Setmanal o menys
Cost de merge Gairebé nul Baix Alt
Encaix amb CI/CD Excel·lent Molt bo Dolent
Complexitat mental Baixa Baixa Alta
Requereix feature flags Sí, sovint A vegades Rarament
Context on brilla Equips amb CI madura i desplegament continu La majoria d'equips de producte Programari amb versions mantingudes en paral·lel

Tres precisions que eviten malentesos:

Git Flow no és "dolent", està dissenyat per a un altre problema. Va néixer el 2010 per a programari que es distribueix en versions i en manté diverses alhora —pensa en una aplicació d'escriptori que dona suport a la 3.x mentre desenvolupa la 4.0—. En aquest context, les branques release/ i hotfix/ tenen sentit. Aplicar-lo a un SaaS amb un únic desplegament, com Reservalia, hi afegeix una branca develop que només serveix per retardar la integració.

GitHub Flow és el punt dolç per a la majoria. Una branca curta per canvi, un pull request, verificació automàtica, revisió i merge a main. És el que fa Reservalia, i és el que hem suposat durant tot el mòdul.

Trunk-based no significa "sense branques ni revisió". Significa branques d'hores i merge diari. Quan un canvi no es pot acabar en un dia, s'integra incomplet però inactiu, amagat darrere d'un feature flag —la tècnica de la lliçó 03-05—. Aquesta és l'habilitat que cal aprendre, i no una qüestió de disciplina.

  1. Branques curtes i mida del pull request

Hi ha una variable que prediu el temps de revisió millor que cap altra: el nombre de línies canviades.

Mida del PR Temps típic fins al merge Qualitat de la revisió
< 100 línies Hores Alta: es llegeix de debò
100-400 línies 1 dia Acceptable
400-1.000 línies 2-4 dies Baixa: es revisa en diagonal
> 1.000 línies Una setmana o més Gairebé nul·la: "LGTM"

I funciona com un bucle que es retroalimenta: un PR gran triga a revisar-se, i mentre espera acumula conflictes amb main, cosa que obliga a refer-ne parts, cosa que el fa més gran. La sortida del bucle és partir la feina.

Com es parteix a la pràctica, amb l'exemple de les reserves recurrents de Reservalia:

  1. PR 1: migració que afegeix la columna recurrencia a cites, sense fer-la servir. Zero risc.
  2. PR 2: el tipus Recurrencia a tipus-compartits i la lògica de càlcul, amb les seves proves unitàries. Ningú no la crida encara.
  3. PR 3: l'endpoint que la fa servir, darrere d'un flag desactivat.
  4. PR 4: la interfície a apps/web, també darrere del flag.
  5. PR 5: activació del flag.

Cinc PR de 100-200 línies, cadascun revisable en una estona, cadascun integrat el mateix dia. I una propietat valuosa: si alguna cosa falla, se sap exactament quin dels cinc ho va trencar.

  1. Protegir main: checks obligatoris, CODEOWNERS i merge queue

Els acords de la lliçó 02-01 —"ningú no empeny a main", "no es fusiona en vermell"— continuen depenent que tothom se'n recordi. Les regles de protecció de branca els converteixen en una cosa que el sistema fa complir.

Configuració de Reservalia per a main:

Regla Què impedeix Per què
Require a pull request before merging git push directe a main Tot canvi passa per verificació i revisió
Require status checks to pass: qualitat, test, build Fusionar en vermell És la regla 3 de l'acord, ja no negociable
Require branches to be up to date Fusionar sobre una base antiga Evita el "verd que es trenca en fusionar"
Require 1 approval Fusionar el teu propi codi sense revisió Segona mirada sobre el disseny
Dismiss stale approvals Aprovar i després canviar el codi L'aprovació es refereix a un diff concret
Require linear history Historial en forma d'espaguetis Un main lineal és llegible i bisecable
Block force pushes Reescriure l'historial de main Un force-push a main és irreversible a la pràctica
Include administrators Que la Marta se salti les seves pròpies regles Una excepció converteix la regla en suggeriment

CODEOWNERS declara qui ha de revisar segons la ruta tocada. Es desa a .github/CODEOWNERS:

# Per defecte, qualsevol canvi el revisa l equip
*                             @reservalia/equip

# Les migracions i el pipeline necessiten mirada experta
apps/api/src/db/migracions/   @reservalia/marta @reservalia/nuria
.github/workflows/            @reservalia/nuria
infra/                        @reservalia/nuria

Combinat amb "require review from Code Owners", garanteix que un canvi en una migració no es fusioni sense que el vegi algú que n'entén les conseqüències.

La merge queue resol un problema real i poc conegut. Escenari: els PR A i B són tots dos verds, tots dos partint del mateix main. A es fusiona. B continua dient verd, però no es va provar mai al costat d'A. Si A ha reanomenat una funció que B fa servir, main es posa vermella després del merge, sense que cap PR estigués en vermell.

flowchart LR
    A["PR A verd<br/>base: main@X"] --> Q["merge queue"]
    B["PR B verd<br/>base: main@X"] --> Q
    Q --> C1["prova A sobre main@X"] --> C2["prova B sobre main@X+A"]
    C2 -- verd --> M["merge d A i B"]
    C2 -- vermell --> R["B surt de la cua<br/>main continua sana"]

La cua construeix una branca temporal amb els canvis ja encuats i verifica cada PR contra el resultat dels anteriors. Amb dos o tres PR al dia no compensa; a partir d'una desena diària sobre el mateix repositori, evita que main es trenqui diverses vegades per setmana. Reservalia no la necessita avui; sí que la necessitaria amb quinze desenvolupadors.

  1. Evitar execucions inútils: paths i concurrency

Cada execució costa minuts i, sobretot, costa atenció. Dos mecanismes eviten gastar-los en va.

concurrency: cancel·lar execucions obsoletes. En Diego empeny tres vegades seguides a la seva branca en deu minuts. Sense configuració, tindràs tres pipelines complets corrent alhora, i només l'últim importa:

concurrency:
  group: ci-${{ github.workflow }}-${{ github.ref }}   # una cua per branca
  cancel-in-progress: true                             # cancella l anterior

github.ref identifica la branca, així que cada branca té la seva pròpia cua i els PR no es cancel·len entre si. Avís important: a main convé no cancel·lar (cancel-in-progress: false o una condició sobre la branca), perquè cada commit de main produeix un artefacte publicable i no en vols perdre cap.

paths: no executar el que no aplica. Un canvi en un .md no necessita construir imatges. La manera robusta no és filtrar l'esdeveniment sencer —ja vam veure a la 02-02 que això deixa els checks obligatoris esperant per sempre—, sinó detectar què ha canviat i saltar-se la feina cara, mantenint el check verd:

      - uses: dorny/paths-filter@v3
        id: canvis
        with:
          filters: |
            codi:
              - 'apps/**'
              - 'packages/**'
              - 'package-lock.json'

      - name: Construir la imatge
        if: steps.canvis.outputs.codi == 'true'        # ← el step se salta, el job surt verd
        run: docker build -f apps/api/Dockerfile .

Així el check build sempre reporta un resultat, i el PR de documentació es fusiona sense esperar tres minuts per res.

  1. Estratègies de merge, traçabilitat i lead time

Com s'integra un PR a main té conseqüències que van més enllà de l'estil.

Estratègia Què deixa a main Efecte en la traçabilitat Efecte en el lead time
Merge commit Tots els commits del PR + un commit de fusió Historial complet però ramificat; git bisect es complica El primer commit pot ser molt anterior al merge
Squash Un sol commit per PR Un commit = un canvi = un artefacte; ideal per a bisect Es perden les dates intermèdies
Rebase Els commits del PR, reescrits sobre main Historial lineal i detallat Cada commit conserva la seva data original

Reservalia fa servir squash, i les raons són concretes:

  • Un commit de main = un artefacte. Com que el job publicar etiqueta la imatge amb el SHA, un main amb un commit per PR fa que reservalia/api:a3f9c21 es correspongui exactament amb un pull request revisable. Amb merge commits, un sol merge introdueix diversos SHA i la correspondència es difumina.
  • git bisect funciona bé. Cada commit de main és un estat complet i verificat. Amb merge commits ensopegues amb commits intermedis trencats ("wip", "arreglant el test").
  • Els commits de treball deixen d'importar. En Diego pot fer quinze commits amb missatges de per casa; el que queda escrit és el títol del PR, que és el que valida commitlint (lliçó 02-05).

Ara l'efecte sobre el lead time for changes de la lliçó 01-05, que es calcula des de la data del commit fins al desplegament:

  • Amb squash, el commit resultant neix en el moment del merge, així que el lead time mesurat cobreix només merge → desplegament i subestima el temps real: no compta el que el canvi va passar esperant revisió.
  • Amb merge commit o rebase, es conserva la data del primer commit i el lead time inclou tota l'espera, amb la qual cosa reflecteix millor la realitat.

Si fas servir squash —com Reservalia—, el mesurament honest consisteix a prendre com a origen la data del primer commit del PR (dada que l'API de GitHub proporciona) en comptes de la del commit fusionat. No és un detall menor: és la diferència entre mesurar 40 minuts i mesurar els 6,2 dies reals de la línia base.

  1. Entorns efímers de previsualització per PR

Un entorn de previsualització és un desplegament temporal i aïllat del codi d'un pull request, amb la seva pròpia URL: pr-482.dev.reservalia.com. Neix en obrir el PR i es destrueix en tancar-lo.

Per a què serveix, en ordre de valor real: la Marta obre l'enllaç i veu funcionar el canvi en comptes d'imaginar-se'l llegint el diff; s'hi poden executar les proves E2E crítiques de la 02-04; i algú de negoci valida el canvi sense instal·lar res.

I el que cal tenir en compte abans de muntar-lo: costa diners (cada PR obert consumeix recursos, i sense destrucció automàtica s'acumulen), necessita dades (una base de dades per entorn, poblada amb dades fictícies: mai una còpia de producció amb dades de clients reals) i no tot s'ho mereix: a Reservalia té molt més sentit per a apps/web, que és visual i barata de desplegar a S3, que per a apps/api.

La mecànica de desplegament i destrucció és matèria del mòdul 3; aquí n'hi ha prou de conèixer el concepte i saber que l'artefacte ja està llest per alimentar-lo.

  1. El ci.yml complet de Reservalia

Aquest és el resultat del mòdul sencer, amb les parts ja explicades resumides perquè se'n vegi l'estructura:

# .github/workflows/ci.yml
name: CI

on:
  pull_request: { branches: [main] }
  push:         { branches: [main] }

concurrency:
  group: ci-${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}   # no cancellar mai main

env:
  TZ: Europe/Madrid

jobs:
  qualitat:                                  # ~50 s
    name: Qualitat
    runs-on: ubuntu-22.04
    timeout-minutes: 10
    steps:
      # checkout · setup-node (.nvmrc + cache) · npm ci
      - run: npx prettier --check .
      - run: npm run lint                    # --max-warnings 0
      - run: npm run typecheck               # tsc --noEmit

  test:                                      # ~3 min
    name: Proves
    runs-on: ubuntu-22.04
    timeout-minutes: 15
    services:
      postgres:
        image: postgres:16.3
        env: { POSTGRES_USER: reservalia, POSTGRES_PASSWORD: ci, POSTGRES_DB: reservalia_test }
        ports: ['5432:5432']
        options: --health-cmd "pg_isready -U reservalia -d reservalia_test" --health-retries 10
    env:
      DATABASE_URL: postgres://reservalia:ci@localhost:5432/reservalia_test
    steps:
      # checkout · setup-node · npm ci
      - run: npm run test:unitat --workspaces --if-present
      - run: npm run migrate --workspace apps/api
      - run: npm run test:integracio --workspace apps/api
      - run: npm run test --workspace apps/api -- --coverage

  build:                                     # ~4 min
    name: Construccio
    runs-on: ubuntu-22.04
    timeout-minutes: 15
    steps:
      # checkout · setup-node · npm ci
      - run: npm run build
      - run: test -f apps/api/dist/index.js && test -d apps/web/dist
      - uses: docker/build-push-action@v5
        with: { context: ., file: apps/api/Dockerfile, push: false,
                cache-from: 'type=gha', cache-to: 'type=gha,mode=max' }

  publicar:                                  # ~2 min · nomes a main
    name: Publicar artefacte
    runs-on: ubuntu-22.04
    needs: [qualitat, test, build]
    if: github.ref == 'refs/heads/main'
    permissions: { id-token: write, contents: read }
    steps:
      # checkout · credencials OIDC d AWS · login a ECR
      - uses: docker/build-push-action@v5
        with: { context: ., file: apps/api/Dockerfile, push: true,
                tags: '${{ steps.ecr.outputs.registry }}/reservalia/api:${{ steps.meta.outputs.sha_curt }}' }
flowchart LR
    E["pull_request / push a main"] --> Q["qualitat ~50 s"]
    E --> T["test ~3 min"]
    E --> B["build ~4 min"]
    Q --> P["publicar ~2 min<br/>nomes a main"]
    T --> P
    B --> P

Tres propietats d'aquest disseny que convé subratllar:

  1. qualitat, test i build corren en paral·lel. El temps total d'un PR és el del job més lent —uns 4 minuts—, no la suma dels tres. Molt per sota del límit de 10 minuts que va fixar l'equip.
  2. publicar depèn dels tres i només s'executa a main. Un PR no publica mai; un main verd sempre deixa artefacte.
  3. Els tres primers són els checks obligatoris de la regla de protecció de branca de l'apartat 4. Sense ells en verd, el botó de fusionar està desactivat.

Errors Comuns i Consells

Error 1: branques de vida llarga amb un pipeline excel·lent. És l'error que resumeix el mòdul. Cap YAML no compensa integrar cada tres setmanes.

Error 2: adoptar Git Flow per costum. Si el teu producte té un únic desplegament i no manté versions antigues, la branca develop només serveix per retardar la integració. Error 3: no marcar "include administrators"; el dia que algú amb permisos se salta el check "perquè corre pressa", la regla deixa d'existir per a tothom.

Error 4: filtrar amb paths un esdeveniment el check del qual és obligatori. El PR queda esperant eternament un resultat que no arribarà mai. Filtra a nivell de step, no d'esdeveniment.

Error 5: cancel·lar execucions a main. cancel-in-progress: true sense excepció per a main et deixa commits fusionats sense artefacte publicat.

Consell 1: mesura la mida mitjana dels teus PR. Si supera les 400 línies, el problema de l'equip no és el pipeline: és com es parteix la feina.

Consell 2: revisa els temps del pipeline cada mes. És la mètrica que es degrada més silenciosament. Tracta-la com un pressupost: per afegir-hi tres minuts, busca d'on treure'ls.

Consell 3: escriu les regles de protecció al README.md; que l'equip sàpiga què s'exigeix i per què redueix la fricció i les peticions d'excepció.

Exercicis

Exercici 1

Un equip de 6 persones desenvolupa un SaaS amb desplegament únic. Fa servir Git Flow: branques feature/ de 2-3 setmanes, develop, branques release/ setmanals i hotfix/. Tenen un pipeline complet i ben fet. Explica tres raons per les quals no practiquen Integració Contínua i proposa la transició, indicant què canviar primer.

Exercici 2

Els PR A i B són verds sobre main@X. A reanomena calcularForats a calcularDisponibilitat i n'actualitza les crides. B afegeix una crida nova a calcularForats. Es fusionen tots dos. Descriu què passa, per què cap regla de protecció no ho va evitar i quins dos mecanismes ho haurien impedit.

Exercici 3

Reservalia mesura un lead time de 41 minuts, però la Marta sap que un canvi triga dies des que es comença. Explica d'on ve la discrepància i com corregir el mesurament.

Solucions

Solució 1. Tres raons: (1) les branques feature/ de 2-3 setmanes violen la pràctica central d'integrar almenys cada dia, així que el pipeline verifica branques cada vegada més divergents de develop; (2) develop actua com un magatzem intermedi: el codi està "integrat" allà, però main —el que de debò es desplega— només el rep setmanalment, amb la qual cosa la integració real és setmanal; (3) les branques release/ impliquen que el programari s'estabilitza després de desenvolupar-se, el contrari de "la branca principal sempre desplegable".

Transició, per ordre: primer escurçar les branques (partir la feina en PR de menys de 400 línies, amb feature flags per al que està incomplet), perquè és el canvi que produeix el benefici i el més difícil; segon, eliminar develop i fer que els PR vagin directes a main; tercer, suprimir les branques release/ desplegant des de main quan correspongui; quart, activar les regles de protecció amb els checks obligatoris. Començar per la configuració sense escurçar les branques no canviaria res.

Solució 2. En fusionar A, main deixa de tenir calcularForats. B continua verd perquè es va verificar contra main@X, on la funció encara existia; en fusionar-se, main es trenca: fallarà el typecheck del job qualitat i probablement les proves. Cap PR no va estar mai en vermell.

No ho va evitar cap regla perquè les regles comproven l'estat del PR, no el resultat de combinar-lo amb el que s'ha fusionat entremig. Els dos mecanismes que ho impedeixen: (a) require branches to be up to date before merging, que obliga B a actualitzar-se amb main —i llavors el seu pipeline falla abans del merge, com ha de ser—; i (b) la merge queue, que prova B sobre el resultat d'A abans de fusionar-lo i el treu de la cua si falla. La primera és gratis i n'hi ha prou per a equips petits; la segona escala millor amb volum alt de PR.

Solució 3. La discrepància ve de l'estratègia de squash. En fusionar, git crea un commit nou la data del qual és la del merge, de manera que el git show -s --format=%cI que fèiem servir a la 01-05 mesura únicament el tram merge → desplegament, que a Reservalia són uns 41 minuts: el temps del pipeline i del desplegament. Tot l'anterior —desenvolupament, espera de revisió, correccions— desapareix del càlcul.

Correcció: prendre com a origen la data del primer commit del pull request, disponible a l'API de GitHub, i desar-la a la taula desplegaments juntament amb el SHA. El lead time passa a mesurar-se des que la feina va començar de debò. Com a validació creuada, convé comparar aquesta xifra amb el temps mitjà entre l'obertura del PR i el seu merge; si difereixen molt, el coll d'ampolla és abans d'obrir el PR.

Conclusió

Amb aquesta lliçó es tanca el mòdul 2, i convé mirar d'on venim. En començar, Reservalia no tenia res: .github/workflows/ estava buit, en Diego compilava al seu portàtil i les proves s'executaven "quan algú se'n recordava". Ara:

  • L'equip té sis regles escrites abans que una sola línia de YAML, i les pràctiques que constitueixen la CI de debò: integració diària, build automàtica, main sempre desplegable, stop the line i retroalimentació ràpida.
  • Un ci.yml que es dispara a cada pull request i a cada push a main, sobre runners fixats, amb Node llegit del .nvmrc i un PostgreSQL 16.3 amb healthcheck per a les proves.
  • Una build reproduïble amb npm ci i el lockfile, i un Dockerfile multietapa amb imatge base fixada, usuari no root i .dockerignore.
  • Proves unitàries i d'integració amb criteri sobre què bloqueja el merge, cobertura tractada com a senyal i una política de quarantena per a les proves inestables.
  • Un job qualitat amb Prettier, ESLint a --max-warnings 0 i tsc --noEmit, i un pla realista per reduir el deute sense aturar l'equip.
  • Un job publicar que puja a ECR un artefacte immutable identificat pel SHA, llest per promocionar-se sense reconstruir-se, i un endpoint /version per saber sempre què s'està executant.
  • I les regles de protecció de main que converteixen els acords en una cosa que el sistema fa complir, juntament amb CODEOWNERS, concurrency i una estratègia de merge —squash— triada per les seves conseqüències sobre la traçabilitat, no per gust.

Quant a les mètriques DORA de la lliçó 01-05, el mòdul ha atacat sobretot el change failure rate: cada canvi es verifica en una màquina neta abans de fusionar-se. El lead time també ha millorat, encara que de manera parcial, perquè els PR són més petits. Però les altres dues mètriques continuen intactes, i la raó és simple: l'artefacte existeix, està publicat i verificat… i continua sent en Diego qui el desplega a mà. El ritual del divendres continua. Hem automatitzat la meitat esquerra del diagrama que vam dibuixar a la 01-01 i no hem tocat la dreta.

Això és exactament el que arrenca al mòdul 3, Desplegament Continu (CD). La primera lliçó, Introducció al Desplegament Continu, reprèn la distinció entre Lliurament Continu i Desplegament Continu —ara amb un pipeline real al davant— i defineix què necessita un equip abans de deixar que una màquina toqui producció: entorns reproduïbles, migracions segures, capacitat de tornar enrere i observabilitat suficient per assabentar-se abans que el client. A partir d'aquí, l'artefacte reservalia/api:a3f9c21 que avui espera a ECR començarà a viatjar sol fins a dev, staging i prod, i el divendres a la tarda tornarà a ser, senzillament, un divendres a la tarda.

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