Reservalia ha resolt dos dels quatre fronts, i en fer-ho ha engreixat el pipeline: el job seguretat de la lliçó anterior hi va afegir dos minuts, les dependències s'auditen, les imatges s'escanegen i es signen. El primer front —el pipeline és lent i s'està engreixant— torna ara amb tota la seva força, i amb una frase d'en Diego que l'equip repeteix: "si la CI triga més que anar a buscar un cafè, deixo de mirar-la". Aquesta lliçó va de recuperar aquell temps sense perdre verificació. Començarem per l'única cosa que no és negociable: mesurar abans de tocar res, perquè s'optimitza el que es mesura i no el que se sospita. Després recorrerem les quatre palanques ordenades per relació cost/benefici —memòria cau, paral·lelització, execució selectiva i elecció del runner—, amb les seves regles d'invalidació i els seus límits teòrics. Mirarem la factura, que creix més silenciosament que el temps. I veurem què passa quan l'equip triplica la seva mida. Tot això vigilant el risc que acompanya totes les optimitzacions: el verd fals, un pipeline rapidíssim que ja no comprova el que creiem. El que no farem aquí és extreure plantilles ni reutilitzar blocs: això és la 04-05.

Contingut

  1. Mesurar abans de tocar: on se'n va el temps
  2. Palanca 1: memòria cau de dependències, de compilació i de capes
  3. Palanca 2: paral·lelització i el límit d'Amdahl
  4. Palanca 3: execució selectiva al monorepo
  5. Palanca 4: elecció del runner
  6. Les quatre palanques comparades
  7. El cost: minuts, emmagatzematge i com evitar que la factura creixi sola
  8. Escalar amb l'equip: cues, merge queue i trenta persones
  9. El risc transversal: el verd fals
  10. Errors Comuns i Consells
  11. Exercicis
  12. Conclusió

  1. Mesurar abans de tocar: on se'n va el temps

La primera regla és incòmoda perquè contradiu l'instint: gairebé mai no encertes on és el temps. Tothom té una hipòtesi —"són les proves"— i a la majoria de pipelines la resposta real és la instal·lació de dependències o la construcció de la imatge.

GitHub Actions dona les dades sense instal·lar res: a la interfície, cada execució mostra la durada per job i, en obrir un job, per step. Per veure-ho de manera agregada, l'API:

gh api "repos/reservalia/reservalia/actions/runs?branch=main&per_page=50" \
  --jq '.workflow_runs[] | select(.name=="CI") | [.run_started_at, .updated_at] | @tsv'   # 1

gh api "repos/reservalia/reservalia/actions/runs/$RUN_ID/jobs" \
  --jq '.jobs[] | {nom: .name,
        segons: ((.completed_at|fromdate) - (.started_at|fromdate))}'                     # 2
  1. Cinquanta execucions, no una. Un sol mesurament barreja el soroll de la infraestructura amb la realitat; amb cinquanta es veu la mediana i, sobretot, la dispersió. Un job la durada del qual oscil·la entre 40 s i 4 min té un problema diferent del que sempre triga 3 min.
  2. Per job i per step. L'agregat diu quant triga el pipeline; el detall diu què cal tocar.

Aquest és el desglossament real de la CI de Reservalia després d'afegir el job seguretat, mesurat sobre les últimes cinquanta execucions de main:

Job Step Mediana % del job Observació
qualitat (0:50) npm ci (32 s) + prettier/lint/tsc (18 s) 50 s 64 % / 36 % La instal·lació domina
test (3:00) npm ci 32 s 18 %
unitàries 42 s 23 %
migracions + integració 1:46 59 % El gruix real
build (4:05) npm ci (32 s) + npm run build (1:08) 1:40 41 %
docker build 2:25 59 % El camí crític
seguretat (2:00) escaneigs 2:00 100 % Ja corre en paral·lel
publicar (2:00) build + push a ECR 2:00 100 % Reconstrueix la imatge

