Escena Viva és a internet. Però desplegar continua sent un ritual: algú executa les proves al seu portàtil (si se'n recorda), construeix la imatge, la publica, llança les migracions, escala els processos i comprova a mà que la venda funciona. Sis passos en l'ordre correcte, amb una persona concreta que sap fer-los. Si aquella persona és de vacances el dia de l'estrena del Festival de Jazz de Primavera, l'equip té un problema.
Aquesta lliçó tanca el mòdul automatitzant la cadena sencera. L'objectiu no és l'elegància tècnica: és que pujar una versió deixi de ser un esdeveniment. Que sigui avorrit. Que es faci cinc vegades al dia sense que ningú contingui la respiració. Un desplegament avorrit es fa sovint, i desplegar sovint és el que fa que cada desplegament sigui petit i, per tant, segur.
Contingut
- Integració, lliurament i desplegament continus: tres coses diferents
- Anatomia de la canonada d'Escena Viva
- GitHub Actions: el fitxer de CI, bloc a bloc
- Secrets a CI
- Què fa que una canonada sigui útil
- Construir una vegada i promoure l'artefacte
- Migracions, proves de fum i reversió automàtica
- Versionat i notes de la versió
- Què no ha d'estar a la canonada
- DORA: saber si estàs millorant
- Integració, lliurament i desplegament continus: tres coses diferents
Les sigles CI/CD es fan servir com una sola paraula i amaguen tres conceptes diferents. La integració contínua (CI) consisteix a integrar cada canvi a la branca principal amb freqüència —almenys cada dia— i, a cada integració, deixar que un sistema automàtic construeixi el projecte i executi les proves. Resol l'infern de la integració: dues persones treballant dues setmanes per separat i descobrint al final que els seus canvis són incompatibles. Amb CI, la incompatibilitat apareix al cap d'hores, quan arreglar-la costa minuts.
El lliurament continu (CD) va un pas més enllà: cada canvi que passa la CI queda llest per desplegar-se, amb l'artefacte construït, provat i emmagatzemat. Desplegar és prémer un botó. El que s'automatitza no és el desplegament, sinó la capacitat de desplegar en qualsevol moment. I el desplegament continu elimina el botó: cada canvi que passa totes les etapes arriba a producció tot sol.
| Integració contínua | Lliurament continu | Desplegament continu | |
|---|---|---|---|
| S'automatitza | Construir i provar | Tot fins a preproducció | Tot, fins a producció |
| A producció hi arriba | A mà | Prement un botó | Tot sol |
| Requereix | Proves fiables | L'anterior + entorns | L'anterior + fum i reversió |
| Risc per desplegament | L'habitual | Menor | El mínim, perquè són diminuts |
Escena Viva implementarà integració contínua completa i lliurament continu, amb desplegament automàtic a preproducció i aprovació manual per a producció. És l'elecció sensata per a un equip petit amb un negoci on una caiguda durant una venda costa diners directes. El desplegament continu pur és un objectiu legítim, però exigeix una confiança en les proves que es guanya amb el temps, no que es decreta.
- Anatomia de la canonada d'Escena Viva
flowchart TD
A[Push o PR] --> B[npm ci amb cache] --> C[Linter i format]
B --> D[Unitaries]
C --> E[Integracio: Postgres, Mongo, Redis]
D --> E
E --> F[Cobertura amb llindar] --> G[Auditoria] --> H{Branca principal?}
H -->|No| I[Fi: informe al PR]
H -->|Si| J[Construir imatge i publicar] --> K[Migrar i desplegar a preproduccio]
K --> L[Proves de fum]
L -->|Fallen| M[Reversio automatica]
L -->|Passen| N{Aprovacio manual} -->|Aprovat| O[Promoure la MATEIXA imatge]
O --> P[Fum en produccio] -->|Fallen| Q[Reversio automatica]
P -->|Passen| R[Notes de la versio]
Dues propietats del disseny, abans d'escriure codi. La primera: les etapes van de ràpida a lenta i de barata a cara; el linter triga 15 segons, i si falla no té sentit gastar quatre minuts en proves d'integració — és el mateix fallar de pressa que vam aplicar a la validació de configuració a 11-01. La segona: la imatge es construeix una sola vegada i després es promou, de manera que l'artefacte que va passar les proves i es va desplegar a preproducció és exactament el que arriba a producció. L'apartat 6 explica per què això no és negociable.
- GitHub Actions: el fitxer de CI, bloc a bloc
# .github/workflows/ci.yml
name: Integracio continua
on:
push:
branches: [master]
pull_request:
branches: [master]
# Cancel.la execucions anteriors de la mateixa branca: no gastis minuts
# provant un commit que ja ha estat reemplacat.
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
# 1. Qualitat estatica: rapida i barata, va primer.
qualitat:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '24.x', cache: 'npm' }
- run: npm ci
- run: npm run lint
- run: npm run format -- --check
# Prohibeix console a src: cal fer servir el logger de pino (11-02).
- run: '! grep -rn "console\." src/ --include="*.js"'
# 2. Unitaries als dos extrems del rang d'engines.
unitat:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix: { node: ['24.5', '24.x'] }
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '${{ matrix.node }}', cache: 'npm' }
- run: npm ci
- run: npm run test:unitat
# 3. Integracio: necessita les tres bases de dades del M9.
integracio:
runs-on: ubuntu-latest
needs: [qualitat, unitat]
services:
postgres:
image: postgres:17-alpine
env: { POSTGRES_USER: escena, POSTGRES_PASSWORD: escena, POSTGRES_DB: escena_viva_test }
ports: ['5432:5432']
options: >-
--health-cmd "pg_isready -U escena -d escena_viva_test"
--health-interval 5s --health-timeout 3s --health-retries 10
mongo:
image: mongo:8
ports: ['27017:27017']
options: >-
--health-cmd "mongosh --quiet --eval 'db.adminCommand(\"ping\")'"
--health-interval 5s --health-retries 10
redis:
image: redis:7-alpine
ports: ['6379:6379']
options: '--health-cmd "redis-cli ping" --health-interval 5s --health-retries 10'
env:
NODE_ENV: test
NIVELL_REGISTRE: error
URL_POSTGRES: postgres://escena:escena@localhost:5432/escena_viva_test
URL_MONGO: mongodb://localhost:27017/escena_viva_test
URL_REDIS: redis://localhost:6379
# Secrets de PROVA: no valen res, pero compleixen l'esquema de 11-01.
JWT_SECRET: '0123456789abcdef0123456789abcdef'
SESSIO_SECRET: 'fedcba9876543210fedcba9876543210'
CSRF_SECRET: 'aaaabbbbccccddddeeeeffff00001111'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '24.x', cache: 'npm' }
- run: npm ci
- run: npm run migrar
- name: Integracio amb cobertura i llindar
run: npm run test:ci
# 4. Cadena de subministrament (M5): checkout + setup-node + npm ci i, al final:
# - run: npm audit --audit-level=high --omit=devon, concurrency, jobs i runs-on. Els dos disparadors tenen propòsits diferents: a pull request s'executa abans de fusionar, com a barrera que impedeix que entri codi trencat; a push a master s'executa després, perquè el resultat de fusionar dues branques que passaven per separat pot fallar — la fusió semàntica trencada, més freqüent del que sembla. concurrency cancel·la l'execució anterior de la mateixa branca quan arriba un commit nou, així que si empenys tres commits seguits només es prova l'últim. Cada job s'executa en una màquina virtual neta i aïllada, per defecte en paral·lel; needs: [qualitat, unitat] estableix l'ordre i així s'implementa el «ràpid i barat primer» del diagrama. I runs-on: ubuntu-latest ha de coincidir amb producció: provar a Windows i desplegar a Alpine és demanar sorpreses amb rutes, permisos i mòduls natius.
checkout, setup-node i la memòria cau. actions/checkout@v4 clona el repositori; fixa't en el @v4, que fixa la versió major de l'acció, perquè fer servir una acció sense versió —o d'un tercer desconegut— és executar codi aliè amb accés al teu repositori, i per a accions no oficials convé fixar el hash del commit. actions/setup-node@v4 instal·la Node, i cache: 'npm' és la línia que més temps estalvia de tot el fitxer: desa la memòria cau d'npm entre execucions fent servir el hash de package-lock.json com a clau, de manera que npm ci passa d'uns 45 segons amb memòria cau freda a uns 8 amb memòria cau calenta.
npm ci: el pagament definitiu del mòdul 5. Instal·la exactament el que diu package-lock.json sense resoldre rangs, així que el que es prova a CI és idèntic versió per versió al que s'executa en producció; esborra node_modules abans d'instal·lar, de manera que no hi ha estat heretat d'una altra execució; falla si package.json i el lock no són coherents, atrapant l'error d'editar l'un sense l'altre; i és més ràpid, perquè es salta la resolució. npm install a CI és un antipatró: pot resoldre una versió nova d'una dependència transitiva i provocar el cas pitjor —que CI passi i producció falli, o a l'inrevés—, i a més modifica el lock sense que ningú ho revisi.
La matriu de versions. engines diu >=24.5.0 <25, així que es proven els dos extrems del rang suportat: la versió mínima declarada i l'última del major. Si alguna cosa fa servir una API que només existeix des de la 24.8, la 24.5 ho detecta — i aquella és justament la que podria instal·lar la PaaS de 11-05. fail-fast: false evita que la fallada d'una combinació cancel·li les altres, perquè saber si falla a totes dues o només a una canvia el diagnòstic.
Serveis auxiliars. El bloc services dóna vida a les proves d'integració del mòdul 9. Tres coses importants:
localhost, no el nom del servei. A diferència dedocker compose(11-04), la feina s'executa a la màquina amfitriona i els contenidors hi exposen ports. És la confusió número u en migrar de compose a Actions.- Les comprovacions de salut no són opcionals. Sense
--health-cmd, la feina arrenca tan bon punt el contenidor existeix, no quan el servei accepta connexions. PostgreSQL triga uns segons, i el resultat és una prova que falla una de cada deu vegades: una prova intermitent creada per la mateixa configuració de CI. - Les versions coincideixen amb producció. Provar contra PostgreSQL 15 i desplegar sobre 17 és provar una altra cosa.
Els env reprodueixen .env.test del mòdul 9, amb secrets de mentida que compleixen el mínim de 32 caràcters que imposa l'esquema de 11-01. Un altre dividend de validar la configuració: si l'esquema i l'entorn de CI es desincronitzen, la fallada és immediata i explícita. Sobre cobertura i auditoria, npm run test:ci executa les proves sota c8, i el llindar es declara a la configuració de c8, no al YAML:
{
"c8": {
"all": true, "include": ["src/**/*.js"],
"lines": 80, "functions": 80, "branches": 70,
"check-coverage": true
}
}Amb check-coverage, c8 surt amb codi diferent de zero si no s'assoleix el llindar i la feina falla. Sobre els números: fixa el llindar al nivell actual i puja'l de mica en mica, perquè posar 90 % quan ets al 62 % garanteix que algú desactivi la comprovació en dues setmanes; i recorda del mòdul 9 que la cobertura mesura quines línies s'executen, no si les proves comproven alguna cosa útil. Per la seva banda, npm audit --audit-level=high --omit=dev tanca el mòdul 5: només falla davant de vulnerabilitats altes o crítiques, perquè la canonada no es trenqui cada setmana per un avís menor, i ignora les dependències de desenvolupament, que no arriben a producció. Com a complement, un escaneig de la imatge amb Trivy detecta a més les del sistema base.
- Secrets a CI
Els secrets es desen a la configuració del repositori o de l'organització i s'injecten com a variables (env: { TOKEN: '${{ secrets.REGISTRE_TOKEN }}' }). Les regles que cal respectar:
- Mínim privilegi. El token que publica imatges només ha de poder escriure al registre. Res de tokens personals amb accés complet al compte.
- Mai al
rundirectament.docker login -p ${{ secrets.TOKEN }}deixa el valor a la línia d'ordres, visible a la taula de processos. Passa'l perenvi llegeix-lo amb--password-stdin. - Els secrets no s'exposen als PR de forks. GitHub ho fa per disseny, i és essencial: sense això, qualsevol podria obrir un PR amb un flux modificat que imprimís les teves credencials. La conseqüència pràctica és que les feines que necessiten secrets —construir i desplegar— només s'executen en pushes a
master. - Rotació periòdica, amb data de caducitat si el proveïdor l'ofereix.
- Si se'n filtra un, s'aplica el procediment de 11-01: revocar primer, investigar després. Els registres de CI són públics en repositoris públics i, encara que GitHub emmascara els valors coneguts, un secret construït a trossos o codificat en base64 escapa d'aquell filtre.
Un patró modern que convé conèixer és OIDC: en comptes de desar credencials de vida llarga, el flux obté un token temporal del proveïdor de núvol demostrant la seva identitat, eliminant el secret persistent completament.
- Què fa que una canonada sigui útil
Una canonada que existeix però que ningú mira és pitjor que no tenir-ne cap: dóna una falsa sensació de seguretat. Tres propietats la fan útil. Ràpida. Objectiu: menys de 10 minuts des del push fins al resultat. Per sobre d'això la gent canvia de tasca, perd el context i deixa d'esperar. Les tècniques que més rendeixen són desar npm a la memòria cau (30-40 s per feina), paral·lelitzar feines independents (pagues la més llarga, no la suma), posar el barat abans que el car amb needs, cancel·lar execucions obsoletes amb concurrency i desar les capes de Docker a la memòria cau (1-3 min per construcció).
Fiable. Aquí hi ha la causa número u que un equip abandoni la seva CI: les proves intermitents. Una prova intermitent falla de vegades sense que el codi hagi canviat. El dany no és la fallada, és el que provoca a les persones: la primera vegada s'investiga; la tercera, algú diu «torna-la a llançar, és aquella intermitent». A partir d'aquí qualsevol fallada vermella s'atribueix a la intermitència, inclosos els reals, i la CI ha deixat d'aportar informació per aportar només retard. Al mòdul 9 vam veure les causes, i aquí reapareixen agreujades perquè les màquines de CI són més lentes i variables que el teu portàtil:
| Causa | Símptoma | Solució |
|---|---|---|
| Dependència de l'ordre | Falla en executar-se sola o en un altre ordre | Estat net a cada beforeEach |
| Temps d'espera fixos | Falla en màquines lentes | Esperar condicions, no rellotges |
| Dates i zones horàries | Falla a mitjanit o en una altra regió | Rellotge fals amb sinon; UTC a CI |
| Servei no preparat o port fix | Falla la primera prova, o en paral·lelitzar | Comprovacions de salut; port 0 |
Política recomanada: una prova intermitent s'arregla o s'elimina en 48 hores. Marcar-la com a omesa és acceptable com a mesura temporal, amb una tasca associada; deixar-la fallant, no.
Amb la branca principal sempre desplegable. De res no serveix la canonada si es pot fusionar codi vermell. A master es configura protecció de branca: prohibit el push directe, comprovacions obligatòries (qualitat, unitat, integracio, auditoria), almenys una revisió aprovatòria i branca actualitzada abans de fusionar, cosa que evita la fusió semàntica trencada. Amb això, l'estat de master és sempre «provat i desplegable», i aquell invariant és el que permet desplegar sense por durant l'estrena del Festival de Jazz.
- Construir una vegada i promoure l'artefacte
Construeix una vegada, promou l'artefacte. No reconstrueixis mai per entorn.
Si construeixes una imatge per a preproducció i una altra per a producció, no són la mateixa imatge: entre una construcció i l'altra pot canviar una dependència transitiva, una imatge base o un paquet del sistema. Hauries provat una cosa i desplegat una altra, i tota la feina anterior no valdria res.
# .github/workflows/desplegament.yml
name: Desplegament
on:
push:
branches: [master]
workflow_dispatch: # Permet llancar-lo a ma des de la interficie.
jobs:
construir:
runs-on: ubuntu-latest
outputs:
etiqueta: ${{ steps.meta.outputs.etiqueta }}
steps:
- uses: actions/checkout@v4
- id: meta
name: Etiqueta = versio + hash del commit
run: |
VERSION=$(node -p "require('./package.json').version")
echo "etiqueta=${VERSION}-${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT"
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ghcr.io/escena-viva/api:${{ steps.meta.outputs.etiqueta }}
cache-from: type=gha # Reaprofita les capes de 11-04.
cache-to: type=gha,mode=max
# preproduccio: identic a la feina de sota, amb environment: preproduccio,
# els seus propis secrets _PRE i fum contra https://pre.escenaviva.test
produccio:
needs: [construir, preproduccio]
runs-on: ubuntu-latest
environment: produccio # Amb revisors obligatoris configurats.
steps:
- uses: actions/checkout@v4
- name: Migrar l'esquema
env: { URL_POSTGRES: '${{ secrets.URL_POSTGRES_PROD }}' }
run: npm ci --omit=dev && npm run migrar
- name: Promoure LA MATEIXA imatge, sense reconstruir
env: { TOKEN_PLATAFORMA: '${{ secrets.TOKEN_PLATAFORMA_PROD }}' }
run: ./scripts/desplegar.sh produccio ${{ needs.construir.outputs.etiqueta }}
- name: Proves de fum
id: fum
run: node scripts/fum.js https://escenaviva.test
- name: Revertir si el fum falla
if: failure() && steps.fum.outcome == 'failure'
env: { TOKEN_PLATAFORMA: '${{ secrets.TOKEN_PLATAFORMA_PROD }}' }
run: ./scripts/revertir.sh produccioClaus del flux: outputs propaga l'etiqueta calculada a la construcció fins a les feines de desplegament, de manera que preproducció i producció reben la mateixa cadena i despleguen la mateixa imatge. L'etiquetatge amb versió i hash (1.4.0-8f3a1c2) combina el que un humà entén amb la veritat exacta del commit, i evita latest, que no és una versió sinó un àlies mutable. environment: produccio activa els entorns de GitHub, on es configuren revisors obligatoris: la feina s'atura i espera aprovació humana, i aquí hi ha la frontera entre lliurament continu i desplegament continu — moure-la és canviar una línia. workflow_dispatch permet llançar el flux a mà per tornar a desplegar sense un commit nou. I cache-from/cache-to desen les capes de Docker aprofitant l'ordre de capes de 11-04: si package-lock.json no ha canviat, la capa d'npm ci es reutilitza.
- Migracions, proves de fum i reversió automàtica
Les migracions s'executen abans del desplegament del codi, en un pas propi i una sola vegada. És la mateixa decisió de 11-04 (fora del CMD) i de 11-05 (fase d'alliberament). I funciona sense talls només perquè segueixen l'estratègia expandir → migrar → contraure: durant el reemplaçament progressiu d'instàncies conviuen el codi antic i el nou sobre l'esquema ja migrat, així que la migració ha de ser compatible amb tots dos.
| Pas | Què passa | Si falla |
|---|---|---|
| 1 | Migració compatible cap enrere | S'avorta; el codi antic continua amb l'esquema antic |
| 2 | Desplegament progressiu de la imatge nova | La plataforma atura el reemplaçament |
| 3 | Proves de fum | Reversió automàtica a l'artefacte anterior |
| 4 | (Desplegament posterior) migració de contracció | Només quan res no fa servir ja el que s'elimina |
Un avís sobre els temps: una migració que bloquegi una taula gran durant minuts converteix un desplegament sense talls en una caiguda, així que mesura'n la durada sobre una còpia del volum real abans de fusionar-la. Pel que fa a les proves de fum, comproven que el que s'ha desplegat respon i fa l'essencial; no substitueixen les d'integració, sinó que verifiquen que el desplegament en si ha anat bé.
// scripts/fum.js
'use strict';
const base = process.argv[2];
const comprovacions = [
{
nom: 'sonda de disponibilitat',
async executar() {
const resposta = await fetch(`${base}/salut/preparat`);
if (resposta.status !== 200) throw new Error(`estat ${resposta.status}`);
// Les tres dependencies han de respondre: el valor de la sonda de 11-02.
const { detall } = await resposta.json();
const caigudes = Object.entries(detall).filter(([, ok]) => !ok).map(([n]) => n);
if (caigudes.length > 0) throw new Error(`dependencies caigudes: ${caigudes.join(', ')}`);
},
},
{
nom: 'cataleg public',
async executar() {
const { dades } = await (await fetch(`${base}/api/esdeveniments`)).json();
if (!Array.isArray(dades) || dades.length === 0) throw new Error('cataleg buit');
},
},
{
// Sense token ha de respondre 401. Si respon 200, alguna cosa greu ha canviat.
nom: 'autenticacio exigida',
async executar() {
const resposta = await fetch(`${base}/api/compres`, { method: 'POST' });
if (resposta.status !== 401) throw new Error(`esperava 401, ha arribat ${resposta.status}`);
},
},
];
// Espera creixent: el desplegament progressiu triga a completar-se.
async function ambReintents(comprovacio, intents = 5) {
for (let intent = 1; intent <= intents; intent += 1) {
try {
return await comprovacio.executar();
} catch (error) {
if (intent === intents) throw error;
await new Promise((resoldre) => setTimeout(resoldre, intent * 2000));
}
}
}
(async () => {
let fallades = 0;
for (const comprovacio of comprovacions) {
try {
await ambReintents(comprovacio);
process.stdout.write(`OK ${comprovacio.nom}\n`);
} catch (error) {
process.stderr.write(`FALLADA ${comprovacio.nom}: ${error.message}\n`);
fallades += 1;
}
}
process.exit(fallades > 0 ? 1 : 0);
})();Tres decisions deliberades: els reintents amb espera creixent són imprescindibles perquè el desplegament progressiu triga a completar-se i les primeres peticions poden arribar a una instància que encara arrenca; s'executen totes les comprovacions abans de sortir, per veure el quadre complet; i comprovar que /api/compres retorna 401 és un sentinella de seguretat que detectaria en segons un desplegament que trenqués l'autenticar.js del mòdul 8. Que l'script surti amb codi diferent de zero és el que dispara el pas de reversió amb if: failure(), i aquí la feina de 11-05 tanca el cercle: revertir és tornar a l'artefacte anterior, operació de segons perquè les imatges estan etiquetades i les migracions són compatibles cap enrere.
- Versionat i notes de la versió
El mòdul 5 ens va donar npm version, que actualitza package.json, crea un commit i una etiqueta de Git: patch per a correccions, minor per a funcionalitat compatible, major per a canvis incompatibles. Per automatitzar les notes calen missatges de commit llegibles per màquina, i els commits convencionals són el format estàndard: feat(compres): permetre seleccionar butaca a l'Auditorio Ribera, fix(aforament): corregir el recompte en cancel·lar una reserva provisional, chore(deps): actualitzar pino a 9.5.0.
| Prefix | Significat | Efecte en la versió |
|---|---|---|
fix: |
Correcció d'error | patch |
feat: |
Funcionalitat nova | minor |
BREAKING CHANGE: al cos |
Canvi incompatible | major |
chore:, docs:, test:, refactor: |
Sense impacte en l'usuari | cap |
Amb això, eines com semantic-release tanquen el cicle: analitzen els commits des de l'última etiqueta, decideixen la versió, generen el CHANGELOG.md, etiqueten i publiquen. L'equip deixa de discutir si un canvi és minor o patch, perquè ho decideix el format del commit. Començar pel més simple —commits convencionals i npm version a mà— és perfectament vàlid; l'automatització total s'afegeix quan el ritme de publicacions ho justifiqui.
- Què no ha d'estar a la canonada
| Antipatró | Per què és un problema |
|---|---|
| Secrets en clar al YAML | Es llegeixen al repositori i a cada registre d'execució |
npm install en comptes d'npm ci |
Instal·la versions diferents de les provades i modifica el lock |
| Passos manuals no documentats | «Abans de desplegar cal executar X» s'oblida, i qui ho sabia se'n va |
| Reconstruir la imatge per entorn | Desplegues alguna cosa diferent del que vas provar |
Etiquetar amb latest |
No saps què hi ha desplegat ni a què revertir |
| Proves contra producció | Dades reals contaminades; correus a persones reals |
El cas dels passos manuals mereix èmfasi. Una canonada amb un forat —«i després algú neteja la memòria cau de Redis a mà»— no és una canonada automatitzada: és una llista d'ordres amb parts ocultes. Si un pas és necessari, va a dins; si no hi cap, va escrit en un procediment que qualsevol pugui seguir.
- DORA: saber si estàs millorant
El programa de recerca DORA va identificar quatre mètriques que correlacionen amb el rendiment dels equips que lliuren programari. Serveixen per saber si el teu procés millora, no per comparar equips ni avaluar persones.
| Mètrica | Què mesura | Rendiment alt | Rendiment baix |
|---|---|---|---|
| Freqüència de desplegament | Cada quant arriba un canvi a producció | Diverses vegades al dia | Menys d'una vegada al mes |
| Temps de lliurament | De commit a producció | Menys d'un dia | Més d'un mes |
| Taxa de fallades en canvis | Quin percentatge causa una incidència | 0-15 % | 46-60 % |
| Temps de recuperació | Quant trigues a restaurar el servei | Menys d'una hora | Dies o setmanes |
La troballa més contraintuïtiva: velocitat i estabilitat no estan en conflicte. Els equips que despleguen més sovint també fallen menys i es recuperen abans, perquè desplegar sovint obliga que cada desplegament sigui petit, i un canvi petit és fàcil de revisar, provar i revertir. La por a desplegar produeix desplegaments grans, i els grans són els que trenquen coses. El que hem construït en aquest mòdul mou les quatre: la canonada permet desplegar quan es vulgui, de commit a preproducció passen minuts, linter i proves i fum atrapen les fallades abans que els usuaris, i la reversió automàtica restaura el servei en segons. Un consell final: mesura-les, però no les converteixis en objectiu de ningú, perquè tan bon punt la freqüència de desplegament és una xifra que algú ha d'assolir apareixen desplegaments buits. Són un termòmetre, no una nota.
Errors Comuns i Consells
- Conviure amb proves intermitents. És la causa principal que l'equip deixi de mirar CI. Arregla-les o elimina-les en 48 hores.
- Serveis sense comprovació de salut. Produeixen fallades aleatòries que semblen defectes del codi i no ho són.
- Usar el nom del servei en comptes de
localhosta Actions. A compose sí, a Actions no. npm installa CI. Trenca la reproduïbilitat quepackage-lock.jsongaranteix.- Reconstruir per entorn. L'artefacte que vas provar no és el que desplegues.
- Llindar de cobertura irreal. Algú el desactivarà. Fixa el nivell actual i puja a poc a poc.
- Canonada sense protecció de branca. Es pot fusionar codi vermell, i llavors no serveix de res.
- Consell: fes que la fallada de CI sigui visible on l'equip ja mira. Una canonada trencada que ningú veu és inútil.
- Consell: mesura el temps de la teva canonada i tracta'l com un indicador de producte. Cada minut estalviat es multiplica per totes les execucions de l'any.
Exercicis
Exercici 1 — Diagnòstic d'una prova intermitent
Una prova d'integració falla a CI aproximadament una de cada cinc execucions, sempre amb ECONNREFUSED contra PostgreSQL, i mai no falla en local. Enumera les tres causes més probables i la correcció de cadascuna.
Exercici 2 — Prova de fum del flux de compra
Amplia scripts/fum.js amb una comprovació que verifiqui el camí de compra a preproducció: autenticar-se amb un usuari de prova, consultar l'aforament d'evt-003 i comprovar que el punt d'entrada de compra respon amb un 400 controlat davant d'un cos buit, en comptes d'un 500.
Solucions
Exercici 1. Primera i més probable: falta la comprovació de salut del servei, o la feina comença abans que PostgreSQL accepti connexions; explica que mai no falli en local, on la base de dades porta hores arrencada, i es corregeix amb --health-cmd "pg_isready ..." i reintents suficients. Segona: les connexions no es tanquen entre proves i s'exhaureix el max_connections del contenidor; es corregeix tancant el pool a l'after global de mocha i comprovant que test/ajudes/ no crea una connexió per fitxer. Tercera: la màquina de CI és més lenta i algun temps d'espera fix del codi de connexió s'exhaureix; es corregeix augmentant el temps d'adquisició del pool a l'entorn de prova i esperant condicions, mai rellotges.
Exercici 2.
{
nom: 'flux de compra accessible',
async executar() {
const json = { 'content-type': 'application/json' };
// 1. Usuari sembrat NOMES en preproduccio; contrasenya des de secrets.
const cos = { correu: '[email protected]', contrasenya: process.env.CONTRASENYA_FUM };
const acces = await fetch(`${base}/api/sessions`, {
method: 'POST', headers: json, body: JSON.stringify(cos),
});
if (acces.status !== 200) throw new Error(`entrada: estat ${acces.status}`);
const auth = { authorization: `Bearer ${(await acces.json()).token}` };
// 2. Aforament del Festival de Jazz de Primavera.
const aforament = await fetch(`${base}/api/esdeveniments/evt-003/aforament`, { headers: auth });
if (aforament.status !== 200) throw new Error(`aforament: estat ${aforament.status}`);
// 3. Validacio: cos buit ha de donar 400, MAI 500.
const compra = await fetch(`${base}/api/compres`, {
method: 'POST', headers: { ...auth, ...json }, body: '{}',
});
if (compra.status !== 400) throw new Error(`esperava 400, ha arribat ${compra.status}`);
},
}El detall important és l'últim: comprovar que un cos invàlid produeix 400 i no 500 verifica d'un cop que la validació zod, el gestor d'errors del mòdul 6 i el mapa ESTAT_PER_CODI continuen connectats. Un 500 allà significaria que alguna cosa s'ha desconnectat al muntatge de l'aplicació, i és exactament el tipus de trencament que una prova de fum ha d'atrapar abans que un usuari.
Conclusió
Escena Viva va començar aquest mòdul sent un projecte que funcionava en un portàtil, amb els secrets en un .env local, sense registres centralitzats, sense supervisor, sense contenidor i sense desplegament automàtic. Acaba sent una altra cosa. La seva configuració és un contracte explícit que es valida en arrencar i mata el procés amb un missatge clar si falta un secret, amb rotació de claus sense desconnectar ningú. Explica el que fa: registres JSON estructurats on cada línia porta l'identificador de petició que permet reconstruir una compra fallida sencera, mètriques de latència, entrades venudes, mida de cua i retard del bucle d'esdeveniments, i sondes de vivacitat i disponibilitat que un supervisor pot interrogar. Està supervisada per un fitxer d'ecosistema que declara els seus dos processos i els recarrega sense tallar ni una venda. Viatja empaquetada en una imatge de 97 MB que s'executa com a usuari sense privilegis i rep SIGTERM de debò, juntament amb un docker compose que aixeca l'entorn complet amb una ordre. Està desplegada en una plataforma que li assigna el port, li injecta la configuració, acaba el TLS a la vora i l'escala horitzontalment perquè cada peça d'estat es va treure del procés al seu degut temps. I ara està automatitzada: cada push passa pel linter, les proves unitàries, les d'integració contra tres bases de dades reals, un llindar de cobertura i una auditoria de dependències; la imatge es construeix una sola vegada i es promou entre entorns; les migracions s'executen al seu pas, compatibles cap enrere; i unes proves de fum contra /salut/preparat decideixen si la versió es queda o es reverteix sola.
Escena Viva és un producte en producció, no un projecte en un portàtil. I la diferència no és al codi —el de les compres és el mateix del mòdul 7—, sinó en tot el que l'envolta: configuració, observabilitat, supervisió, empaquetatge, desplegament i automatització. Aquesta és la distància que separa saber programar en Node de saber lliurar programari en Node, i acabes de recórrer-la sencera.
Queda una última part del viatge. Al Mòdul 12, Projectes del Món Real, deixem de construir una sola aplicació per construir-ne quatre de completes, cadascuna amb el seu propi repte: una aplicació de xat en temps real amb Socket.IO, una API de comerç electrònic, una plataforma de blogs i una eina de gestió de tasques. Cada projecte aplicarà el que s'ha après als onze mòduls anteriors —mòduls, streams, Express, bases de dades, autenticació, proves, rendiment i tot el que acabes de veure sobre desplegament— i el mòdul tancarà el curs amb el pas que avui ja saps fer: de projecte a producció.
Curs de Node.js: De Principiant a Avançat
Mòdul 1: Introducció a Node.js
- Què és Node.js?
- Instal·lació i Configuració de l'Entorn
- El Teu Primer Programa en Node.js
- El REPL de Node.js
- JavaScript Modern per a Node.js
- El Projecte del Curs: la Plataforma Escena Viva
Mòdul 2: Conceptes Bàsics
- Arquitectura de Node.js
- El Bucle d'Esdeveniments (Event Loop)
- Callbacks i Programació Asíncrona
- Promeses i async/await
- Esdeveniments i EventEmitter
- Mòduls CommonJS i require()
- Mòduls ES i Interoperabilitat
Mòdul 3: Sistema de Fitxers i E/S
- Lectura i Escriptura de Fitxers
- El Mòdul fs a Fons
- Rutes Multiplataforma amb el Mòdul path
- Treballant amb Streams
- Streams de Transformació i pipeline
- Buffers i Dades Binàries
Mòdul 4: HTTP i Servidors Web
- Creant un Servidor HTTP Simple
- Gestió de Sol·licituds i Respostes
- Enrutament Manual
- Servint Fitxers Estàtics
- Rebent Dades: Cossos de Petició i JSON
- Consumint APIs Externes des de Node.js
Mòdul 5: NPM i Gestió de Paquets
- Introducció a NPM i package.json
- Instal·lació i Ús de Paquets
- Versionat Semàntic i package-lock
- Scripts d'npm i Automatització del Projecte
- Creació i Publicació de Paquets
- Seguretat i Manteniment de Dependències
Mòdul 6: Framework Express.js
- Introducció a Express.js
- Configuració d'una Aplicació Express
- Enrutament a Express
- Middleware
- Middleware de Tercers Essencials
- Validació de Dades d'Entrada
- Gestió d'Errors
Mòdul 7: Bases de Dades i ORMs
- Introducció a les Bases de Dades
- Usant MongoDB amb Mongoose
- Operacions CRUD
- Relacions, Poblat i Consultes Avançades
- Usant Bases de Dades SQL amb Sequelize
- Migracions, Transaccions i Dades de Prova
Mòdul 8: Autenticació i Autorització
- Introducció a l'Autenticació
- Registre d'Usuaris i Hash de Contrasenyes
- Sessions i Galetes amb Passport.js
- Autenticació amb JWT
- Control d'Accés Basat en Rols
- Bones Pràctiques de Seguretat en APIs
Mòdul 9: Proves i Depuració
- Introducció a les Proves
- Proves Unitàries amb Mocha i Chai
- Dobles de Prova amb Sinon
- Proves d'Integració
- Cobertura i Automatització de les Proves
- Depuració d'Aplicacions Node.js
Mòdul 10: Temes Avançats
- El Mòdul Cluster
- Fils de Treball (Worker Threads)
- Memòria Cau i Cues de Treball amb Redis
- Optimització del Rendiment
- Construcció d'APIs RESTful
- GraphQL amb Node.js
Mòdul 11: Desplegament i DevOps
- Configuració i Variables d'Entorn
- Registre i Monitoratge en Producció
- Usant PM2 per a la Gestió de Processos
- Empaquetatge amb Docker
- Desplegant a Heroku i Altres PaaS
- Integració i Desplegament Continus
