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
- On viu el pipeline i què el dispara
- El primer
ci.ymlde Reservalia, línia a línia checkoutisetup-node: les dues accions que faràs servir sempre- Runners allotjats davant d'autoallotjats
- Variables d'entorn i secrets
- Serveis de suport: un PostgreSQL real per a les proves
- Depurar un flux de treball que falla
- Errors Comuns i Consells
- Exercicis
- Conclusió
- 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.
- El primer
ci.yml de Reservalia, línia a línia
ci.yml de Reservalia, línia a líniaAquest é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 # 15Ara, cada número:
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.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à.pull_requestambbranches: [main]: s'executa quan algú obre un PR cap amaini 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.pushambbranches: [main]: s'executa també després del merge. Com vam veure a la 02-01,mainpot haver rebut altres commits mentre el PR estava obert, així que verificar-la de nou no és redundant.jobs:obre la llista d'unitats de treball. Cadascuna correrà a la seva pròpia màquina.test:és l'identificador del job —el nom que faran servir altres jobs per dependre'n ambneeds: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.runs-on: ubuntu-22.04tria la màquina. Fixa't que no fem servirubuntu-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 depostgres:16.3davant depostgres:latestde la lliçó 01-04.timeout-minutes: 15mata 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.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.uses: actions/checkout@v4descarrega el codi del repositori al runner. Sense aquest pas el disc està buit: el runner no sap res del teu projecte. El@v4fixa la versió major de l'acció.uses: actions/setup-node@v4instal·la Node.js al runner.node-version-file: .nvmrcés el detall més important del fitxer. En comptes d'escriurenode-version: 20.11.0—duplicant la versió en dos llocs que es desincronitzaran—, li diem que llegeixi el.nvmrcdel repositori. Una única font de veritat per al portàtil d'en Diego i per al runner.cache: npmdesa la memòria cau de descàrregues d'npm entre execucions, indexada pel hash delpackage-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.run: npm ciexecuta una ordre a la shell del runner.npm ci(i nonpm install) és la instal·lació reproduïble; el perquè és matèria de la 02-03.run: npm testexecuta 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.
checkout i setup-node: les dues accions que faràs servir sempre
checkout i setup-node: les dues accions que faràs servir sempreConvé 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.jsonPer 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-nodepersetup-python,setup-javaosetup-go, inpm ciperpip install -r requirements.txt,mvn -B verifyogo build ./....
- 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.
- 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 pasEls 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 faechod'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.
- 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 # 6postgres:és una etiqueta que tries tu; també serà el nom de xarxa del contenidor.image: postgres:16.3és exactament la mateixa versió que fa servir eldocker-compose.ymllocal i la mateixa família que RDS en producció. Que els tres coincideixin és la meitat de la reproduïbilitat.env:configura el contenidor. Fem servir la basereservalia_test, noreservalia: anomenar diferent la base de proves evita accidents el dia que algú copiï una cadena de connexió.ports: - 5432:5432publica el port del contenidor al runner, perquè el procés de Node s'hi pugui connectar alocalhost:5432.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 unECONNREFUSEDintermitent —el pitjor tipus de fallada, perquè unes vegades passa i altres no—. Amb--health-cmd, la plataforma espera quepg_isreadyrespongui abans d'executar el primer step.DATABASE_URLapunta alocalhost, no apostgres. Els steps s'executen directament al runner, no dins d'un contenidor, així que veuen el port publicat. (Si el job fes servircontainer:, l'adreça correcta seriapostgres: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
- 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 testSi 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 secretsFixa'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 testact é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 main— i 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/postgresExercici 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:
- Falta
actions/checkout@v4com a primer step: el runner no té el codi, així quenpm installno trobapackage.json. És la causa arrel que res no funcioni. - Falta el healthcheck al servei i falta publicar el port. Sense
ports: - 5432:5432el port no és accessible des del runner, i sense--health-cmdels steps arrenquen abans que la base estigui llesta. Tots dos produeixenECONNREFUSED. - Versions flotants:
ubuntu-latest,postgres:latestinode-version: 20. Han de serubuntu-22.04,postgres:16.3inode-version-file: .nvmrc.
De regal, dues millores: npm install ha de ser npm ci, i falta timeout-minutes.
Solució 2. Hipòtesis ordenades:
- 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 && dateal runner; solució:TZ: Europe/Madrida nivell de flux de treball, o millor, fer la prova independent del rellotge injectant-hi la data. - 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.
- 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 amaini a cada push amain. - El job
testcorre en un runnerubuntu-22.04fixat, ambtimeout-minutes, i executa quatre passos:checkout,setup-nodellegint la versió del.nvmrcamb memòria cau d'npm,npm ciinpm 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.NOMsense escriure'ls mai al YAML.TZ: Europe/Madridevita una família sencera de fallades horàries. - Un contenidor de servei
postgres:16.3amb 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_DEBUGcom 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
- Conceptes Bàsics de CI/CD
- Beneficis del CI/CD
- Eines Populars de CI/CD
- El Projecte del Curs: l'Aplicació que Automatitzarem
- Mètriques DORA: Com es Mesura el Lliurament de Programari
Mòdul 2: Integració Contínua (CI)
- Introducció a la Integració Contínua
- Configuració d'un Entorn de CI
- Automatització de la Construcció
- Proves Automatitzades
- Qualitat de Codi i Anàlisi Estàtica
- Artefactes, Versionat i Promoció
- Integració amb el Control de Versions
Mòdul 3: Desplegament Continu (CD)
- Introducció al Desplegament Continu
- Automatització del Desplegament
- Infraestructura com a Codi i Entorns Reproduïbles
- Estratègies de Desplegament
- Feature Flags, Rollback i Recuperació davant Errors
- Monitoratge i Retroalimentació
Mòdul 4: Pràctiques Avançades de CI/CD
- Pipelines de CI/CD
- Gestió de Dependències
- Seguretat en CI/CD
- Escalabilitat i Rendiment
- Pipeline as Code: Plantilles, Reutilització i Proves del Pipeline
- Bases de Dades al Pipeline: Migracions Segures
Mòdul 5: Implementació de CI/CD en Projectes Reals
- Cas d'Estudi: Projecte Web
- Cas d'Estudi: Aplicació Mòbil
- Cas d'Estudi: Microserveis
- Cas d'Estudi: Modernitzar un Projecte Legacy
Mòdul 6: Eines i Tecnologies
- Jenkins
- GitLab CI/CD
- CircleCI
- Travis CI
- Docker i Kubernetes
- GitHub Actions a Fons
- Comparativa i Criteris per Triar Eina
Mòdul 7: Exercicis Pràctics
- Exercici 1: Configuració d'un Pipeline Bàsic
- Exercici 2: Integració de Proves Automatitzades
- Exercici 3: Desplegament en un Entorn de Producció
- Exercici 4: Monitoratge i Retroalimentació
- Exercici 5: Enfortir el Pipeline amb Seguretat i Secrets
- Projecte Final: Pipeline Complet d'Extrem a Extrem