Tres conclusions que cap intuïció no hauria donat. El camí crític és build (4:05) + publicar (2:00) = 6 minuts, i dins d'ell mana docker build, no les proves. npm ci es repeteix quatre vegades, 32 segons cadascuna: no suma al rellotge perquè els jobs són paral·lels, però sí als minuts facturables. I publicar reconstrueix la imatge que build ja havia construït: dos minuts regalats. L'objectiu que fixa l'equip, coherent amb la regla dels 10 minuts de la 04-01, és baixar de 6 minuts a 3. I un advertiment metodològic: anota la línia base abans de tocar res. Sense ella, d'aquí a dos mesos ningú no sabrà si la memòria cau va servir d'alguna cosa o si el pipeline és més ràpid perquè algú va esborrar proves.

  1. Palanca 1: memòria cau de dependències, de compilació i de capes

És la palanca amb millor relació cost/benefici: s'implementa en minuts i estalvia a cada execució de cada job. Memòria cau de dependències. Reservalia la té des de la 02-02 (cache: npm a setup-node) i la 04-02 en va explicar la clau: el hash del lockfile. Dels 32 segons d'npm ci, uns 20 són descàrrega i 12 instal·lació; amb memòria cau calenta en queden uns 14 segons.

Memòria cau de compilació. TypeScript i Vite saben compilar de manera incremental si se'ls deixa desar el seu estat:

      - uses: actions/cache@v4
        with:
          path: |
            .tsbuildinfo
            apps/web/node_modules/.vite       # 1
          key: build-${{ runner.os }}-${{ hashFiles('**/*.ts','**/*.tsx','tsconfig*.json') }}
          restore-keys: build-${{ runner.os }}-   # 2
  1. Cada eina té el seu directori d'estat: .tsbuildinfo per a tsc, .vite per a Vite, .turbo o .nx amb un orquestrador de monorepo.
  2. restore-keys és aquí el que és decisiu. Una coincidència exacta de clau és rara —qualsevol canvi en qualsevol .ts la invalida—, però recuperar la memòria cau anterior permet recompilar només el que ha canviat. Sense restore-keys, la memòria cau de compilació gairebé mai no encerta i no serveix de res.

Memòria cau de capes de Docker. És la que més estalvia a Reservalia, perquè docker build són 2:25 dels 4:05 de build.

      - uses: docker/setup-buildx-action@v3          # 1
      - uses: docker/build-push-action@v5
        with:
          context: .
          file: apps/api/Dockerfile
          push: true
          tags: ${{ env.ECR }}/reservalia/api:${{ github.sha }}
          cache-from: type=registry,ref=${{ env.ECR }}/reservalia/api:cache   # 2
          cache-to:   type=registry,ref=${{ env.ECR }}/reservalia/api:cache,mode=max
  1. buildx és el constructor estès de Docker; sense ell no hi ha exportació de memòria cau.
  2. Memòria cau al registre (type=registry) en lloc de a GitHub (type=gha). La memòria cau de GitHub té un límit de 10 GB per repositori i expulsa el que menys es fa servir, així que en un repositori actiu es perd constantment; una etiqueta :cache al mateix ECR no té aquest problema, es comparteix entre jobs i branques, i viu on ja es té accés. mode=max desa també les capes intermèdies, que és el que fa que la memòria cau serveixi de debò.

L'ordre de les instruccions del Dockerfile és l'altra meitat del rendiment, i és gratis. Docker invalida una capa i totes les següents quan en canvien les entrades, així que el que canvia poc va a dalt:

FROM node:20.11.0-bookworm-slim AS deps
WORKDIR /app
COPY package.json package-lock.json ./     # 1 · nomes els manifests
RUN npm ci --omit=dev                       # 2 · capa cara, reutilitzada gairebe sempre
COPY . .                                    # 3 · el codi, al final
RUN npm run build
  1. Copiar només els manifests abans d'instal·lar és el truc central: mentre el lockfile no canviï, la capa de l'npm ci es reutilitza.
  2. Si el COPY . . fos abans del RUN npm ci, qualsevol canvi en qualsevol fitxer invalidaria la instal·lació i es reinstal·larien 418 paquets a cada commit.
  3. Un .dockerignore que exclogui node_modules, .git i dist redueix el context enviat al daemon, cosa que de vegades estalvia més que la memòria cau mateixa.

