A la lliçó anterior l'equip de Reservalia va acordar les seves sis regles sense obrir cap editor. Ara toca el contrari: escriure el primer fitxer de configuració real i deixar que la màquina comenci a treballar. Al final d'aquesta lliçó, cada pull request obert contra main dispararà automàticament un pipeline que descarrega el codi en una màquina neta, instal·la exactament Node 20.11.0, aixeca un PostgreSQL 16.3 de debò i executa les proves. Construirem aquest fitxer des de zero i línia a línia, sense copiar plantilles màgiques d'internet, perquè entendre què fa cada paraula clau és la diferència entre mantenir un pipeline i resar-li. A més veurem on s'executa realment aquesta feina —runners allotjats davant d'autoallotjats, amb el seu cost i les seves implicacions—, com es passen variables i secrets, i —potser el més útil de tot— com depurar un flux de treball que falla quan el registre (log) no diu res evident.

Contingut

  1. On viu el pipeline i què el dispara
  2. El primer ci.yml de Reservalia, línia a línia
  3. checkout i setup-node: les dues accions que faràs servir sempre
  4. Runners allotjats davant d'autoallotjats
  5. Variables d'entorn i secrets
  6. Serveis de suport: un PostgreSQL real per a les proves
  7. Depurar un flux de treball que falla
  8. Errors Comuns i Consells
  9. Exercicis
  10. Conclusió

  1. On viu el pipeline i què el dispara

GitHub Actions busca els fitxers YAML en una ruta fixa: .github/workflows/. El nostre serà .github/workflows/ci.yml. Que el pipeline visqui dins del repositori té tres conseqüències:

  • Es versiona amb el codi. Canviar el pipeline és un commit revisable i reversible.
  • Cada branca pot tenir la seva versió. El flux de treball que s'executa en un PR és el que hi ha en aquella branca, cosa que permet provar canvis del mateix pipeline en un PR.
  • La configuració i el codi evolucionen junts. Si afegeixes una dependència que necessita una altra eina, tots dos canvis entren al mateix commit.

  1. El primer ci.yml de Reservalia, línia a línia

Aquest és el fitxer complet amb què arrenca Reservalia. Està deliberadament reduït a un sol job: preferim una cosa petita que estigui verda avui abans que una de completa que mai no arribi a funcionar.

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

on:                                       # 2
  pull_request:
    branches: [main]                      # 3
  push:
    branches: [main]                      # 4

jobs:                                     # 5
  test:                                   # 6
    name: Proves
    runs-on: ubuntu-22.04                 # 7
    timeout-minutes: 15                   # 8
    steps:                                # 9
      - name: Descarregar el codi
        uses: actions/checkout@v4         # 10

      - name: Preparar Node.js
        uses: actions/setup-node@v4       # 11
        with:
          node-version-file: .nvmrc       # 12
          cache: npm                      # 13

      - name: Installar dependencies
        run: npm ci                       # 14

      - name: Executar proves
        run: npm test                     # 15

