La lliçó anterior acabava amb una frase que ara toca complir: «el que queda és fer-ho tu». Durant sis mòduls has llegit el ci.yml de Reservalia línia a línia, has vist com es promociona un artefacte per digest i com es torna enrere en quatre minuts. Tot allò era el pipeline d'una altra persona. A partir d'aquí el pipeline és teu: l'escrius, el trenques expressament, l'arregles i el tornes a trencar fins que entenguis per què falla cada cosa. Aquest primer laboratori construeix des de zero un projecte anomenat Mini-Reservalia —una versió reduïda i autocontinguda de Reservalia, amb el mateix domini de cites i disponibilitat però sense base de dades, sense AWS i sense dependències de producció— i li munta a sobre el seu primer flux de treball d'integració contínua, en quatre increments que pots veure executar-se un a un. En acabar tindràs un repositori a GitHub on cap canvi no pot arribar a main sense que les proves passin en verd, i ho hauràs comprovat intentant saltar-t'ho.
El projecte que creïs aquí no es llença: les lliçons 07-02 a 07-06 l'amplien. Pren-te seriosament el codi d'aquest primer apartat, perquè és el substrat de tot el mòdul.
Contingut
- Objectiu, requisits previs i punt de partida
- Preparar la màquina i verificar les eines
- Crear Mini-Reservalia des de zero
- Inicialitzar el repositori i pujar-lo a GitHub
- Increment 1: el flux de treball mínim que només s'executa
- Increment 2: instal·lar, comprovar i provar
- Increment 3: dos jobs en paral·lel amb
needs - Increment 4: memòria cau de dependències i mesura de l'efecte
- Provocar una fallada expressament i llegir el vermell
- El flux real: branca, pull request, comprovacions, merge
- Protegir
maini comprovar que bloqueja - El distintiu d'estat al README
- Verificació final
- Errors Comuns i Consells
- Exercicis
- Conclusió
- Objectiu, requisits previs i punt de partida
Objectiu. En acabar aquesta lliçó tindràs un repositori a GitHub amb un projecte Node.js funcional i un flux de treball de GitHub Actions que, a cada push i a cada pull request, instal·la dependències de manera reproduïble, executa l'anàlisi estàtica i les proves en paral·lel, i bloqueja el merge a main si alguna cosa falla.
Requisits previs.
| Requisit | Per què | Com comprovar-ho |
|---|---|---|
| Node.js 20.6 o superior | El projecte fa servir el runner de test natiu (node:test) i ESM |
node --version |
| npm 10 o superior | npm ci i el lockfile v3 |
npm --version |
| Git 2.30 o superior | Branques, remots | git --version |
| Compte de GitHub gratuït | Actions inclou minuts gratis en repositoris públics | Entra a github.com |
| Docker (opcional) | Només es fa servir a partir de la 07-03 | docker --version |
Punt de partida. Un directori buit. No hi ha res previ: aquest és el quilòmetre zero.
Cost. Zero. GitHub Actions és gratuït i il·limitat en minuts per a repositoris públics. Si fas el repositori privat consumiràs de la quota gratuïta mensual del pla Free, que sobra de llarg per a aquest mòdul. Recomanació: fes-lo públic.
Equivalent real a Reservalia. El repositori de Reservalia és privat, amb Actions sobre runners allotjats de mida més gran per als jobs de test. Res del que fas aquí no canvia conceptualment: canvia la factura.
- Preparar la màquina i verificar les eines
Abans d'escriure ni una línia, comprova l'entorn. Un percentatge incòmode dels «no em funciona el pipeline» del món real són en realitat «tinc Node 16 en local i 20 a la CI».
# Pas 0: verificacio de l entorn
node --version # esperat: v20.6.0 o superior (v22.x tambe val)
npm --version # esperat: 10.x o superior
git --version # esperat: 2.30 o superiorQuè has de veure: tres versions que compleixin els mínims. Si node --version mostra v18.x o inferior, instal·la una versió recent (amb nvm install 20 && nvm use 20, amb l'instal·lador oficial o amb el gestor de paquets del teu sistema). El runner de test natiu existeix des de Node 18, però l'opció --test-shard que farem servir a la 07-02 requereix Node 20.6+.
Configura també la teva identitat de Git si no la tens, perquè els commits sense autor donen problemes a l'hora de calcular mètriques més endavant:
git config --global user.name "El Teu Nom"
git config --global user.email "[email protected]"Opcionalment, instal·la la CLI de GitHub (gh). No és obligatòria —tot es pot fer des del web— però escurça molt els passos i a la 07-04 la farem servir per calcular les mètriques DORA:
gh --version
gh auth login # tria GitHub.com > HTTPS > autenticar amb navegador
gh auth status # esperat: "Logged in to github.com as <el-teu-usuari>"
- Crear Mini-Reservalia des de zero
Mini-Reservalia resol el problema central de Reservalia —donat un horari d'obertura i unes cites ja reservades, quins forats queden lliures?— amb zero dependències de producció. Aquesta restricció és deliberada: un projecte sense dependències arrenca en mil·lisegons, es pot executar en qualsevol runner i deixa el focus en el pipeline, que és el que estem aprenent.
Crea l'estructura:
3.1 package.json
{
"name": "mini-reservalia",
"version": "0.1.0",
"private": true,
"description": "Versio reduida de Reservalia per al modul practic de CI/CD",
"type": "module",
"engines": {
"node": ">=20.6.0"
},
"scripts": {
"lint": "eslint .",
"test": "node --test test/",
"build": "node scripts/build.js",
"start": "node src/servidor.js"
},
"devDependencies": {
"@eslint/js": "^9.14.0",
"eslint": "^9.14.0"
}
}Quatre decisions que importen:
"type": "module": fem servir ESM (import/export), com el Reservalia real."engines": documenta la versió mínima. No la imposa per si sola, perònpm ciavisa i a la 07-02 la farem servir com a referència de la matriu.- Els quatre scripts (
lint,test,build,start) són el contracte entre el projecte i el pipeline. Això és el que la 06-07 anomenava «lògica als scripts i YAML prim»: el flux de treball no sabrà com es fa el lint, només que existeixnpm run lint. Si demà canvies ESLint per una altra cosa, elci.ymlno es toca. - Zero dependències de producció:
dependenciesni tan sols hi apareix.
3.2 src/disponibilitat.js
Aquest és el cor del domini. Llegeix-lo amb calma, perquè a la 07-02 n'escriuràs els casos límit.
// src/disponibilitat.js
// Calcul de forats lliures per a Mini-Reservalia.
// Sense dependencies: nomes aritmetica de minuts des de mitjanit.
const PATRO_HORA = /^([01]\d|2[0-3]):([0-5]\d)$/;
/**
* Converteix "HH:MM" en minuts des de mitjanit.
* @param {string} hhmm hora en format 24 h
* @returns {number} minuts (0..1439)
*/
export function aMinuts(hhmm) {
if (typeof hhmm !== 'string' || !PATRO_HORA.test(hhmm)) {
throw new TypeError(`Hora invalida: ${JSON.stringify(hhmm)}. S esperava "HH:MM" en 24 h.`);
}
const [hores, minuts] = hhmm.split(':').map(Number);
return hores * 60 + minuts;
}
/**
* Converteix minuts des de mitjanit en "HH:MM".
* @param {number} minuts
* @returns {string}
*/
export function aHora(minuts) {
const h = Math.floor(minuts / 60);
const m = minuts % 60;
return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`;
}
/**
* Normalitza la llista de cites: converteix a minuts, descarta les degenerades,
* ordena i FUSIONA els solapaments. Sense aquesta fusio, dues cites que es
* trepitgen generarien forats fantasma.
*/
function normalitzarOcupacio(cites) {
return cites
.map((cita) => ({ inici: aMinuts(cita.inici), fi: aMinuts(cita.fi) }))
.filter((cita) => cita.fi > cita.inici) // una cita de duracio 0 o negativa no ocupa
.sort((a, b) => a.inici - b.inici)
.reduce((acumulat, cita) => {
const ultima = acumulat[acumulat.length - 1];
if (ultima && cita.inici <= ultima.fi) {
ultima.fi = Math.max(ultima.fi, cita.fi); // es solapen o es toquen: fusionar
} else {
acumulat.push({ ...cita });
}
return acumulat;
}, []);
}
/** Trosseja l interval [desDe, finsA) en slots de `duracio` minuts. */
function trossejar(desti, desDe, finsA, duracio) {
for (let inici = desDe; inici + duracio <= finsA; inici += duracio) {
desti.push({ inici: aHora(inici), fi: aHora(inici + duracio) });
}
}
/**
* Calcula els forats lliures d un dia.
*
* @param {{inici: string, fi: string}[]|{inici: string, fi: string}} horari
* Un o diversos trams d obertura. Diversos trams = horari partit.
* @param {{inici: string, fi: string}[]} cites Cites ja reservades.
* @param {number} duracioMin Duracio del servei, en minuts.
* @returns {{inici: string, fi: string}[]} forats lliures, en ordre cronologic.
*/
export function calcularForats(horari, cites = [], duracioMin = 30) {
if (!Number.isInteger(duracioMin) || duracioMin <= 0) {
throw new RangeError(`La duracio ha de ser un enter positiu de minuts; rebut: ${duracioMin}`);
}
const trams = Array.isArray(horari) ? horari : [horari];
const ocupats = normalitzarOcupacio(cites);
const forats = [];
for (const tram of trams) {
const obertura = aMinuts(tram.inici);
const tancament = aMinuts(tram.fi);
if (tancament <= obertura) {
throw new RangeError(`Tram invalid: ${tram.inici}-${tram.fi}. El tancament ha de ser posterior a l obertura.`);
}
let cursor = obertura;
for (const cita of ocupats) {
if (cita.fi <= cursor || cita.inici >= tancament) continue; // fora d aquest tram
trossejar(forats, cursor, Math.min(cita.inici, tancament), duracioMin);
cursor = Math.max(cursor, cita.fi); // una cita que creua el tancament retalla el tram
if (cursor >= tancament) break;
}
if (cursor < tancament) trossejar(forats, cursor, tancament, duracioMin);
}
return forats;
}Tres detalls que solen ser font de bugs reals i que convé que vegis ara, perquè a la 07-02 els convertiràs en proves:
| Cas | Comportament | Per què |
|---|---|---|
| Dues cites solapades (10:00-11:00 i 10:30-11:30) | Es fusionen en una sola ocupació 10:00-11:30 | Si no, l'algorisme «veuria» un forat entre elles |
| Cita que comença abans del tancament i acaba després (13:45-14:30 amb tancament a les 14:00) | Retalla el tram fins al tancament | El cursor avança a 14:30, més gran que el tancament; se surt del bucle |
| Horari partit (09:00-14:00 i 16:00-20:00) | Els forats no creuen mai la pausa | Cada tram es trosseja de manera independent |
3.3 src/servidor.js
Un servidor HTTP sense dependències, amb dues rutes: /salut (que el pipeline farà servir com a smoke test a la 07-03) i /api/forats.
// src/servidor.js
// Servidor HTTP minim de Mini-Reservalia. Sense dependencies externes.
import http from 'node:http';
import { fileURLToPath } from 'node:url';
import { calcularForats } from './disponibilitat.js';
export const VERSIO = process.env.APP_VERSION ?? 'dev';
export const PORT = Number(process.env.PORT ?? 3000);
/** Horari per defecte del negoci de demostracio: mati i tarda. */
export const HORARI_PER_DEFECTE = [
{ inici: '09:00', fi: '14:00' },
{ inici: '16:00', fi: '20:00' },
];
/** Agenda en memoria. A la 07-02 se substitueix per una capa de persistencia. */
export const AGENDA_DEMO = new Map([
['2026-03-02', [{ inici: '10:00', fi: '10:30' }, { inici: '17:00', fi: '18:00' }]],
['2026-03-03', [{ inici: '09:00', fi: '12:00' }]],
]);
const PATRO_DATA = /^\d{4}-\d{2}-\d{2}$/;
function respondreJson(res, codi, cos) {
const text = JSON.stringify(cos);
res.writeHead(codi, {
'content-type': 'application/json; charset=utf-8',
'content-length': Buffer.byteLength(text),
});
res.end(text);
}
/**
* Crea el servidor. S exporta com a funcio perque les proves puguin
* aixecar-lo en un port efimer sense tocar variables d entorn.
*/
export function crearServidor({ agenda = AGENDA_DEMO, horari = HORARI_PER_DEFECTE } = {}) {
return http.createServer((req, res) => {
const url = new URL(req.url, `http://${req.headers.host ?? 'localhost'}`);
if (req.method === 'GET' && url.pathname === '/salut') {
return respondreJson(res, 200, {
estat: 'ok',
versio: VERSIO,
actiuSeg: Math.round(process.uptime()),
});
}
if (req.method === 'GET' && url.pathname === '/api/forats') {
const data = url.searchParams.get('data');
const duracio = Number(url.searchParams.get('duracio') ?? 30);
if (!data || !PATRO_DATA.test(data)) {
return respondreJson(res, 400, { error: 'Parametre "data" obligatori amb format YYYY-MM-DD' });
}
if (!Number.isInteger(duracio) || duracio <= 0) {
return respondreJson(res, 400, { error: 'Parametre "duracio" ha de ser un enter positiu de minuts' });
}
try {
const cites = agenda.get(data) ?? [];
const forats = calcularForats(horari, cites, duracio);
return respondreJson(res, 200, { data, duracio, total: forats.length, forats });
} catch (error) {
return respondreJson(res, 400, { error: error.message });
}
}
return respondreJson(res, 404, { error: 'Ruta no trobada' });
});
}
// Nomes arrenca si s executa directament (node src/servidor.js).
// En importar-lo des d una prova, no s obre cap port.
if (process.argv[1] === fileURLToPath(import.meta.url)) {
crearServidor().listen(PORT, () => {
console.log(`Mini-Reservalia ${VERSIO} escoltant a http://localhost:${PORT}`);
});
}La guarda del final (process.argv[1] === fileURLToPath(import.meta.url)) és l'equivalent ESM del clàssic if __name__ == "__main__". Sense ella, qualsevol prova que importés el mòdul obriria un port i el procés de test no acabaria mai: una penjada de CI clàssica i desconcertant.
3.4 test/disponibilitat.test.js
// test/disponibilitat.test.js
import test from 'node:test';
import assert from 'node:assert/strict';
import { calcularForats, aMinuts, aHora } from '../src/disponibilitat.js';
const MATI = { inici: '09:00', fi: '12:00' };
test('aMinuts converteix hores valides', () => {
assert.equal(aMinuts('00:00'), 0);
assert.equal(aMinuts('09:30'), 570);
assert.equal(aMinuts('23:59'), 1439);
});
test('aMinuts rebutja formats invalids', () => {
assert.throws(() => aMinuts('9:00'), TypeError);
assert.throws(() => aMinuts('25:00'), TypeError);
assert.throws(() => aMinuts(900), TypeError);
});
test('aHora es la inversa d aMinuts', () => {
for (const hora of ['00:00', '07:05', '13:45', '23:59']) {
assert.equal(aHora(aMinuts(hora)), hora);
}
});
test('un dia sense cites es trosseja sencer', () => {
const forats = calcularForats(MATI, [], 60);
assert.deepEqual(forats, [
{ inici: '09:00', fi: '10:00' },
{ inici: '10:00', fi: '11:00' },
{ inici: '11:00', fi: '12:00' },
]);
});
test('una cita parteix el dia en dos blocs', () => {
const forats = calcularForats(MATI, [{ inici: '10:00', fi: '11:00' }], 60);
assert.deepEqual(forats, [
{ inici: '09:00', fi: '10:00' },
{ inici: '11:00', fi: '12:00' },
]);
});
test('la resta sobrant no genera un forat curt', () => {
// De 09:00 a 12:00 amb slots de 50 min n hi caben 3 (fins a 11:30) i sobren 30 min.
const forats = calcularForats(MATI, [], 50);
assert.equal(forats.length, 3);
assert.equal(forats.at(-1).fi, '11:30');
});
test('una duracio invalida es un error de programacio, no un resultat buit', () => {
assert.throws(() => calcularForats(MATI, [], 0), RangeError);
assert.throws(() => calcularForats(MATI, [], 12.5), RangeError);
});Sis proves és poc: a la 07-02 pujarem a les tres capes de la piràmide amb casos límit. Per al primer pipeline n'hi ha prou de tenir un senyal real que es pugui posar en vermell.
3.5 scripts/build.js
// scripts/build.js
// "Build" de Mini-Reservalia: copia src/ a dist/ i segella la versio.
// Es trivial expressament, pero fa el paper del build real: produeix un
// artefacte identificable i falla si alguna cosa no es pot resoldre.
import { cp, mkdir, rm, writeFile } from 'node:fs/promises';
import { execSync } from 'node:child_process';
const DESTI = new URL('../dist/', import.meta.url);
function commitActual() {
if (process.env.GITHUB_SHA) return process.env.GITHUB_SHA;
try {
return execSync('git rev-parse HEAD', { encoding: 'utf8' }).trim();
} catch {
return 'desconegut';
}
}
await rm(DESTI, { recursive: true, force: true });
await mkdir(DESTI, { recursive: true });
await cp(new URL('../src/', import.meta.url), new URL('src/', DESTI), { recursive: true });
const segell = {
nom: 'mini-reservalia',
versio: process.env.npm_package_version ?? '0.0.0',
commit: commitActual(),
construitEl: new Date().toISOString(),
};
await writeFile(new URL('version.json', DESTI), `${JSON.stringify(segell, null, 2)}\n`);
// Comprovacio de fum del build mateix: si el modul no importa, fallem aqui
// i no pas a produccio.
await import('../dist/src/servidor.js');
console.log(`Build OK -> dist/ (commit ${segell.commit.slice(0, 7)})`);Fixa't en l'await import(...) final: és una verificació de l'artefacte dins del build mateix. Si algú deixa un import trencat, el build falla en 200 ms en comptes que ho descobreixi el smoke test tres etapes més tard.
3.6 eslint.config.js
// eslint.config.js (flat config, ESLint 9)
import js from '@eslint/js';
export default [
{ ignores: ['dist/**', 'node_modules/**', 'coverage/**'] },
js.configs.recommended,
{
languageOptions: {
ecmaVersion: 2023,
sourceType: 'module',
globals: {
process: 'readonly',
console: 'readonly',
Buffer: 'readonly',
URL: 'readonly',
fetch: 'readonly',
setTimeout: 'readonly',
},
},
rules: {
'no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'no-console': 'off',
eqeqeq: ['error', 'always'],
},
},
];3.7 .gitignore
node_modules/ i dist/ fora del repositori, evidentment. I .env des del primer dia: la 07-05 explicarà amb detall el que costa un secret commitejat, però la prevenció comença aquí.
3.8 Comprovar que tot funciona en local
npm install # genera package-lock.json
npm run lint # esperat: sense sortida (tot net)
npm test # esperat: 7 proves en verd
npm run build # esperat: "Build OK -> dist/ (commit ...)"
npm start # arrenca a http://localhost:3000Què has de veure en executar npm test:
▶ aMinuts converteix hores valides ✔ aMinuts converteix hores valides (0.9ms) ... ℹ tests 7 ℹ suites 0 ℹ pass 7 ℹ fail 0
I amb el servidor arrencat, en una altra terminal:
curl -s http://localhost:3000/salut
# {"estat":"ok","versio":"dev","actiuSeg":12}
curl -s "http://localhost:3000/api/forats?data=2026-03-02&duracio=60"
# {"data":"2026-03-02","duracio":60,"total":6,"forats":[{"inici":"09:00","fi":"10:00"}, ...]}Si això funciona a la teva màquina, ja tens la part difícil: un projecte que sap verificar-se a si mateix amb una sola ordre. El pipeline només executarà aquestes ordres en una màquina que no és la teva.
- Inicialitzar el repositori i pujar-lo a GitHub
git init -b main
git add .
git commit -m "feat: Mini-Reservalia amb calcul de forats i servidor HTTP"Amb la CLI de GitHub, en una ordre:
Què has de veure: https://github.com/<el-teu-usuari>/mini-reservalia i, en obrir-lo, els fitxers que acabes de crear.
Sense gh: crea el repositori buit des del web (sense README, sense .gitignore, sense llicència, per evitar un historial divergent) i després:
git remote add origin https://github.com/<el-teu-usuari>/mini-reservalia.git
git push -u origin mainComprova que package-lock.json sí que és al repositori. És el requisit de la construcció reproduïble de la 02-03: sense lockfile, npm ci no funciona i cada execució del pipeline podria instal·lar versions diferents.
- Increment 1: el flux de treball mínim que només s'executa
Construirem el ci.yml en quatre passos, veient cadascun executar-se. La temptació és escriure el flux de treball sencer de cop; resisteix-t'hi. Quan un flux de treball de 80 línies falla al primer intent, no saps quina de les 80 línies en té la culpa.
Fitxer .github/workflows/ci.yml — versió 1:
# .github/workflows/ci.yml - INCREMENT 1
# Objectiu: comprovar que el flux de treball es dispara i que el runner funciona.
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
hola:
name: Comprovacio del runner
runs-on: ubuntu-latest
steps:
- name: Descarregar el codi
uses: actions/checkout@v4
- name: Mostrar l entorn
run: |
echo "Runner: $RUNNER_OS"
echo "Branca: $GITHUB_REF_NAME"
echo "Commit: $GITHUB_SHA"
node --version
npm --version
ls -lagit add .github/workflows/ci.yml
git commit -m "ci: flux de treball minim de comprovacio del runner"
git pushQuè has de veure. Entra a la pestanya Actions del teu repositori. Hi ha d'aparèixer una execució anomenada «ci: flux de treball minim de comprovacio del runner» amb el flux de treball CI. Obre-la, entra al job Comprovacio del runner i desplega el pas «Mostrar l entorn». Hi veuràs una cosa així:
Runner: Linux Branca: main Commit: 8f3c1e2... v20.18.0 10.8.2 total 48 drwxr-xr-x 5 runner docker 4096 ... -rw-r--r-- 1 runner docker 612 package.json
Tres coses que acabes d'aprendre empíricament i que cap lectura no substitueix:
- El runner no té el teu codi per defecte. Si treus el pas
actions/checkout@v4, l'ls -lasurt gairebé buit. Prova-ho si vols. - Node ja hi és instal·lat al runner
ubuntu-latest, però amb la versió que GitHub decideixi. Per això l'increment 2 fixa la versió explícitament. - El context viatja en variables d'entorn:
GITHUB_SHA,GITHUB_REF_NAMEi desenes més, tal com vam veure a la 06-06.
Durada esperada: entre 5 i 15 segons. Apunta-la; la farem servir com a referència.
- Increment 2: instal·lar, comprovar i provar
Ara el pipeline fa feina de debò.
# .github/workflows/ci.yml - INCREMENT 2
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
verificar:
name: Verificar
runs-on: ubuntu-latest
steps:
- name: Descarregar el codi
uses: actions/checkout@v4
- name: Preparar Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Instal·lar dependencies
run: npm ci
- name: Analisi estatica
run: npm run lint
- name: Proves
run: npm test
- name: Construir
run: npm run buildQuè has de veure. L'execució triga ara uns 35-60 segons. Tots els passos en verd. Fixa't especialment en el log d'«Instal·lar dependencies»:
I en el de «Proves»:
Per què npm ci i no npm install. Ja ho vam veure a la 02-03, però ara ho pots verificar: npm ci esborra node_modules, instal·la exactament el que diu package-lock.json i falla si el lockfile no concorda amb el package.json. npm install pot actualitzar el lockfile silenciosament, cosa que vol dir que el pipeline provaria un arbre de dependències diferent del que tu vas provar. Comprova-ho tu mateix:
# En local, edita package.json i puja la versio d eslint a "^9.99.0" SENSE tocar el lock
npm ci
# npm error `npm ci` can only install packages when your package.json and
# npm error package-lock.json are in sync.Desfés aquest canvi abans de continuar.
- Increment 3: dos jobs en paral·lel amb
needs
needsUn sol job és una fila índia: si el lint triga 20 segons, les proves esperen 20 segons. Pitjor encara: si el lint falla, no veus el resultat de les proves, així que arregles el lint, tornes a esperar i descobreixes que a més hi ha un test trencat. Dos viatges on n'hi hauria d'haver un.
La 04-01 en deia «retroalimentació en una sola passada». Separem-ho.
# .github/workflows/ci.yml - INCREMENT 3
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
# Si arriben dos pushes seguits a la mateixa branca, cancel·la l anterior:
# ningu no necessita el resultat d un commit que ja ha estat superat.
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
qualitat:
name: Qualitat
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- name: ESLint
run: npm run lint
test:
name: Proves
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- name: Proves unitaries
run: npm test
build:
name: Construir
runs-on: ubuntu-latest
# Nomes es construeix si qualitat I proves han passat: no te sentit
# gastar temps construint una cosa que ja sabem que esta malament.
needs: [qualitat, test]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- name: Construir artefacte
run: npm run build
- name: Publicar dist/ com a artefacte
uses: actions/upload-artifact@v4
with:
name: dist-${{ github.sha }}
path: dist/
retention-days: 7git add .github/workflows/ci.yml
git commit -m "ci: separar qualitat i test en paral·lel, build despres dels dos"
git pushQuè has de veure. A la vista del run, GitHub dibuixa el graf: Qualitat i Proves l'un al costat de l'altre, i Construir a la seva dreta amb les fletxes de dependència. Els dos primers comencen alhora. Al final, a la part inferior de la pàgina del run, apareix la secció Artifacts amb dist-<sha> descarregable.
El que has guanyat:
| Un job (increment 2) | Tres jobs (increment 3) | |
|---|---|---|
| Retroalimentació si falla el lint | Només el lint | Lint i proves alhora |
| Temps de paret | Suma de tot | Màxim de les branques paral·leles + build |
| Cost en minuts | Menor (una màquina) | Major (tres màquines) |
| Artefacte | Es queda al runner | Descarregable durant 7 dies |
És la contrapartida clàssica: paral·lelitzar redueix el temps de paret i augmenta el consum de minuts. En repositoris públics els minuts són gratis, així que la decisió és òbvia; en un de privat amb centenars d'execucions diàries s'ha de pensar. Fixa't, a més, que el npm ci es repeteix tres vegades —una per job, perquè cada job és una màquina neta—. Això ho ataca l'increment 4.
- Increment 4: memòria cau de dependències i mesura de l'efecte
actions/setup-node sap posar en memòria cau el directori de npm. Un canvi de dues línies:
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm' # <-- afegit
cache-dependency-path: package-lock.jsonAplica-ho als tres jobs. El fitxer complet, la versió que tanca aquesta lliçó:
# .github/workflows/ci.yml - VERSIO FINAL DE LA 07-01
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
# Minim privilegi: aquest flux de treball nomes necessita llegir el codi.
# La 07-05 hi aprofundeix; es posa ja per no adquirir el mal habit.
permissions:
contents: read
jobs:
qualitat:
name: Qualitat
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: package-lock.json
- run: npm ci
- name: ESLint
run: npm run lint
test:
name: Proves
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: package-lock.json
- run: npm ci
- name: Proves unitaries
run: npm test
build:
name: Construir
runs-on: ubuntu-latest
needs: [qualitat, test]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: package-lock.json
- run: npm ci
- name: Construir artefacte
run: npm run build
- name: Publicar dist/ com a artefacte
uses: actions/upload-artifact@v4
with:
name: dist-${{ github.sha }}
path: dist/
retention-days: 7Com mesurar l'efecte correctament. La primera execució amb memòria cau és més lenta, perquè l'ha de desar. La mesura honesta compara la segona execució amb memòria cau contra la línia base:
- Apunta el temps del pas
npm cide l'increment 3 (sense memòria cau). Es llegeix al log mateix:Run npm ci ... 8s. - Fes push del canvi. Primera execució: al log de
setup-nodehi veuràsCache not found for input keys: node-cache-Linux-npm-...i al finalCache saved with key: .... - Fes un segon push trivial (canvia el README). Ara el log dirà
Cache restored from key: node-cache-Linux-npm-<hash>.
Taula típica en un projecte d'aquesta mida:
| Mesura | npm ci |
Total del run |
|---|---|---|
| Sense memòria cau | 7-9 s | ~45 s |
| Amb memòria cau (primer cop) | 7-9 s + desat | ~50 s |
| Amb memòria cau (a partir de la segona) | 2-3 s | ~30 s |
Cinc segons per job no semblen gaire. Multiplica-ho per tres jobs, per 40 execucions diàries i per 250 dies laborables: són unes 40 hores de màquina l'any en un projecte sense dependències de producció. A Reservalia, amb un node_modules de centenars de megues, la memòria cau retalla minuts per execució, no segons. La 04-04 ho quantificava; ara ho has vist.
Detall important sobre la clau de memòria cau. cache-dependency-path: package-lock.json fa que la clau inclogui el hash del lockfile. Quan canviïs una dependència, la clau canvia i es torna a descarregar tot: és exactament el que vols. Una memòria cau la clau de la qual no depèn del lockfile és una memòria cau que serveix dependències caducades, i aquest és el «verd fals» de què parlava la 04-04.
- Provocar una fallada expressament i llegir el vermell
Un pipeline que no has vist fallar mai no és un pipeline: és decoració. El trencarem.
Edita src/disponibilitat.js i canvia una sola línia dins de trossejar:
function trossejar(desti, desDe, finsA, duracio) {
- for (let inici = desDe; inici + duracio <= finsA; inici += duracio) {
+ for (let inici = desDe; inici < finsA; inici += duracio) {
desti.push({ inici: aHora(inici), fi: aHora(inici + duracio) });
}
}És un bug realista: ara es genera un darrer forat que sobresurt de l'horari de tancament. Un negoci rebria reserves a les 11:30 quan tanca a les 12:00 i el servei dura 50 minuts.
Què has de veure en local:
✖ la resta sobrant no genera un forat curt (1.2ms) AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: 4 !== 3 ℹ tests 7 ℹ pass 6 ℹ fail 1
Puja-ho igualment, que és el que volem observar:
Ara obre el pull request:
gh pr create --title "Trossejat fins al final del tram" --body "Canvi al bucle de trossejat." --base mainQuè has de veure al PR:
- Una caixa de comprovacions al final de la conversa amb tres entrades:
Qualitat,Proves,Construir. Qualitaten verd (ESLint no detecta bugs lògics: només mira la forma del codi; això és exactament l'advertència de la 02-05).Provesen vermell, amb l'aspa.Construiren gris, marcat com a skipped: no va arribar a executar-se mai perquè el seuneedsno es va complir. Has estalviat un job.- El botó de merge en gris amb el missatge «Some checks were not successful».
Clica Details de la comprovació vermella. GitHub et porta al log del step fallit, ja desplegat i amb la línia de l'error ressaltada. A més, a la pestanya Files changed del PR apareix una anotació vermella sobre la línia del fitxer de test: el runner de Node emet la fallada en un format que Actions reconeix, així que l'error es veu sobre el codi, no només al log.
Ara arregla-ho dins del mateix PR:
git checkout src/disponibilitat.js # revertir el canvi
npm test # verd en local
git commit -am "revert: restaurar el trossejat correcte"
git pushQuè has de veure: el PR reexecuta les comprovacions automàticament (pel trigger pull_request, que es dispara també en synchronize), les tres passen a verd i el botó de merge s'habilita. Fes el merge:
Acabes de recórrer el cicle complet: branca → PR → vermell → diagnòstic → arranjament → verd → merge. És el cicle que un equip repeteix vint vegades al dia.
- El flux real: branca, pull request, comprovacions, merge
Formalitzem el que acabes de fer, perquè l'ordre importa i hi ha un pas que gairebé tothom es salta.
flowchart LR
A["git checkout -b branca"] --> B["Canvi petit<br/>+ prova"]
B --> C["npm test en LOCAL"]
C -->|vermell| B
C -->|verd| D["push + pull request"]
D --> E["CI executa<br/>qualitat / test / build"]
E -->|vermell| F["Llegir log,<br/>reproduir en local"]
F --> B
E -->|verd| G["Revisio humana"]
G --> H["Merge a main"]
H --> I["CI a main"]
El pas que la gent es salta és npm test en local abans del push. Fer servir la CI com a intèrpret d'ordres —empènyer per veure si passa— converteix un cicle de 3 segons en un de 3 minuts i omple l'historial de commits d'«arreglar CI», «arreglar CI de debò», «ara sí». Regla pràctica: si l'ordre que executarà el pipeline no la pots executar tu, el pipeline està mal dissenyat. Per això els quatre scripts de package.json són el contracte.
- Protegir
main i comprovar que bloqueja
main i comprovar que bloquejaFins ara les comprovacions són informatives: res no t'impedeix fer merge en vermell, ni empènyer directament a main. Tancarem aquesta porta, que és el «stop the line» de la 02-01 fet configuració.
Des del web: Settings → Rules → Rulesets → New ruleset → New branch ruleset.
- Name:
protegir-main - Enforcement status:
Active - Target branches: Add target → Include default branch
- Marca Require a pull request before merging (amb
Required approvals: 0si treballes sol; en un equip, 1). - Marca Require status checks to pass, cerca i afegeix
Qualitat,ProvesiConstruir. Marca també Require branches to be up to date before merging. - Marca Block force pushes.
Amb gh, en una sola ordre (crea el fitxer i l'aplica):
cat > /tmp/ruleset.json <<'JSON'
{
"name": "protegir-main",
"target": "branch",
"enforcement": "active",
"conditions": { "ref_name": { "include": ["~DEFAULT_BRANCH"], "exclude": [] } },
"rules": [
{ "type": "deletion" },
{ "type": "non_fast_forward" },
{ "type": "pull_request",
"parameters": {
"required_approving_review_count": 0,
"dismiss_stale_reviews_on_push": true,
"require_code_owner_review": false,
"require_last_push_approval": false,
"required_review_thread_resolution": false
} },
{ "type": "required_status_checks",
"parameters": {
"strict_required_status_checks_policy": true,
"required_status_checks": [
{ "context": "Qualitat" },
{ "context": "Proves" },
{ "context": "Construir" }
]
} }
]
}
JSON
gh api --method POST "repos/{owner}/{repo}/rulesets" --input /tmp/ruleset.jsonLa comprovació que falla quan ha de fallar. Una protecció que no has intentat violar no saps si existeix. Dues proves:
Prova A — push directe a main:
Què has de veure:
remote: error: GH013: Repository rule violations found for refs/heads/main. remote: - Changes must be made through a pull request. ! [remote rejected] main -> main (push declined due to repository rule violations)
Desfés el commit local: git reset --hard origin/main.
Prova B — merge d'un PR en vermell:
Trenca un altre cop el test (canvia assert.equal(forats.length, 3) per 4 a test/disponibilitat.test.js), fes el commit, empeny, obre el PR i intenta fer el merge:
Què has de veure:
o, des del web, el botó de merge deshabilitat amb «Required statuses must pass before merging». Tanca el PR i esborra la branca:
Aquesta comprovació negativa —que el sistema impedeix el que ha d'impedir— és tan important com la positiva, i és la que gairebé ningú no fa. Una comprovació obligatòria mal escrita (per exemple, amb el nom test en comptes de Proves) queda eternament «pendent» i ho bloqueja tot; o, pitjor, si la configures sobre un job que es pot saltar, deixa passar qualsevol cosa.
Detall que costa mitja hora a tothom: el nom de la comprovació obligatòria és el
name:del job, no la clau del job al YAML. El nostre jobtest:es diuProvesperquè téname: Proves. Si afegeixes la comprovació amb el nom equivocat, GitHub l'espera per sempre i el PR no és mai mergeable. Si et passa, la llista de noms exactes és a la caixa de comprovacions de qualsevol PR recent.
- El distintiu d'estat al README
Crea README.md:
# Mini-Reservalia [](https://github.com/<el-teu-usuari>/mini-reservalia/actions/workflows/ci.yml) Versio reduida de Reservalia per al modul practic del curs de CI/CD. Calcula els forats lliures d un dia a partir d un horari d obertura i les cites ja reservades. ## Us
npm ci npm test npm start # http://localhost:3000
## Endpoints | Metode | Ruta | Descripcio | |---|---|---| | GET | `/salut` | Estat del servei i versio desplegada | | GET | `/api/forats?data=YYYY-MM-DD&duracio=30` | Forats lliures del dia | ## Pipeline | Etapa | Que fa | Trenca el build | |---|---|---| | Qualitat | ESLint sobre tot el codi | Si | | Proves | `node --test` | Si | | Construir | `dist/` segellat amb el commit | Si |
Puja'l per PR (ja no pots empènyer a main, precisament):
git checkout -b docs-readme
git add README.md
git commit -m "docs: README amb distintiu de CI"
git push -u origin docs-readme
gh pr create --fill
# ...esperar que les comprovacions passin...
gh pr merge --squash --delete-branchQuè has de veure: a la portada del repositori, un distintiu verd que diu CI: passing. El ?branch=main importa: sense ell, el distintiu reflecteix l'última execució de qualsevol branca, així que un experiment trencat en una branca personal posaria el distintiu en vermell i deixaria de significar res.
- Verificació final
Repassa aquesta llista. Tot s'ha de complir abans de passar a la 07-02.
| # | Comprovació | Com es verifica | Resultat esperat |
|---|---|---|---|
| 1 | El projecte funciona en local | npm ci && npm test && npm run build |
7 proves en verd, dist/ creat |
| 2 | El servidor respon | npm start i curl localhost:3000/salut |
{"estat":"ok",...} |
| 3 | El flux de treball es dispara en push | Pestanya Actions després d'un push a una branca | Un run nou |
| 4 | El flux de treball es dispara en PR | Obrir un PR | Tres comprovacions a la conversa |
| 5 | Els jobs corren en paral·lel | Graf del run | Qualitat i Proves a la mateixa alçada |
| 6 | La memòria cau funciona | Log de setup-node al 2n run |
Cache restored from key: ... |
| 7 | El pipeline es posa en vermell | Trencar un test i empènyer | Comprovació Proves en vermell, Construir omès |
| 8 | main rebutja el push directe |
git push des de main |
GH013: Repository rule violations |
| 9 | Un PR en vermell no es pot fusionar | Botó de merge | Deshabilitat |
| 10 | L'artefacte està disponible | Secció Artifacts del run | dist-<sha> descarregable |
| 11 | El distintiu és verd | Portada del repositori | CI: passing |
Els tres en negreta són els que de debò demostren que el pipeline serveix per a alguna cosa. Un pipeline que només has vist en verd és una hipòtesi sense comprovar.
Errors Comuns i Consells
Símptoma: el flux de treball no apareix a la pestanya Actions després del push.
Causa: la ruta del fitxer és incorrecta. Ha de ser exactament .github/workflows/ci.yml, a l'arrel del repositori. github/workflows/, .github/workflow/ o .github/actions/ no valen.
Arranjament: git ls-files .github ha de mostrar .github/workflows/ci.yml. Si no, mou el fitxer i torna a empènyer.
Símptoma: Error: Dependencies lock file is not found in /home/runner/work/....
Causa: has posat cache: 'npm' a setup-node però package-lock.json no està commitejat (probablement per un .gitignore massa agressiu).
Arranjament: comprova que package-lock.json no és al .gitignore, executa npm install per generar-lo i commiteja'l. El lockfile va sempre al repositori.
Símptoma: npm error code EUSAGE — npm ci can only install packages when your package.json and package-lock.json are in sync.
Causa: vas editar package.json a mà sense regenerar el lockfile.
Arranjament: en local, npm install (que sí que actualitza el lock) i commiteja els dos fitxers junts. Un package.json i un lock que no concorden són la primera causa de «a la meva màquina va».
Símptoma: el job de proves es queda penjat fins al timeout de 6 hores.
Causa clàssica a Node: algun mòdul importat per les proves obre un port o un temporitzador i el procés no acaba. Per això src/servidor.js té la guarda process.argv[1] === fileURLToPath(import.meta.url).
Arranjament: afegeix timeout-minutes: 10 a tots els jobs —hauria de ser un reflex— i investiga en local amb node --test --test-reporter=spec test/. Si el procés no acaba, el culpable és un recurs obert.
Símptoma: la comprovació obligatòria queda en Expected — Waiting for status to be reported per sempre.
Causa: el nom de la comprovació configurada al ruleset no coincideix amb el name: del job, o el job no s'executa en aquest context (per exemple, tens un filtre paths que se'l salta).
Arranjament: copia els noms exactes de la caixa de comprovacions d'un PR recent. I compte amb els filtres paths: un job requerit que se salta per filtres bloqueja el PR indefinidament; la solució habitual és un job «verd permanent» que sempre s'executa i del qual depenen els altres.
Símptoma: dues execucions de la mateixa branca, la primera cancel·lada amb «Canceling since a higher priority waiting request exists».
Causa: no és un error. És el teu bloc concurrency amb cancel-in-progress: true fent la seva feina.
Consell: no posis això al flux de treball de desplegament amb cancel-in-progress: true; cancel·lar un desplegament a mitges pot deixar el sistema en un estat inconsistent. A la CI està bé; al CD, la 07-03 farà servir cancel-in-progress: false.
Símptoma: ESLint falla amb Parsing error: 'import' and 'export' may appear only with 'sourceType: module'.
Causa: falta "type": "module" al package.json o sourceType: 'module' a eslint.config.js.
Arranjament: hi han de ser tots dos. En aquest projecte, els dos.
Consell d'higiene: commits petits i freqüents. La 02-07 ho justificava amb la integració contínua; aquí ho notaràs de manera immediata: quan el pipeline es posa en vermell després d'un commit de 400 línies, el diagnòstic és arqueologia; després d'un de 20, és evident.
Exercicis
Exercici 1: un job de comprovació del format amb --check
Afegeix al pipeline una comprovació de format amb Prettier que falli si el codi no està formatat, i un script format que ho arregli. El job s'ha de dir Format i córrer en paral·lel amb Qualitat i Proves. Comprova que falla desformatant un fitxer expressament.
Exercici 2: no executar el pipeline quan només canvia documentació
Un canvi al README.md no necessita executar tres jobs. Configura el flux de treball perquè se salti els canvis que només toquen Markdown, sense trencar la protecció de branca. Pensa-t'ho bé: si les comprovacions requerides no s'executen, el PR queda bloquejat per sempre. Explica en un comentari del YAML per què la teva solució no cau en aquest parany.
Exercici 3: un resum llegible del run
Fes que el job Construir escrigui a $GITHUB_STEP_SUMMARY una taula amb el commit, la versió, la mida de dist/ i el nombre de proves executades, de manera que es vegi a la portada del run sense obrir cap log.
Solucions
Solució 1.
.prettierrc.json:
.prettierignore:
Al package.json, dos scripts nous:
"scripts": {
"format": "prettier --write .",
"format:check": "prettier --check .",
"lint": "eslint .",
"test": "node --test test/",
"build": "node scripts/build.js",
"start": "node src/servidor.js"
}Executa npm run format un cop per normalitzar tot el projecte i commiteja el resultat en un commit propi (un «commit de format» aïllat, perquè no contamini els diffs futurs). Després, el job:
format:
name: Format
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: package-lock.json
- run: npm ci
- name: Comprovar format
run: npm run format:checkI afegeix format al needs del job build: needs: [qualitat, format, test].
Comprovació que falla: posa espais estranys a src/servidor.js i empeny. Hi veuràs:
Checking formatting... [warn] src/servidor.js [warn] Code style issues found in the above file. Run Prettier with --write to fix. Error: Process completed with exit code 1.
La diferència entre --write i --check és exactament la diferència entre una eina de desenvolupament i una porta de qualitat: el pipeline no modifica mai el codi, només constata. Un pipeline que autoformata i commiteja genera commits fantasma, dispara execucions noves i pot entrar en bucle.
Solució 2.
El parany: si afegeixes paths-ignore: ['**.md'] al trigger, en un PR que només toca Markdown els jobs no s'executen, les comprovacions requerides no reporten mai i el PR queda bloquejat en Expected per sempre.
Hi ha dues solucions correctes. La senzilla i robusta:
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
canvis:
name: Detectar canvis
runs-on: ubuntu-latest
outputs:
codi: ${{ steps.filtre.outputs.codi }}
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- id: filtre
# Comparem amb la base del PR (o amb el commit anterior en push).
run: |
BASE="${{ github.event.pull_request.base.sha || github.event.before }}"
if git diff --name-only "$BASE" HEAD | grep -qvE '\.(md|txt)$|^docs/'; then
echo "codi=true" >> "$GITHUB_OUTPUT"
else
echo "codi=false" >> "$GITHUB_OUTPUT"
echo "Nomes documentacio: s ometen les verificacions pesades." >> "$GITHUB_STEP_SUMMARY"
fi
test:
name: Proves # <-- la comprovacio requerida SEMPRE existeix
needs: canvis
runs-on: ubuntu-latest
steps:
- name: Omes per ser nomes documentacio
if: needs.canvis.outputs.codi != 'true'
run: echo "Sense canvis de codi; res a provar."
- uses: actions/checkout@v4
if: needs.canvis.outputs.codi == 'true'
- uses: actions/setup-node@v4
if: needs.canvis.outputs.codi == 'true'
with: { node-version: '20', cache: 'npm' }
- run: npm ci
if: needs.canvis.outputs.codi == 'true'
- run: npm test
if: needs.canvis.outputs.codi == 'true'La clau, i el que cal escriure al comentari del YAML: el job requerit sempre s'executa i sempre reporta verd; el que se salta són els seus passos cars. Un job que existeix i no fa res costa ~5 segons i manté la protecció de branca funcionant. Un job que no existeix bloqueja el PR per sempre.
L'alternativa avançada —i el que fa Reservalia— és la 04-04: paths al trigger més un job agregador ci-ok amb if: always() que avalua el resultat dels altres i és l'única comprovació requerida.
Solució 3.
Afegeix al job build, després de construir:
- name: Resum del run
run: |
MIDA=$(du -sh dist | cut -f1)
VER=$(node -p "require('./dist/version.json').versio")
COM=$(node -p "require('./dist/version.json').commit.slice(0,7)")
N_TESTS=$(npm test 2>&1 | grep -oP '(?<=^# pass )\d+' || echo "?")
{
echo "## Artefacte construit"
echo ""
echo "| Camp | Valor |"
echo "|---|---|"
echo "| Commit | \`$COM\` |"
echo "| Versio | $VER |"
echo "| Mida de dist/ | $MIDA |"
echo "| Proves en verd | $N_TESTS |"
echo "| Branca | \`${{ github.ref_name }}\` |"
echo "| Autor | @${{ github.actor }} |"
} >> "$GITHUB_STEP_SUMMARY"Una versió millor evita reexecutar les proves: fes que el job test escrigui el nombre en un output i consumeix-lo aquí amb needs.test.outputs.proves. Reexecutar la suite només per comptar-la és el tipus de malbaratament que la 04-04 anomenava «feina duplicada per comoditat».
$GITHUB_STEP_SUMMARY és Markdown que es renderitza a la portada del run. És l'eina més infravalorada de GitHub Actions: converteix un pipeline en una cosa que un tech lead pot llegir en deu segons sense obrir cap log. La farem servir molt a la 07-02 (cobertura) i a la 07-04 (mètriques DORA).
Repte opcional
Fes que el pipeline s'executi també contra Node 22 sense duplicar el job, fent servir una matriu de dues entrades. És un tastet de la 07-02, així que si te'n surts, ja tens mig exercici fet. Pista: strategy.matrix.node: [20, 22] i node-version: ${{ matrix.node }}. I un advertiment: en fer servir matriu, els noms de les comprovacions canvien a Proves (20) i Proves (22), així que hauràs d'actualitzar el ruleset de protecció de branca o el teu PR quedarà bloquejat esperant una comprovació anomenada Proves que ja no existeix. Aquest descobriment val més que l'exercici.
Què has construït
Un repositori a GitHub amb:
- Mini-Reservalia: un projecte Node.js amb domini real (càlcul de forats), servidor HTTP amb
/saluti/api/forats, proves, lint i build, tot sense dependències de producció. - Un
ci.ymlde tres jobs amb paral·lelització,concurrency, memòria cau de dependències i publicació d'artefacte, construït en quatre increments que has vist executar-se. - Una porta real:
mainprotegida, amb tres comprovacions obligatòries, verificada pel costat positiu i pel negatiu. - L'experiència de veure'l en vermell, diagnosticar-lo des del log i l'anotació, i arreglar-lo dins del mateix PR.
Conclusió
El que acabes de muntar és, en miniatura, l'etapa preparar → qualitat/test → build del ci.yml de Reservalia que vas llegir a la 02-02. La diferència és que ara saps per què hi ha cada línia, perquè has vist què passa quan falta: sense checkout el runner és buit, sense setup-node la versió és la que toqui, sense lockfile la instal·lació no és reproduïble, sense needs es construeix codi que ja se sap trencat, sense memòria cau es reinstal·la tot tres vegades, i sense protecció de branca res de tot això no obliga ningú.
Aquest pipeline, però, té una debilitat que no es veu en verd: el seu senyal és feble. Set proves unitàries sobre una funció pura no diuen res sobre si l'endpoint /api/forats respon bé, ni sobre si el servidor arrenca, ni sobre quin percentatge del codi s'està exercitant realment. Un pipeline verd amb proves insuficients dona exactament la mateixa sensació de seguretat que un de bo, i aquesta és la seva peculiar perillositat —el «verd fals» de la 04-04—.
A la 07-02 ataquem justament això: afegirem a Mini-Reservalia una capa de persistència, escriurem les tres capes de la piràmide de proves al seu damunt (unitàries amb casos límit de debò, integració contra la persistència real, i un end-to-end contra el servidor arrencat), mesurarem la cobertura i la publicarem al resum del run, hi posarem un llindar que trenqui el build, executarem la suite en una matriu de versions de Node i en dos shards paral·lels, i —el més instructiu— fabricarem una prova flaky expressament per veure-la fallar de manera intermitent i aplicar-hi la política de quarantena. No tanquis el repositori: continuem just aquí.
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