Amb la memòria cau de capes calenta, docker build baixa de 2:25 a uns 40 segons i el job build complet a 2:10.

  1. Palanca 2: paral·lelització i el límit d'Amdahl

Reservalia ja paral·lelitza els jobs independents des de la 02-07. El següent és partir el job més lent per dins, que en proves s'anomena sharding:

  test:
    strategy:
      fail-fast: false
      matrix:
        particio: [1, 2, 3]                                     # 1
    steps:
      - run: npm run test:unitat -- --shard=${{ matrix.particio }}/3
      - run: npm run test:integracio --workspace apps/api -- --shard=${{ matrix.particio }}/3

  test-complet:                                                  # 2
    needs: [test]
    if: always()
    runs-on: ubuntu-22.04
    steps:
      - run: '[ "${{ needs.test.result }}" = "success" ] || exit 1'
  1. Tres particions, tres màquines. Cadascuna executa un terç dels fitxers de prova. El repartiment el fa l'executor (Vitest i Jest ho admeten) i convé que sigui per durada estimada i no per nombre de fitxers, o una partició acabarà carregant amb totes les proves d'integració.
  2. El job agregador és imprescindible i s'oblida sempre. Els checks obligatoris de la protecció de branca es configuren per nom de job, i amb una matriu els noms passen a ser test (1), test (2), test (3). Si demà passes a cinc particions, els checks test (4) i test (5) no existeixen a la configuració i ningú no els exigeix: dues cinquenes parts de la suite deixen de bloquejar el merge sense que ningú se n'adoni. Un job agregador amb nom fix, del qual s'exigeix el resultat, resol el problema per sempre.

I ara el límit, que evita gastar diners sense guanyar temps. La llei d'Amdahl diu que la millora total està acotada per la part que no es pot paral·lelitzar. Al job test hi ha 32 s d'npm ci que passen a totes les particions i no es divideixen:

Particions Part sèrie Part paral·lela (2:28) Total Guany
1 32 s 2:28 3:00
2 32 s 1:14 1:46 −41 %
3 32 s 49 s 1:21 −55 %
6 32 s 25 s 0:57 −68 %

D'1 a 3 particions es guanya un 55 %; de 3 a 6, només un 13 % més a canvi de duplicar la despesa, i de 6 a 12 amb prou feines un 8 % addicional. Reservalia es queda en 3. La conclusió general és que la paral·lelització té rendiments ràpidament decreixents: el terra el posa la part sèrie —instal·lació, arrencada del runner, descàrrega del codi—, i per això la memòria cau de l'apartat anterior millora també el sostre d'aquesta palanca.

  1. Palanca 3: execució selectiva al monorepo

La palanca més rendible d'un monorepo: no executar el que el canvi no pot haver trencat. Requereix conèixer el graf de dependències intern, que a Reservalia és aquest:

flowchart TD
    T["packages/tipus-compartits"] --> A["apps/api"]
    T --> W["apps/web"]
    D[".github/ · infra/ · package-lock.json"] -.->|"afecta a tot"| A
    D -.-> W

La regla es llegeix cap amunt: un canvi a tipus-compartits obliga a verificar-ho tot, perquè els dos consumidors en depenen; un canvi només a apps/web no pot trencar l'API; i un canvi a docs/ no trenca res.

  canvis:                                           # 1 · job barat que decideix
    runs-on: ubuntu-22.04
    outputs:
      api: ${{ steps.filtre.outputs.api }}
      web: ${{ steps.filtre.outputs.web }}
    steps:
      - uses: actions/checkout@v4
      - uses: dorny/paths-filter@v3
        id: filtre
        with:
          filters: |
            api:
              - 'apps/api/**'
              - 'packages/tipus-compartits/**'      # 2 · la dependencia, explicita
              - 'package-lock.json'
              - '.github/workflows/**'
            web:
              - 'apps/web/**'
              - 'packages/tipus-compartits/**'
              - 'package-lock.json'

  build-api:
    needs: [canvis]
    if: needs.canvis.outputs.api == 'true'          # 3 · el job es salta sencer
    runs-on: ubuntu-22.04
    steps: [ ... ]
  1. Un job previ d'uns 10 segons calcula què ha canviat i ho publica com a outputs: el mecanisme de la 04-01 aplicat a una decisió.
  2. La dependència interna es declara a mà al filtre, i aquest és el punt fràgil: si algú afegeix un paquet nou a packages/ i oblida incloure'l, els jobs es saltaran canvis que sí que importaven. Eines com Turborepo o Nx deriven el graf del mateix codi i eviten aquell oblit, a canvi d'una eina més per mantenir. Amb tres paquets el filtre a mà és raonable; amb trenta, no.
  3. if a nivell de job salta el job sencer, no només un step.