Ara, cada número:

  1. name: CI és l'etiqueta que apareixerà a la pestanya Actions i al pull request. És purament cosmètica, però un nom clar estalvia confusió quan tinguis cinc fluxos de treball.
  2. on: declara els esdeveniments que disparen el flux de treball. És la peça que converteix un script en un pipeline: ningú no el llança a mà.
  3. pull_request amb branches: [main]: s'executa quan algú obre un PR cap a main i a cada nou push a la branca d'aquell PR. Aquesta és l'execució que implementa la regla 3 de l'acord: no es fusiona res en vermell.
  4. push amb branches: [main]: s'executa també després del merge. Com vam veure a la 02-01, main pot haver rebut altres commits mentre el PR estava obert, així que verificar-la de nou no és redundant.
  5. jobs: obre la llista d'unitats de treball. Cadascuna correrà a la seva pròpia màquina.
  6. test: és l'identificador del job —el nom que faran servir altres jobs per dependre'n amb needs: i el que configurarem com a check obligatori a la 02-07—. name: Proves és només el que es mostra a la interfície.
  7. runs-on: ubuntu-22.04 tria la màquina. Fixa't que no fem servir ubuntu-latest: aquesta etiqueta canvia de sistema operatiu sense avisar i un bon dia la teva build es trenca sense que ningú hagi tocat res. És el mateix principi de postgres:16.3 davant de postgres:latest de la lliçó 01-04.
  8. timeout-minutes: 15 mata el job si es penja. Sense això, una prova que espera una connexió que no arriba mai pot consumir sis hores de runner. Posa'l sempre, encara que sigui generós.
  9. steps: són els passos seqüencials dins del job. Si un retorna un codi de sortida diferent de 0, els següents no s'executen i el job es marca en vermell.
  10. uses: actions/checkout@v4 descarrega el codi del repositori al runner. Sense aquest pas el disc està buit: el runner no sap res del teu projecte. El @v4 fixa la versió major de l'acció.
  11. uses: actions/setup-node@v4 instal·la Node.js al runner.
  12. node-version-file: .nvmrc és el detall més important del fitxer. En comptes d'escriure node-version: 20.11.0 —duplicant la versió en dos llocs que es desincronitzaran—, li diem que llegeixi el .nvmrc del repositori. Una única font de veritat per al portàtil d'en Diego i per al runner.
  13. cache: npm desa la memòria cau de descàrregues d'npm entre execucions, indexada pel hash del package-lock.json. Com que el lockfile és únic al monorepo (lliçó 01-04), una sola memòria cau cobreix tot el projecte. Ho desenvolupem a la 02-03.
  14. run: npm ci executa una ordre a la shell del runner. npm ci (i no npm install) és la instal·lació reproduïble; el perquè és matèria de la 02-03.
  15. run: npm test executa les proves de tots els workspaces. És exactament la mateixa ordre que en Diego escriu al seu portàtil, i aquesta és la idea: el pipeline no inventa ordres, només les executa en una màquina neta.

Amb aquestes 20 línies, Reservalia ja compleix la pràctica 3 de la lliçó anterior: build automatitzada a cada canvi.

  1. checkout i setup-node: les dues accions que faràs servir sempre

Convé entendre què és una acció. Un run: executa una ordre de shell; un uses: invoca un component reutilitzable publicat en un repositori, amb els seus propis paràmetres sota with:. Són les peces de Lego de l'ecosistema.

      - name: Descarregar el codi
        uses: actions/checkout@v4
        with:
          fetch-depth: 0        # clona TOT l historial, no nomes l ultim commit

      - name: Preparar Node.js
        uses: actions/setup-node@v4
        with:
          node-version-file: .nvmrc
          cache: npm
          cache-dependency-path: package-lock.json

Per defecte, checkout fa un clon superficial d'un sol commit: és més ràpid i suficient gairebé sempre. Necessitaràs fetch-depth: 0 quan alguna cosa del pipeline llegeixi l'historial: git describe per versionar (lliçó 02-06), el càlcul del lead time de la 01-05 o les anàlisis que comparen amb la branca base (lliçó 02-05).

A setup-node, cache-dependency-path indica quin fitxer determina la clau de memòria cau. A Reservalia és el package-lock.json de l'arrel; en un monorepo amb diversos lockfiles caldria enumerar-los.

Consell transferible. El patró descarregar codi → preparar runtime en la versió fixada → instal·lar dependències de manera reproduïble → executar ordre és idèntic en qualsevol llenguatge. Canvia setup-node per setup-python, setup-java o setup-go, i npm ci per pip install -r requirements.txt, mvn -B verify o go build ./....

  1. Runners allotjats davant d'autoallotjats

El runner és la màquina que executa un job. Hi ha dues maneres d'aconseguir-la.

