El pipeline de Reservalia ja executa les proves a cada pull request, però encara no construeix res: treballa sobre el codi font i s'acaba aquí. Falta la peça que converteix un repositori en una cosa executable. En aquesta lliçó veurem què significa exactament "construir", per què ha de ser una única ordre reproduïble i no una seqüència de passos que algú recorda, i com s'orquestra la construcció d'un monorepo on un paquet depèn d'un altre. Després atacarem el concepte central de tot el mòdul —la reproduïbilitat: per què npm ci no és el mateix que npm install, quin paper hi juga el lockfile i quines coses fan que una build deixi de ser reproduïble—. Acabarem empaquetant apps/api en un Dockerfile multietapa explicat etapa a etapa, i afegint el job build al ci.yml. El que no farem aquí és publicar ni versionar aquest artefacte: això és la lliçó 02-06.
Contingut
- Què és realment "construir"
- La build de Reservalia, paquet a paquet
- Reproduïbilitat: el lockfile i
npm ci - Què fa que una build no sigui reproduïble
- Empaquetar amb Docker: el
Dockerfilemultietapa .dockerignore: el que no hi ha d'entrar- Memòries cau de construcció i el perill d'invalidar-les malament
- El job
buildalci.ymlde Reservalia - Errors Comuns i Consells
- Exercicis
- Conclusió
- Què és realment "construir"
Construir és transformar el codi font en l'artefacte que s'executarà, sense intervenció humana. Segons la tecnologia, aquesta transformació inclou compilar, transpilar, empaquetar, minimitzar, generar codi o crear una imatge de contenidor.
El que defineix una bona build no és el que fa, sinó quatre propietats:
| Propietat | Què significa | Com es comprova |
|---|---|---|
| Una sola ordre | npm run build i res més |
Algú ha d'executar alguna cosa "abans"? |
| Reproduïble | El mateix codi produeix el mateix resultat | Construir dues vegades i comparar |
| Sense estat | No depèn de restes de builds anteriors | rm -rf node_modules dist && npm ci && npm run build |
| Autoverificable | Falla sorollosament, amb codi de sortida ≠ 0 | Trencar alguna cosa a propòsit i veure si es posa en vermell |
La primera és la que més s'incompleix. Reservalia parteix d'un cas real: en Diego construeix l'API executant npm install && npm run build, però abans copia a mà un fitxer .env que només és al seu portàtil, i si ha tocat packages/tipus-compartits ha de construir aquest paquet primer i recordar-se de fer-ho. Això no és una ordre: és un procediment oral. I un procediment oral no es pot automatitzar fins que algú l'escriu.
La regla pràctica. Si necessites explicar a algú com construir el projecte amb més d'una frase, el pas següent no és escriure el pipeline: és arreglar l'ordre de build.
- La build de Reservalia, paquet a paquet
El monorepo té tres paquets i una dependència real entre ells: tant l'API com la web importen tipus de @reservalia/tipus-compartits.
flowchart LR
T["packages/tipus-compartits<br/>tsc → dist/"] --> A["apps/api<br/>tsc → dist/"]
T --> W["apps/web<br/>vite build → dist/"]
Això significa que l'ordre importa: si apps/api compila abans que tipus-compartits, TypeScript no trobarà les declaracions i fallarà. Les tres ordres, una per paquet:
npm run build --workspace packages/tipus-compartits # genera JS + fitxers .d.ts
npm run build --workspace apps/api # tsc → apps/api/dist/
npm run build --workspace apps/web # vite build → apps/web/dist/Cadascuna fa una cosa diferent:
tsc(paquet compartit i API) transpila TypeScript a JavaScript executable per Node i genera els.d.ts. No agrupa ni minimitza: el resultat conserva l'estructura de carpetes desrc/.vite build(web) fa molt més: resol tots elsimport, agrupa el codi en uns pocs fitxers, elimina el que ningú no fa servir, minimitza, processa el CSS i afegeix un hash al nom de cada fitxer (index-4a7f2b.js) perquè el navegador els pugui posar en memòria cau indefinidament.
A l'arrel del monorepo, la lliçó 01-04 ja va deixar definida l'ordre única: "build": "npm run build --workspaces --if-present". L'opció --workspaces recorre tots els paquets i npm resol l'ordre topològic: com que apps/api declara "@reservalia/tipus-compartits": "*" a les seves dependències, npm construeix primer el paquet del qual es depèn. Aquest és un motiu excel·lent per declarar bé les dependències internes: el gestor de paquets pot deduir l'ordre i tu no l'has d'escriure.
En eines d'altres ecosistemes la idea és idèntica: Maven, Gradle, Nx o Turborepo resolen el mateix graf. El que no has de fer mai és escriure l'ordre a mà al YAML del pipeline, perquè el dia que algú afegeixi un paquet, el pipeline no se n'assabentarà.
- Reproduïbilitat: el lockfile i
npm ci
npm ciAquest apartat és el cor de la lliçó. Reproduïble significa: el mateix commit, construït avui al teu portàtil i d'aquí a sis mesos en un runner net, produeix el mateix artefacte.
3.1. El paper del lockfile
A package.json declares intencions ("express": "4.19.2" o, pitjor, "^4.19.2"). A package-lock.json queda registrat el resultat exacte de resoldre aquestes intencions: la versió concreta de cadascuna dels centenars de dependències transitives, la seva URL i el seu hash d'integritat.
"node_modules/express": {
"version": "4.19.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
"integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZ..."
}El camp integrity és el que fa que el lockfile sigui també un control de seguretat: si el paquet descarregat no coincideix amb aquell hash, la instal·lació falla. Per això el lockfile es versiona al repositori, sempre, i a Reservalia n'hi ha un de sol per a tot el monorepo.
3.2. npm ci davant de npm install
npm install |
npm ci |
|
|---|---|---|
| Què fa servir | package.json, i el lockfile com a suggeriment |
Només el lockfile |
| Si hi ha discrepància | Modifica el lockfile silenciosament | Falla amb un error explícit |
node_modules previ |
L'actualitza de manera incremental | L'esborra i l'instal·la de zero |
| Determinisme | No garantit | Garantit |
| Velocitat en CI | Menor | Major (no resol l'arbre) |
| Ús correcte | Al teu portàtil, en afegir una dependència | Al pipeline, sempre |
El cas que il·lustra la diferència: algú afegeix "lodash": "^4.17.20" al package.json i no actualitza el lockfile. Amb npm install, el pipeline instal·la el que hi hagi i reescriu el lockfile dins del runner, així que la build passa; i com que el runner és efímer, aquell lockfile modificat desapareix sense que ningú se n'assabenti. Cada execució podria instal·lar una versió diferent. Amb npm ci, el pipeline s'atura amb un missatge clar —npm ci can only install packages when your package.json and package-lock.json are in sync—, i aquest error no és una molèstia: és el pipeline fent exactament la seva feina.
- Què fa que una build no sigui reproduïble
Els enemics habituals, i com es neutralitzen:
| Enemic | Símptoma típic | Antídot |
|---|---|---|
Rangs de versió (^, ~) sense lockfile aplicat |
"Ahir funcionava i avui no, sense tocar res" | Versions exactes + npm ci |
| Runtime no fixat | Funciona amb Node 20 i falla amb Node 22 | .nvmrc + engines + node-version-file |
Imatge base flotant (node:20, postgres:latest) |
La build canvia quan el proveïdor publica | Fixar la versió completa |
Fitxers no versionats (.env local, certificats) |
"A la meva màquina funciona" | Tot al repositori o injectat per variables |
| Descàrregues des d'internet durant la build | Falla quan l'URL canvia o cau | Dependències declarades, no descarregades a mà |
| Restes de builds anteriors | Passa al segon intent però no al primer | Netejar dist/ abans de construir |
| Dependència de la data o la màquina | L'artefacte binari canvia entre execucions | Segells de temps fixos si necessites bit a bit |
La prova definitiva de reproduïbilitat cap en quatre línies i val la pena executar-la de tant en tant:
git clone https://github.com/reservalia/reservalia.git /tmp/neta && cd /tmp/neta
git checkout a3f9c21 # el commit exacte
npm ci # installacio des del lockfile
npm run build # construeix sense cap pas manual?Si això falla, el pipeline fallarà. Si funciona, el pipeline serà avorrit, que és justament el que es busca.
- Empaquetar amb Docker: el
Dockerfile multietapa
Dockerfile multietapaapps/api/dist/ és JavaScript solt: per executar-lo cal un Node de la versió correcta i les dependències de producció. Una imatge de contenidor empaqueta les tres coses juntes i elimina d'una revolada la diferència entre entorns.
El problema és que per construir cal TypeScript, i per executar no. Ficar-ho tot a la imatge final dona una imatge enorme i amb més superfície d'atac de la necessària. La solució és una construcció multietapa.
# apps/api/Dockerfile
# ───────── ETAPA 1: dependencies completes ─────────
FROM node:20.11.0-bookworm-slim AS deps
WORKDIR /app
COPY package.json package-lock.json ./
COPY apps/api/package.json apps/api/
COPY packages/tipus-compartits/package.json packages/tipus-compartits/
RUN npm ci
# ───────── ETAPA 2: construccio ─────────
FROM node:20.11.0-bookworm-slim AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build --workspace packages/tipus-compartits \
&& npm run build --workspace apps/api
# ───────── ETAPA 3: dependencies de produccio ─────────
FROM node:20.11.0-bookworm-slim AS prod-deps
WORKDIR /app
COPY package.json package-lock.json ./
COPY apps/api/package.json apps/api/
COPY packages/tipus-compartits/package.json packages/tipus-compartits/
RUN npm ci --omit=dev
# ───────── ETAPA 4: imatge final ─────────
FROM node:20.11.0-bookworm-slim AS runtime
ENV NODE_ENV=production TZ=Europe/Madrid
WORKDIR /app
COPY --from=prod-deps --chown=node:node /app/node_modules ./node_modules
COPY --from=build --chown=node:node /app/apps/api/dist ./dist
COPY --from=build --chown=node:node /app/packages/tipus-compartits/dist ./node_modules/@reservalia/tipus-compartits/dist
USER node
EXPOSE 3000
CMD ["node", "dist/index.js"]Etapa a etapa:
Etapa 1 (deps). Copia només els package.json i el lockfile, no el codi, i executa npm ci. Aquest ordre és deliberat i és el truc de memòria cau més rendible de Docker: mentre no canviïn les dependències, aquesta capa es reutilitza i t'estalvies un npm ci complet a cada build. Si copiessis el codi abans, qualsevol canvi en un .ts invalidaria la instal·lació.
Etapa 2 (build). Reutilitza els node_modules de l'etapa anterior amb COPY --from=deps, copia ara sí tot el codi i construeix en l'ordre correcte: primer el paquet compartit, després l'API.
Etapa 3 (prod-deps). Torna a instal·lar, però amb --omit=dev: sense TypeScript, sense Vitest, sense ESLint. Redueix molt la mida final i, sobretot, treu del contenidor de producció desenes de paquets que mai no haurien de ser-hi. Etapa 4 (runtime). L'única que acaba al registre. Només conté Node, les dependències de producció i el dist/ compilat. Quatre decisions importants:
node:20.11.0-bookworm-slim: versió completament fixada —igual que el.nvmrc— i variantslim, molt més lleugera que la imatge per defecte. Mainode:20ninode:latest.USER node: el procés no corre com a root. Les imatges oficials de Node ja porten aquell usuari creat. És una línia que redueix de manera real l'impacte d'una vulnerabilitat: si algú aconsegueix executar codi dins del contenidor, ho fa sense privilegis.--chown=node:nodea cadaCOPY: els fitxers pertanyen a l'usuari que els farà servir, no a root.ENV NODE_ENV=production: moltes llibreries, Express entre elles, canvien el seu comportament amb aquesta variable (menys registres, més memòria cau, missatges d'error sense traça interna).
.dockerignore: el que no hi ha d'entrar
.dockerignore: el que no hi ha d'entrarEl COPY . . de l'etapa 2 copia tot el context de construcció. Sense filtre, això inclou node_modules locals (d'una altra arquitectura), l'historial de git i, en el pitjor cas, un .env amb credencials. El fitxer .dockerignore va a l'arrel del repositori:
node_modules .git .env
**/node_modules .github .env.*
**/dist *.log infra/
**/coverage docker-compose.ymlTres motius, per ordre d'importància. Seguretat: un .env copiat a una capa queda dins de la imatge per sempre encara que una instrucció posterior l'esborri, perquè les capes són immutables i qualsevol amb la imatge les pot llegir. Correcció: copiar node_modules des d'un macOS a una imatge Linux hi fica binaris natius incompatibles. Velocitat: un context més petit es transfereix abans i evita que un fitxer irrellevant invalidi capes.
- Memòries cau de construcció i el perill d'invalidar-les malament
Hi ha dues memòries cau diferents en joc. La memòria cau de dependències és la que vam activar a la 02-02 amb cache: npm: desa les descàrregues d'npm entre execucions, amb una clau derivada del hash del package-lock.json; si el lockfile no canvia, la clau coincideix i no cal descarregar res de la xarxa. La memòria cau de capes de Docker funciona per instrucció: cada línia del Dockerfile produeix una capa identificada pel seu contingut, i si la instrucció i les seves entrades no han canviat, Docker la reutilitza. D'aquí la regla d'or d'ordenar el Dockerfile del que canvia menys al que canvia més, que és exactament el que fa la nostra etapa 1.
Ara la part important: una memòria cau mal invalidada és pitjor que no tenir-ne.
Imagina't una clau de memòria cau fixa, del tipus cache-key: dependencies-api, que no inclou el hash del lockfile. Algú actualitza express de 4.19.2 a 4.19.3 i el lockfile canvia; però com que la clau és la mateixa, el pipeline restaura els node_modules vells. Resultat: el pipeline verifica una versió del codi que no existeix. Les proves passen, l'artefacte es publica i en producció corre una altra cosa. Aquesta fallada pot trigar setmanes a aparèixer i és endimoniadament difícil de diagnosticar, perquè el pipeline diu que tot va bé.
Quatre regles per no caure-hi: la clau s'ha de derivar del contingut del que posa en memòria cau (hash del package-lock.json, mai un nom fix); no posis mai en memòria cau el resultat de la construcció, només les entrades, perquè un dist/ en memòria cau és un artefacte potencialment obsolet; davant del dubte, invalida —reconstruir costa minuts, publicar l'artefacte equivocat costa un incident—; i una memòria cau absent no ha de trencar la build: si falta, es reconstrueix i prou.
L'optimització avançada de temps —memòries cau remotes compartides, construcció només d'allò afectat— és matèria de la lliçó 04-04.
- El job
build al ci.yml de Reservalia
build al ci.yml de ReservaliaAfegim el segon job al flux de treball de la lliçó anterior:
build:
name: Construccio
runs-on: ubuntu-22.04
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: npm
- run: npm ci
- name: Construir tots els paquets
run: npm run build # 1
- name: Comprovar que l'artefacte existeix
run: | # 2
test -f apps/api/dist/index.js
test -d apps/web/dist
echo "Mida de la web: $(du -sh apps/web/dist | cut -f1)"
- name: Construir la imatge de l'API
uses: docker/build-push-action@v5 # 3
with:
context: .
file: apps/api/Dockerfile
push: false # 4
tags: reservalia/api:${{ github.sha }}
cache-from: type=gha # 5
cache-to: type=gha,mode=maxnpm run builda l'arrel: una sola ordre construeix els tres paquets en ordre. El pipeline no sap res del graf de dependències, i així ha de ser.- La comprovació explícita sembla redundant, però atrapa una fallada real: una configuració mal posada pot fer que
tscacabi amb èxit sense escriure res. Verificar que l'artefacte existeix converteix un fals verd en un vermell honest. docker/build-push-actionconstrueix la imatge fent servir BuildKit, amb suport de memòria cau entre execucions.context: .és l'arrel del monorepo, perquè elDockerfilenecessita el lockfile de l'arrel.push: false: en un pull request construïm per comprovar que elDockerfilecontinua funcionant, però no publiquem res. La publicació és la lliçó 02-06.cache-from/cache-tode tipusghadesen les capes de Docker a la memòria cau d'Actions.mode=maxdesa també les capes intermèdies, cosa que accelera molt les builds multietapa a canvi de més espai.
Errors Comuns i Consells
Error 1: npm install al pipeline. És l'error més estès i el més silenciós: cada execució pot instal·lar una cosa diferent i el lockfile modificat desapareix amb el runner. En CI, sempre npm ci.
Error 2: copiar el codi abans que el package.json al Dockerfile. Invalida la capa de dependències a cada canvi d'una línia de codi i converteix una build de 40 segons en una de 4 minuts.
Error 3: no tenir .dockerignore. El context es dispara de mida, s'hi colen node_modules incompatibles i, en el pitjor cas, un fitxer amb credencials queda dins de la imatge per sempre. Error 4: executar el contenidor com a root, que és el valor per defecte: sense USER node, el teu procés de producció té privilegis que no necessita.
Error 5: claus de memòria cau que no depenen del contingut. La fallada que fa que el pipeline verifiqui una versió del codi que no existeix. Si algun cop sospites de la memòria cau, esborra-la abans de continuar investigant.
Consell 1: construeix una vegada, prova moltes. Encara que en aquest mòdul el job build construeixi per verificar, la meta —lliçó 02-06— és que existeixi un sol artefacte per commit i que tota la resta hi operi a sobre.
Consell 2: mesura la mida de la imatge i posa-li un sostre. Una imatge que creix de 180 MB a 900 MB gairebé sempre significa que s'hi han colat dependències de desenvolupament; un docker images al registre n'hi ha prou per detectar-ho. I prova el Dockerfile en local abans de pujar-lo: docker build -f apps/api/Dockerfile -t prova . costa un minut i evita cinc execucions vermelles.
Exercicis
Exercici 1
Ordena les instruccions següents d'un Dockerfile d'una sola etapa per maximitzar l'aprofitament de la memòria cau, i explica el criteri:
COPY . .
RUN npm ci
COPY package.json package-lock.json ./
FROM node:20.11.0-bookworm-slim
RUN npm run build
WORKDIR /appExercici 2
El pipeline d'un equip fa servir aquesta configuració de memòria cau:
Descriu la fallada concreta que es produirà i per què és especialment difícil de diagnosticar. Proposa la correcció.
Exercici 3
La imatge final d'apps/api pesa 1,1 GB. Enumera quatre causes probables ordenades per impacte i la correcció de cadascuna.
Solucions
Solució 1. Ordre correcte:
FROM node:20.11.0-bookworm-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run buildEl criteri és del que canvia menys al que canvia més. Els package.json canvien poques vegades per setmana; el codi, diverses vegades al dia. En copiar primer només els manifestos i executar npm ci immediatament després, la capa de dependències només s'invalida quan canvien les dependències de debò. Amb COPY . . abans d'npm ci, qualsevol canvi en qualsevol fitxer obligaria a reinstal·lar-ho tot.
Solució 2. La clau moduls-node és fixa: no depèn del contingut del lockfile. La primera execució desa els node_modules d'aquell moment; a partir d'aquí, totes les execucions restauren aquesta mateixa còpia encara que el lockfile canviï. El pipeline provarà i construirà amb dependències antigues.
És difícil de diagnosticar perquè el pipeline és verd: no hi ha cap error, simplement es verifica un codi que no és el que es desplegarà. El símptoma apareix molt més tard i en un altre lloc —una funció que no existeix en producció, una fallada que en local no es reprodueix—, sense cap pista que apunti a la memòria cau.
La correcció és posar en memòria cau ~/.npm (la memòria cau de descàrregues, no node_modules) amb una clau derivada del contingut: key: npm-${{ hashFiles('package-lock.json') }}. I a Reservalia, encara millor: cache: npm a actions/setup-node, que ja fa exactament això.
Solució 3. Per impacte:
- Dependències de desenvolupament a la imatge final: TypeScript, Vitest i ESLint pesen centenars de megues. Correcció:
npm ci --omit=deven una etapa a part i copiar només aquestsnode_modules, com fa l'etapa 3 de l'exemple. - No fer servir construcció multietapa: tot el codi font, les eines de compilació i les capes intermèdies queden a la imatge. Correcció: separar
buildderuntimei copiar únicamentdist/. - Imatge base pesant:
node:20.11.0completa ronda el gigabyte davant dels ~200 MB de la variantslim. Correcció: fer servir-slim. - Manca de
.dockerignore: s'hi colennode_moduleslocals,.gitcomplet i fitxers de cobertura. Correcció: afegir-lo amb les entrades de l'apartat 6.
Conclusió
Reservalia ja construeix de manera automàtica i reproduïble:
- Construir és transformar el codi en l'artefacte executable amb una sola ordre, sense estat previ i fallant sorollosament quan alguna cosa va malament. Si cal explicar el procediment de paraula, encara no està automatitzat.
- Al monorepo, npm resol l'ordre topològic a partir de les dependències declarades:
tipus-compartitsabans queapiiweb. El pipeline no ha de conèixer aquest ordre. - La reproduïbilitat es recolza en tres pilars: el lockfile versionat,
npm cien comptes denpm installi el runtime fixat pel.nvmrc. Els enemics són sempre els mateixos: versions flotants, fitxers no versionats, descàrregues durant la build i restes d'execucions anteriors. - El
Dockerfilemultietapa d'apps/apisepara instal·lació, construcció, dependències de producció i execució; la imatge final només porta Node fixat a20.11.0, les dependències de producció idist/, corre ambUSER nodei va acompanyada d'un.dockerignore. - Hi ha dues memòries cau diferents —dependències i capes de Docker— i una regla que no es negocia: la clau s'ha de derivar del contingut. Una memòria cau mal invalidada produeix un pipeline verd que verifica codi que no existeix.
- El job
buildja forma part delci.yml: construeix els tres paquets, comprova que els artefactes existeixen i construeix la imatge sense publicar-la.
Tenim, doncs, una cosa que es construeix. Falta respondre a la pregunta de si funciona, i respondre-la amb rigor. A la lliçó següent, Proves Automatitzades, veurem la piràmide de proves aplicada al pipeline —què s'executa a cada PR i què no—, escriurem amb Vitest una prova unitària de la lògica de disponibilitat de cites i una d'integració contra el PostgreSQL del services:, parlarem de cobertura sense convertir-la en un objectiu, i posarem nom i política al destructor més gran de confiança en un pipeline: les proves inestables.
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