I aquí hi ha la propietat que cal entendre bé: un job saltat per if reporta "success" —èxit neutre—, no una fallada. GitHub el tracta com a complert a efectes dels checks obligatoris, de manera que un PR que només toca apps/web es pot fusionar encara que build-api no s'hagi executat. És exactament el comportament que es busca, i també el motiu pel qual a la 02-07 es va recomanar filtrar per job o per step i mai filtrar l'esdeveniment sencer amb on: paths: un esdeveniment filtrat no produeix cap resultat, i el check obligatori es queda esperant per sempre. Un avís d'honestedat: l'execució selectiva és la palanca amb més risc de verd fals de les quatre, perquè la seva premissa és "això no es pot haver trencat" i aquella premissa es pot equivocar. Hi tornarem a l'apartat 9.

  1. Palanca 4: elecció del runner

Opció Avantatge Inconvenient Cost relatiu
Estàndard (2 vCPU, 7 GB) Barat i sempre disponible Lent en tasques amb molts nuclis ×1
Gran (8 o 16 vCPU) Compila i construeix molt més ràpid Costa 4-8 vegades més per minut ×4 a ×8
Més jobs en paral·lel Escala en amplada Multiplica els npm ci i les arrencades ×N
Autoallotjat Sense cost per minut; disc i memòria cau persistents S'ha de mantenir i assegurar Cost fix

Tres criteris per decidir. Una màquina gran només ajuda si la tasca fa servir diversos nuclis: docker build i tsc sí; esperar una base de dades, no. Pagar ×4 per un job que espera a la xarxa és llençar els diners, i per això convé mirar l'ús de CPU abans de pujar de mida. Més màquines escalen millor que màquines més grans quan la feina és divisible, fins al límit d'Amdahl de l'apartat 3. I l'arrencada en fred és un cost fix de 10 a 30 segons per job —aprovisionar la màquina i descarregar el codi—: amb vint jobs petits es paguen diversos minuts només arrencant, així que partir en excés deixa de compensar. Sobre els runners autoallotjats, l'advertiment que enllaça amb les dues lliçons anteriors: eliminen el cost per minut i permeten una memòria cau local persistent, però reintrodueixen el problema dels pipelines de llarga vida de la 04-01 —l'estat s'acumula entre execucions— i un risc de seguretat seriós: un runner autoallotjat que executi workflows de pull requests de forks està executant codi de desconeguts dins de la teva xarxa. Si es fan servir, han de ser efímers (una màquina nova per job, destruïda en acabar) i mai acceptar feines de forks. Reservalia no els necessita: amb el seu volum, els runners allotjats surten més barats que el temps de la Nuria mantenint-los.

  1. Les quatre palanques comparades

Aplicades a Reservalia, en l'ordre en què convé executar-les, amb el seu risc associat:

Palanca Esforç Estalvi estimat Risc Risc concret
Memòria cau de dependències Molt baix −18 s per job Baix Clau mal construïda: verd fals
Memòria cau de capes Docker Baix −1:45 a build Baix Dockerfile mal ordenat: no encerta mai
Reutilitzar la imatge a publicar Baix −1:50 Molt baix Cap de rellevant
Sharding de proves ×3 Mitjà −1:39 a test Mitjà Sense job agregador: checks no exigits
Execució selectiva Mitjà −2 a −4 min en PR parcials Alt Filtre incomplet: no es prova el que ha canviat
Runner gran per a build Molt baix −40 s Baix Cost ×8 sense mesurar si compensa

I el resultat sobre el camí crític, mesurant amb la mateixa vara:

Job Abans Després Què ho ha aconseguit
qualitat 0:50 0:35 Memòria cau de dependències
test 3:00 1:21 Memòria cau + sharding ×3
build 4:05 2:10 Memòria cau de capes al registre
seguretat 2:00 2:00 Sense canvis: ja era paral·lel
publicar 2:00 0:50 Reutilitza el digest en comptes de reconstruir
Camí crític 6:05 3:00

Objectiu complert amb marge i sense haver eliminat ni una sola verificació: això és exactament el que distingeix una optimització d'una retallada.

  1. El cost: minuts, emmagatzematge i com evitar que la factura creixi sola

El temps el pateix l'equip; la factura la pateix algú que no és a la conversa, i per això creix sense que ningú se n'assabenti. Tres partides:

Minuts de runner. Es facturen per job i per minut començat, així que paral·lelitzar redueix el rellotge però augmenta la despesa: els 6 minuts de rellotge de Reservalia eren uns 12 minuts-màquina sumant els cinc jobs. Amb 12 desplegaments i uns 40 pull requests per setmana, el compte surt sol. Emmagatzematge d'artefactes i memòries cau. És la partida que sorprèn. Un artefacte web-dist de 40 MB per execució, amb la retenció per defecte de 90 dies i 200 execucions al mes, són uns 24 GB acumulats que ningú no tornarà a obrir.

      - uses: actions/upload-artifact@v4
        with: { name: web-dist, path: apps/web/dist, retention-days: 7 }   # 1
  1. Abaixar retention-days és la mesura de més rendiment per caràcter escrit. Set dies basten per investigar una fallada; noranta només serveixen per pagar. La retenció també es pot fixar a nivell d'organització.

Les mesures que eviten que la factura creixi sola, per ordre d'eficàcia:

  1. concurrency amb cancel-in-progress a les branques de PR: si en Diego empeny tres vegades en deu minuts, només corre l'última. Amb l'excepció de main, que no es cancel·la mai (02-07).
  2. No executar en esborranys: un if: github.event.pull_request.draft == false evita el pipeline complet mentre el PR està en construcció.
  3. timeout-minutes a tots els jobs, perquè un job penjat consumeix sis hores facturables abans de rendir-se per defecte; i retenció curta d'artefactes, amb neteja periòdica de les memòries cau antigues.

Un criteri per a les converses de pressupost: compara el cost del pipeline amb el cost del temps de l'equip. Cinc persones esperant dos minuts de més en cadascuna de quaranta execucions setmanals són diverses hores de feina cada setmana, que valen força més que la diferència entre un runner estàndard i un de gran. La conclusió no sempre és gastar més, però la comparació honesta és aquella i gairebé mai no es fa.

  1. Escalar amb l'equip: cues, merge queue i trenta persones

Un pipeline de 3 minuts per a tres persones pot ser una espera de 25 minuts per a trenta, sense que canviï ni una sola línia de YAML. El que canvia és la contenció.

Amb 3 persones Amb 30 persones
Els jobs arrenquen a l'instant Els jobs esperen pel límit de concurrència del pla
main es trenca una vegada al mes main es trenca diverses vegades per setmana per PR incompatibles entre si
Una prova inestable molesta Una prova inestable al 3 % falla diverses vegades al dia i bloqueja tothom
La memòria cau encerta gairebé sempre La memòria cau s'invalida diàriament: el lockfile canvia molt més

Tres respostes concretes, i un efecte poc intuïtiu al final. La cua d'execució és el primer límit que es toca: els plans tenen un màxim de jobs concurrents per organització, i en assolir-lo els jobs esperen encara que cadascun sigui rapidíssim. Es detecta mesurant el temps d'espera a més del d'execució —si creix, el problema no és el teu YAML— i es resol pujant el límit o reduint el nombre de jobs per execució. La merge queue de la 02-07 passa de luxe a necessitat: amb trenta persones fusionant el mateix dia, la situació de dos PR verds per separat que trenquen main en ajuntar-se passa diverses vegades per setmana; la cua verifica cada PR contra el resultat dels anteriors, i el seu cost —més execucions— és justament el que s'estalvia en main trencada. I les proves inestables deixen de ser una molèstia i es converteixen en un impost: una que falla el 3 % de les vegades, amb 100 execucions diàries, produeix tres rellançaments al dia; la política de quarantena de la 02-04 —treure-la del camí bloquejant, amb tiquet, propietari i termini— és el que impedeix que l'equip aprengui a rellançar sense mirar. I l'efecte poc intuïtiu: la memòria cau encerta menys com més gran és l'equip, perquè el lockfile canvia més sovint. És un altre argument a favor d'uns restore-keys ben pensats, que permeten aprofitar una memòria cau parcial en lloc de perdre-ho tot per un byte.

  1. El risc transversal: el verd fals