Allotjat (GitHub) Autoallotjat (teu)
Qui el manté El proveïdor Tu
Estat inicial Màquina neta cada vegada El que tu garanteixis
Posada en marxa Zero configuració Instal·lar, registrar i mantenir l'agent
Cost Per minut consumit Cost de la màquina, corri o no
Accés a xarxa privada No, tret de túnel Sí, és dins de la teva xarxa
Maquinari especial Limitat al catàleg El que vulguis (GPU, macOS, ARM)
Risc de seguretat Aïllat i efímer Persistent: el que deixa un job el pot veure el següent

Un runner autoallotjat compensa en quatre situacions: necessites accés a recursos que no estan exposats a internet; la teva build requereix maquinari que el catàleg no ofereix o que resulta desproporcionadament car per minut; consumeixes tants minuts que una màquina pròpia surt més barata; o una norma exigeix que el codi no surti de la teva infraestructura.

L'avís de seguretat que no es pot ometre: un runner autoallotjat mai no ha d'executar fluxos de treball de pull requests que vinguin de forks. Un desconegut obre un PR, modifica el flux de treball i el seu codi s'executa en una màquina de la teva xarxa interna. Com que els runners autoallotjats són persistents, un job maliciós pot deixar fitxers o credencials que veurà el següent. La mitigació mínima és executar cada job en un contenidor efímer i aïllar el runner a la seva pròpia subxarxa. L'enduriment seriós del pipeline —permisos del token, OIDC, aïllament de secrets— és matèria de la lliçó 04-03.

La decisió de Reservalia: runners allotjats. Són 340 negocis de pagament i tres persones; els minuts són barats comparats amb el temps de la Nuria mantenint màquines. És la decisió correcta per a la immensa majoria dels equips petits.

  1. Variables d'entorn i secrets

El pipeline necessita valors de configuració. Uns són públics (la regió d'AWS) i altres no (una contrasenya). GitHub Actions els distingeix.

env:                                    # variables per a TOT el flux de treball
  NODE_ENV: test
  TZ: Europe/Madrid
jobs:
  test:
    runs-on: ubuntu-22.04
    env:                                # variables nomes per a aquest job
      DATABASE_URL: postgres://reservalia:ci@localhost:5432/reservalia_test
    steps:
      - run: npm run test:integracio
        env: { LOG_LEVEL: debug }       # variables nomes per a aquest pas

Els tres àmbits es combinen: el més específic guanya. TZ: Europe/Madrid mereix atenció especial: els runners corren en UTC, i una aplicació de reserva de cites és plena de lògica horària. Fixar la zona horària explícitament evita el clàssic "la prova falla només en CI i només després de les 22:00".

Els secrets es declaren a la configuració del repositori (o de l'organització) i es llegeixen amb el context secrets, per exemple SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} dins de l'env: d'un step. Regles bàsiques d'ús, sense entrar encara en l'enduriment seriós:

  • Mai no s'escriuen al YAML (el fitxer és al repositori; el secret, no) i s'injecten com a variables d'entorn o paràmetres with:, mai concatenats en una cadena que després s'imprimeix.
  • La plataforma emmascara els valors coneguts als registres, substituint-los per ***. No hi confiïs com a única protecció: si el teu script fa echo d'un JSON que conté el secret transformat (per exemple, en base64), l'emmascarament no ho detecta.
  • Els secrets no arriben als fluxos de treball disparats per PR des de forks. És una mesura de seguretat deliberada, i explica per què un PR extern pot fallar en passos que necessiten credencials.

A Reservalia, per ara, només hi ha dos secrets: SONAR_TOKEN (lliçó 02-05) i AWS_ROLE_CI (lliçó 02-06). La contrasenya de PostgreSQL en CI no és un secret: és una base de dades efímera que viu nou minuts dins del runner i mor. Tractar com a secret una cosa que no ho és genera soroll i fa més difícil protegir el que sí que importa.

  1. Serveis de suport: un PostgreSQL real per a les proves

Les proves d'integració de Reservalia consulten una base de dades de debò. En local això ho resol el docker-compose.yml de la lliçó 01-04; en CI es resol amb services:, que aixeca contenidors auxiliars al costat del job.

jobs:
  test:
    runs-on: ubuntu-22.04
    services:
      postgres:                                    # 1
        image: postgres:16.3                       # 2
        env:                                       # 3
          POSTGRES_USER: reservalia
          POSTGRES_PASSWORD: ci
          POSTGRES_DB: reservalia_test
        ports:
          - 5432:5432                              # 4
        options: >-                                # 5
          --health-cmd "pg_isready -U reservalia -d reservalia_test"
          --health-interval 5s
          --health-timeout 3s
          --health-retries 10
    steps:
      # ... checkout, setup-node i npm ci, igual que a l apartat 2 ...
      - name: Proves d'integracio
        run: npm run test:integracio --workspace apps/api
        env:
          DATABASE_URL: postgres://reservalia:ci@localhost:5432/reservalia_test   # 6
  1. postgres: és una etiqueta que tries tu; també serà el nom de xarxa del contenidor.
  2. image: postgres:16.3 és exactament la mateixa versió que fa servir el docker-compose.yml local i la mateixa família que RDS en producció. Que els tres coincideixin és la meitat de la reproduïbilitat.
  3. env: configura el contenidor. Fem servir la base reservalia_test, no reservalia: anomenar diferent la base de proves evita accidents el dia que algú copiï una cadena de connexió.
  4. ports: - 5432:5432 publica el port del contenidor al runner, perquè el procés de Node s'hi pugui connectar a localhost:5432.
  5. options: són opcions de Docker. El healthcheck és imprescindible: sense ell, els steps arrenquen tan bon punt el contenidor existeix, no quan la base de dades està llesta per acceptar connexions. El resultat seria un ECONNREFUSED intermitent —el pitjor tipus de fallada, perquè unes vegades passa i altres no—. Amb --health-cmd, la plataforma espera que pg_isready respongui abans d'executar el primer step.
  6. DATABASE_URL apunta a localhost, no a postgres. Els steps s'executen directament al runner, no dins d'un contenidor, així que veuen el port publicat. (Si el job fes servir container:, l'adreça correcta seria postgres:5432, el nom del servei. És una de les confusions més freqüents de l'ecosistema.)
flowchart LR
    subgraph RUNNER["Runner ubuntu-22.04 efimer"]
        S["steps: node + npm test"] -- "localhost:5432" --> P["servei postgres:16.3"]
    end

  1. Depurar un flux de treball que falla

Tard o d'hora el flux de treball es posarà vermell per alguna cosa que no entens. Aquest és l'ordre d'atac, del mètode més barat al més car.

Pas 1: llegir el registre del step correcte. Sona obvi i gairebé ningú no ho fa bé. Desplega el primer step en vermell (els següents no s'executen) i busca la primera línia d'error, no l'última: l'última sol ser el resum inútil Process completed with exit code 1. Pas 2: comprovar si és un problema d'entorn o de codi. La pregunta que separa els dos mons: aquest mateix commit passa al meu portàtil?

git checkout a3f9c21          # el commit exacte que va fallar en CI
rm -rf node_modules           # imitar la maquina neta
npm ci                        # la mateixa installacio que fa el runner
npm test

Si en local passa i en CI no, la diferència és a l'entorn: versió de Node, zona horària, variables absents, fitxers no versionats que només existeixen al teu disc, ordre de les proves o dependència d'una base de dades amb dades prèvies.

Pas 3: activar els registres de diagnòstic. Defineix al repositori dues variables de tipus secret: ACTIONS_STEP_DEBUG: true (detall intern de cada step: entrades de les accions, ordres executades) i ACTIONS_RUNNER_DEBUG: true (preparació de la màquina, xarxa, memòria cau). En rellançar, els registres inclouran línies ##[debug]. És moltíssim text: fes-lo servir quan el registre normal no n'hi hagi prou i desactiva'l després.

Pas 4: imprimir l'estat del runner. Un step temporal de diagnòstic resol un percentatge sorprenent de casos:

      - name: Diagnostic
        run: |
          node --version            # coincideix amb .nvmrc?
          echo "TZ=$TZ  data=$(date)"
          pwd && ls -la             # es el codi on et penses?
          env | sort | grep -v -i 'token\|secret\|password'   # variables, sense filtrar secrets

Fixa't en el grep -v: no bolquis mai l'entorn complet en un registre públic.

Pas 5: reproduir el pipeline en local.

act pull_request -W .github/workflows/ci.yml     # opcio A: simular el flux de treball

docker run --rm -it -v "$PWD":/repo -w /repo \
  node:20.11.0-bookworm-slim bash                # opcio B: el mateix contenidor base
#   a dins: npm ci && npm test

act és còmode per iterar sobre l'estructura del YAML, però la seva imatge no és idèntica a la del runner real i algunes accions no funcionen igual. L'opció B és més laboriosa i reprodueix millor la realitat.

Pas 6: la fallada intermitent. Si el mateix commit passa unes vegades i falla altres, no ets davant d'un problema de configuració sinó d'una prova inestable. No ho resolguis rellançant: anota-ho i aplica la política de quarantena de la lliçó 02-04.

Errors Comuns i Consells

Error 1: fer servir ubuntu-latest i versions flotants. El dia que l'etiqueta apunti a una altra versió del sistema, la teva build es trenca sense que ningú hagi tocat el repositori, i perdràs mig dia buscant al teu diff una causa que no hi és. Fixa ubuntu-22.04, fixa postgres:16.3, llegeix la versió de Node del .nvmrc. Error 2: oblidar el checkout, el clàssic de principiant: el runner arrenca amb el disc buit i npm ci falla amb un desconcertant "no existeix package.json".

Error 3: services: sense healthcheck. El símptoma és una prova d'integració que falla amb ECONNREFUSED una de cada cinc execucions. La causa no és la xarxa: és que els steps van arrencar abans que PostgreSQL estigués llest.

Error 4: duplicar la versió de Node. Posar node-version: 20 al YAML mentre el .nvmrc diu 20.11.0 funciona fins que deixa de funcionar. Una versió, un lloc.

Error 5: no posar timeout-minutes. Un job penjat consumeix minuts facturables fins al límit per defecte de la plataforma, que es mesura en hores.

Consell 1: comença amb un job i fes-lo créixer. El ci.yml d'aquest capítol té un sol job i ja aporta valor real. A les lliçons següents hi afegirem build, qualitat i publicar.

Consell 2: prova els canvis del pipeline en un PR —el flux de treball es llegeix de la branca del PR, així que pots iterar sense embrutar maini anomena sempre els steps: - name: Installar dependencies davant d'un run nu és la diferència entre un registre llegible i un mur d'ordres.

Exercicis

Exercici 1

Aquest flux de treball falla sempre al pas de proves amb ECONNREFUSED 127.0.0.1:5432. Troba els tres problemes i corregeix-los.

name: CI
on: [push]
jobs:
  test:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:latest
        env: { POSTGRES_PASSWORD: ci }
    steps:
      - uses: actions/setup-node@v4
        with: { node-version: 20 }
      - run: npm install
      - run: npm run test:integracio --workspace apps/api
        env:
          DATABASE_URL: postgres://postgres:ci@localhost:5432/postgres

Exercici 2

En Diego diu: "Al meu portàtil npm test passa sempre; en CI falla la prova calcula forats del dia seguent una de cada tres vegades, i sobretot a la tarda". Enumera tres hipòtesis ordenades per probabilitat i digues què comprovaries en cada cas.

Exercici 3

Reservalia vol que el pipeline no s'executi quan el PR només canvia fitxers d'infra/terraform/ o el README.md. Escriu el bloc on: corresponent i explica un risc d'aquesta optimització.

Solucions

Solució 1. Els tres problemes:

  1. Falta actions/checkout@v4 com a primer step: el runner no té el codi, així que npm install no troba package.json. És la causa arrel que res no funcioni.
  2. Falta el healthcheck al servei i falta publicar el port. Sense ports: - 5432:5432 el port no és accessible des del runner, i sense --health-cmd els steps arrenquen abans que la base estigui llesta. Tots dos produeixen ECONNREFUSED.
  3. Versions flotants: ubuntu-latest, postgres:latest i node-version: 20. Han de ser ubuntu-22.04, postgres:16.3 i node-version-file: .nvmrc.

De regal, dues millores: npm install ha de ser npm ci, i falta timeout-minutes.

Solució 2. Hipòtesis ordenades:

  1. Zona horària. És la més probable, i el "a la tarda" és la pista definitiva: el runner corre en UTC i el portàtil d'en Diego a Europe/Madrid. Una prova sobre "el dia següent" executada a les 23:30 de Barcelona cau en un dia diferent en UTC. Comprovació: echo $TZ && date al runner; solució: TZ: Europe/Madrid a nivell de flux de treball, o millor, fer la prova independent del rellotge injectant-hi la data.
  2. Estat compartit entre proves. Si l'ordre d'execució varia o hi ha paral·lelisme, una prova pot deixar cites a la base que una altra troba. Comprovació: executar només aquella prova de manera aïllada i amb la base acabada de crear.
  3. Dades que depenen del calendari. Festius o caps de setmana: la prova passa de dilluns a dijous i falla el divendres perquè "el dia següent" és dissabte i el negoci tanca. Comprovació: fixar una data concreta a la prova.

Solució 3.

on:
  pull_request:
    branches: [main]
    paths-ignore: ['infra/terraform/**', '**/*.md']
  push:
    branches: [main]
    paths-ignore: ['infra/terraform/**', '**/*.md']

El risc: si el check test està configurat com a obligatori per fusionar (lliçó 02-07), un PR que només toca README.md no executarà mai el check i quedarà bloquejat per sempre esperant un resultat que no arribarà. La solució habitual és un flux de treball bessó que retorni verd immediatament per a aquestes rutes, o fer servir filtres a nivell de job en comptes de a nivell d'esdeveniment. Hi tornarem a la 02-07.

Conclusió

Reservalia ja té Integració Contínua de debò:

  • El pipeline viu a .github/workflows/ci.yml, versionat amb el codi, i es dispara sol a cada pull request cap a main i a cada push a main.
  • El job test corre en un runner ubuntu-22.04 fixat, amb timeout-minutes, i executa quatre passos: checkout, setup-node llegint la versió del .nvmrc amb memòria cau d'npm, npm ci i npm test.
  • Els runners allotjats són l'elecció correcta per a un equip petit; els autoallotjats només compensen per accés a xarxa privada, maquinari especial, volum o compliment normatiu, i porten amb ells un problema d'aïllament que cal prendre's seriosament.
  • Les variables es declaren en tres àmbits (flux de treball, job, step) i els secrets es llegeixen amb secrets.NOM sense escriure'ls mai al YAML. TZ: Europe/Madrid evita una família sencera de fallades horàries.
  • Un contenidor de servei postgres:16.3 amb healthcheck dona a les proves d'integració una base de dades real, idèntica a la de local i de la mateixa família que producció.
  • I tens un mètode de depuració de sis passos, del registre al contenidor reproduït en local, amb ACTIONS_STEP_DEBUG com a artilleria intermèdia.

El que encara no fa aquest pipeline és construir res: executa les proves sobre el codi font i s'acaba aquí. A la lliçó següent, Automatització de la Construcció, veurem què significa realment "construir", en quin ordre cal compilar els paquets d'un monorepo, per què npm ci i el lockfile són els pilars d'una build reproduïble, com empaquetar apps/api en un Dockerfile multietapa amb usuari no root, i com funcionen les memòries cau —inclosa la raó per la qual una memòria cau mal invalidada és pitjor que no tenir-ne cap—. Al final, el ci.yml tindrà el seu segon job: build.

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