Totes les optimitzacions comparteixen el mateix mode de fallada: el pipeline continua en verd però ja no comprova el que creiem, i és una fallada silenciosa perquè ningú no investiga per què la CI ha passat.

Optimització Com produeix un verd fals Com detectar-ho
Memòria cau de dependències Clau sense hash del lockfile: es prova amb dependències antigues Imprimir la versió instal·lada d'un paquet clau
Memòria cau de compilació Es reutilitza un artefacte compilat d'un altre commit Comparar la data dels fitxers generats
Sharding Una partició no executa res per un patró mal escrit Comptar les proves executades davant del total
Execució selectiva El filtre no cobreix un canvi que sí que afectava Execució nocturna completa i sense filtres
continue-on-error Una fallada real queda en groc i no bloqueja Auditar els continue-on-error del repositori

Les dues comprovacions que més rendiment donen, per si només en pots implementar dues:

# 1 · El nombre total de proves executades no ha de baixar mai
TOTAL=$(jq '[.testResults[].assertionResults[]] | length' informe-*.json | paste -sd+ | bc)
[ "$TOTAL" -ge "$MINIM_ESPERAT" ] || { echo "::error::nomes $TOTAL proves executades"; exit 1; }
  1. Comptar les proves i fallar si baixen. És la defensa més barata i més eficaç que existeix contra el verd fals: cobreix el sharding mal configurat, el filtre que es va saltar un paquet i el fitxer de proves que va deixar de descobrir-se per un canvi de nom. Cinc línies de Bash que valen per una política sencera. La segona és el pipeline nocturn de la 04-01 executat sense cap optimització: sense memòria cau, sense filtres de ruta i amb la suite completa. És l'execució de referència. Si el nocturn falla i la CI dels PR és verda, la diferència entre tots dos és la fallada de la teva optimització, i aquell diagnòstic val més que cap auditoria manual. I una regla d'higiene que resumeix l'apartat: cada vegada que optimitzis, executa una vegada el pipeline sense l'optimització i compara els informes; si el nombre de proves, de fitxers analitzats o de troballes detectades no coincideix, no has optimitzat res, has deixat de comprovar coses.

Errors Comuns i Consells

Error 1: optimitzar sense mesurar. S'ataca el que se sospita —les proves— i el temps era a docker build o en quatre npm ci. Mitja hora de mesurament estalvia dies de feina mal dirigida. Error 2: no anotar la línia base, amb la qual cosa després no es pot demostrar la millora ni detectar la regressió.

Error 3: clau de memòria cau fixa. key: npm-cache no s'invalida mai: acabes provant contra dependències de fa mesos; la clau sempre deriva del contingut. Error 4: desar en memòria cau node_modules/ en lloc del directori de descàrregues, amb binaris compilats per a una altra màquina. Error 5: paral·lelitzar sense job agregador: els checks obligatoris es configuren per nom, i en passar de 3 a 5 particions dues d'elles deixen d'exigir-se sense que ningú se n'adoni.

Error 6: sharding excessiu, que multiplica el cost per la part sèrie i amb prou feines millora el rellotge (Amdahl). Error 7: filtres de ruta incomplets que no inclouen els paquets interns dels quals depèn cada aplicació: la manera més directa d'aconseguir un verd fals. Error 8: runners autoallotjats no efímers, que acumulen estat i, si accepten PR de forks, executen codi de desconeguts dins de la teva xarxa.

Consell 1: mesura el temps d'espera a més del d'execució. Si creix, el problema és de capacitat i no del teu YAML. Consell 2: posa un pressupost de temps per pipeline i tracta'l com a tal: per afegir tres minuts, busca d'on treure'ls. Consell 3: revisa les durades una vegada al mes, que és la mètrica que es degrada més silenciosament. Consell 4: compta les proves executades a cada execució, la defensa més barata contra el verd fals.

Exercicis

Exercici 1

La CI d'un equip triga 22 minuts i tothom assumeix que "són les proves E2E". Descriu com obtindries el desglossament real, quines tres hipòtesis alternatives comprovaries primer i com decidiries per on començar amb un criteri de cost/benefici.

Exercici 2

Un equip activa execució selectiva amb paths-filter. Dues setmanes després, un canvi a packages/tipus-compartits arriba a producció i trenca l'API, amb la CI en verd durant tot el procés. Explica exactament què va fallar, per què la porta de qualitat no ho va aturar i quines tres mesures ho eviten.

Exercici 3

Reservalia ha de triar entre dues inversions: (a) un runner de 16 vCPU per a build, que baixa docker build de 2:25 a 45 s i costa ×8 per minut; (b) implementar la memòria cau de capes al registre, que el baixa a 40 s amb memòria cau calenta i el deixa en 2:25 en fred. Argumenta quina triar, en quin ordre i en quines circumstàncies canviaria la resposta.

Solucions

Solució 1. Com mesurar: extreure amb l'API la durada per job i per step de les últimes cinquanta execucions de main, calcular la mediana i la dispersió de cada step i ordenar per temps absolut. Importa la mediana i no la mitjana —una execució anòmala ho distorsiona— i importa la dispersió, perquè un step molt variable assenyala espera de xarxa o contenció, no feina real. Amb això s'identifica el camí crític: els jobs paral·lels no sumen, així que optimitzar un job que no és al camí crític no redueix ni un segon el rellotge. Les tres hipòtesis alternatives, per probabilitat: (1) la instal·lació de dependències, repetida a cada job i sense memòria cau o amb la memòria cau fallant —comprovable mirant si el log diu cache hit o cache miss—; (2) la construcció de la imatge, sense memòria cau de capes o amb un Dockerfile que copia el codi abans d'instal·lar, de manera que no encerta mai; (3) el temps d'espera a la cua, que no és execució en absolut i es veu comparant run_started_at amb l'inici real del primer job.

Com decidir: ordenar les intervencions per (estalvi estimat ÷ esforç) i executar-les de dalt a baix, sempre començant pel que és al camí crític. A la pràctica, la memòria cau de dependències i l'ordre del Dockerfile són gairebé sempre el primer —hores de feina, minuts d'estalvi per execució i risc baix—; treure els E2E del pipeline de PR i portar-los al nocturn sol ser el segon; i només aleshores té sentit plantejar-se runners més grans.

Solució 2. El que va fallar: el filtre del job de l'API declarava apps/api/** però no incloïa packages/tipus-compartits/**. En canviar només el paquet compartit, paths-filter va decidir que l'API no estava afectada, el job es va saltar i —per la propietat de l'apartat 4— va reportar èxit neutre. El PR va mostrar tots els checks en verd.

La porta de qualitat no ho va aturar precisament perquè va funcionar com està dissenyada: els checks obligatoris comproven que el job tingui resultat success, i un job saltat el té. La porta no distingeix "verificat i correcte" de "no verificat". Aquest és el preu explícit de l'execució selectiva, i per això és la palanca de més risc de les quatre. Les tres mesures: (1) completar el filtre perquè tot consumidor inclogui les rutes de les seves dependències internes, i afegir CODEOWNERS sobre .github/workflows/ perquè un canvi als filtres el revisi algú que entengui el graf; (2) substituir el filtre escrit a mà per una eina que derivi el graf del codi (Turborepo, Nx o els mateixos workspaces de npm), de manera que afegir un paquet no exigeixi recordar-se de res; i (3) executar al pipeline nocturn la suite completa sense filtres, que hauria detectat el problema en menys de 24 hores encara que les dues primeres mesures fallessin. Complementàriament, la comprovació del nombre de proves executades de l'apartat 9 hauria donat el senyal al mateix PR: passar de 480 proves a 120 és una dada visible.

Solució 3. La resposta correcta és (b) primer, i (a) probablement mai, per quatre raons.

Cost. La memòria cau és una configuració: s'implementa en mitja hora i no afegeix ni un cèntim al minut. El runner gran multiplica per vuit el cost d'un job que s'executa a cada pull request i a cada push a main: unes 240 execucions al mes a Reservalia. Resultat. Són equivalents en el cas bo —45 s davant de 40 s— i la memòria cau guanya en el cas típic, perquè la majoria de commits no toquen el lockfile ni les primeres capes del Dockerfile, que és justament quan la memòria cau encerta.

Compatibilitat. No són excloents, però s'apliquen per ordre: amb la memòria cau calenta, la feina restant de docker build és tan petita que un runner gran amb prou feines la millora, així que optimitzar primer i ampliar la màquina després evita pagar ×8 per feina que es podia no fer. Risc. El del runner gran és només econòmic; el de la memòria cau és un verd fals si la clau està mal construïda, i es mitiga amb el de l'apartat 9 i amb una propietat de les capes de Docker: s'identifiquen pel hash del seu contingut, així que la seva invalidació és criptogràfica i no depèn de cap convenció.

Quan canviaria la resposta: si el docker build en fred fos al camí crític d'alguna cosa urgent —un rollback que exigeix reconstruir—; si la construcció fes servir compilació nativa multinucli (Rust, C++, imatges multiplataforma), on 16 vCPU donen una millora que cap memòria cau no dona; o si l'equip publiqués desenes d'imatges diferents i la memòria cau encertés poques vegades. A Reservalia, amb un sol servei i commits petits, no passa res d'això.

Conclusió

El primer front està tancat, i amb números: la CI de Reservalia ha passat de 6 minuts a 3, sense eliminar ni una verificació. Ho van aconseguir, en aquest ordre, la memòria cau —de dependències per hash del lockfile, de compilació amb restore-keys, i de capes de Docker exportada al registre en lloc del magatzem de GitHub, recolzada en un Dockerfile que copia els manifests abans que el codi—; el sharding de la suite en tres particions, amb un job agregador de nom fix perquè els checks obligatoris continuïn exigint-se; l'execució selectiva basada en el graf del monorepo, amb paths-filter a nivell de job i l'èxit neutre com a peça clau; i una decisió conscient sobre el runner, que en aquest cas va ser no gastar més. Però la lliçó de fons no és cap de les quatre palanques: és el mètode. Primer mesurar —per job i per step, sobre cinquanta execucions, anotant la línia base—, perquè la intuïció assenyalava les proves i el temps era a docker build i en quatre npm ci repetits. Després ordenar per cost/benefici i aplicar de dalt a baix, sempre sobre el camí crític. I comprovar després de cada pas que es continua verificant el mateix, perquè totes les optimitzacions comparteixen el mateix mode de fallada: el verd fals. Comptar les proves executades i mantenir un nocturn sense optimitzacions com a execució de referència són les dues defenses que costen gairebé res i detecten gairebé tot. A això s'hi suma la mirada al cost —minuts, retenció d'artefactes, concurrency, esborranys— i la previsió del que canvia quan l'equip triplica: cues d'execució, merge queue i proves inestables que passen de molèstia a impost.

Queda, tanmateix, un problema que cap optimització no arregla i que aquesta lliçó ha agreujat. El ci.yml ja té cinc jobs i, entre ell, cd.yml, infra.yml, rollback.yml i nocturn.yml, el mateix bloc de checkout, setup-node i npm ci amb la seva memòria cau apareix sis vegades copiat: cada millora que hem introduït es va haver d'aplicar sis vegades, i la propera persona que en toqui una deixarà les altres cinc enrere. A sobre, el mòdul 5 hi afegirà una aplicació mòbil i uns microserveis que necessiten exactament el mateix. La lliçó següent, Pipeline as Code: Plantilles, Reutilització i Proves del Pipeline, tracta el pipeline com el que és —codi de producció— i li aplica el que s'aplica al codi: extreure el que es repeteix en composite actions i workflows reutilitzables, versionar-ho, revisar-ho i, sobretot, provar-ho, perquè fins avui l'única manera que ha tingut Reservalia de provar un canvi de pipeline ha estat fusionar-lo i mirar.

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