Les tres lliçons anteriors van descriure tres fluxos de treball diferents, i totes tres van acabar assenyalant la mateixa dependència: comprovacions automàtiques en les quals es pugui confiar. Git Flow les necessita menys perquè té una fase de QA manual a la branca de versió. GitHub Flow les exigeix per poder afirmar que main és desplegable. Trunk Based Development les exigeix ràpides i perfectes, perquè el tronc rep canvis cada hora.

Aquesta lliçó construeix aquella peça. I tanca dues promeses pendents del curs: la de la lliçó 06-01, on vam veure que els hooks de client no són un control de seguretat perquè qualsevol els esquiva amb --no-verify, i vam deixar anunciada la família de hooks que s'executen al servidor; i la de la 07-04, on va aparèixer un protected branch hook declined sense explicar del tot què era.

També convé delimitar l'abast des del principi. Aquí parlem d'integració contínua: comprovar automàticament que allò que s'integra funciona. No d'entrega ni de desplegament continus —com aquell codi arriba a producció, amb quins entorns, quina estratègia de promoció i com es reverteix—, que són el contingut de la lliçó 10-05: Git a DevOps. La frontera és just on el codi es declara apte per integrar.

Contingut

  1. Què és la integració contínua i què no és
  2. Com s'enganxa a Git: els disparadors
  3. Quin commit es comprova realment en una pull request
  4. Una canonada d'exemple, explicada línia a línia
  5. Comprovacions d'estat i branques protegides
  6. Els hooks de servidor: pre-receive, update i post-receive
  7. Client, servidor i CI: qui comprova què
  8. Mantenir la CI ràpida
  9. Merge queue: integrar sense curses
  10. Què queda fora: el desplegament

  1. Què és la integració contínua i què no és

Comencem pel malentès, perquè és gairebé universal.

«Nosaltres ja fem integració contínua: tenim un servidor que executa les proves.»

Això no és integració contínua. És automatització de proves, que està molt bé i és un requisit, però no és el mateix.

La definició original, de Martin Fowler i del moviment de la programació extrema, és aquesta:

Integració contínua és la pràctica per la qual els membres d'un equip integren la seva feina amb freqüència, almenys diàriament, i cada integració es verifica mitjançant una compilació i una bateria de proves automàtiques per detectar els errors d'integració com abans millor.

La paraula que carrega amb tot el significat és integren. Un equip on cada persona treballa tres setmanes a la seva branca, amb un servidor de CI que prova aquelles branques aïllades religiosament, no està fent integració contínua. Està provant de manera contínua tres versions divergents del projecte que encara no s'han trobat. El dia que es trobin, apareixeran tots els problemes de cop: és exactament el que la pràctica pretenia evitar.

Per això aquesta lliçó ve després de la 07-05 i no abans. La política de ramificació és la part difícil; el servidor de comprovacions és la part fàcil.

És integració contínua No ho és
Tot l'equip integra a la línia principal diàriament Tenir un servidor de CI
Cada integració es verifica automàticament Executar les proves abans d'una publicació mensual
La línia principal està sempre sana Que les proves passin a la branca de cadascú
Una fallada s'arregla immediatament, amb prioritat Acumular un tauler de comprovacions en vermell
La resposta arriba en minuts Un cicle de comprovació de dues hores

I les quatre regles pràctiques que la sostenen:

  1. Integrar sovint, almenys una vegada al dia per persona (lliçó 07-05).
  2. Cada integració dispara la verificació, sense excepcions ni exclusions manuals.
  3. Arreglar la línia principal és la màxima prioritat. Si main està en vermell, es para tot. Si no s'arregla en minuts, es reverteix (lliçó 05-06).
  4. La resposta ha de ser ràpida, o el bucle deixa de tancar-se (apartat 8).

  1. Com s'enganxa a Git: els disparadors

El sistema de CI no endevina quan ha de treballar: reacciona a esdeveniments de Git. Aquells esdeveniments són els disparadors (triggers), i són la superfície de contacte entre les dues coses.

Els tres fonamentals:

Disparador per push

El més bàsic. Cada vegada que algú actualitza una referència al servidor, es llancen les comprovacions sobre aquell commit.

# Disparar a cada enviament a qualsevol branca
on:
  push:
    branches:
      - '**'

I les variants habituals, que en un projecte real es combinen:

on:
  push:
    # Nomes a la linia principal i a les branques de treball
    branches:
      - main
      - 'funcionalitat/**'
      - 'correccio/**'
    # Ignorar canvis que no poden trencar res
    paths-ignore:
      - '**.md'
      - 'docs/**'

El paths-ignore mereix un comentari: filtrar per rutes estalvia temps de màquina, però fes-lo servir amb compte. Si el filtre exclou un fitxer que sí que afecta el resultat (una configuració, un fitxer de dades), tindràs integracions sense comprovar i no ho sabràs. I si aquella comprovació és obligatòria a la branca protegida, la PR es pot quedar esperant eternament una comprovació que no es llançarà mai. És un problema clàssic i desconcertant.

Disparador per pull request

Es llança en obrir una PR i cada vegada que se li afegeixen commits. És el disparador que sosté la revisió: qui revisa vol veure el resultat abans d'aprovar.

on:
  pull_request:
    branches: [main]
    types: [opened, synchronize, reopened, ready_for_review]

Els tipus d'esdeveniment importen:

Esdeveniment Quan passa
opened S'obre la PR
synchronize Arriben commits nous a la branca: el cas més freqüent
reopened Es reobre una PR tancada
ready_for_review Deixa de ser esborrany (lliçó 07-01)

Disparador per etiqueta

Les etiquetes de la lliçó 05-05 són el disparador natural de tot el que té a veure amb publicar una versió:

on:
  push:
    tags:
      - 'v[0-9]+.[0-9]+.[0-9]+'      # v2.4.0, pero no v2.4.0-beta

Recorda de la 05-05 que les etiquetes no viatgen soles: git push origin main no les envia. Si la teva publicació depèn d'una etiqueta, l'ordre és git push origin main --follow-tags o git push origin v2.4.0. És una causa molt comuna d'«he etiquetat i no ha passat res».

Altres disparadors

Disparador Ús típic
Programat (cron) Bateria completa nocturna, auditoria de dependències
Manual Rellançar una comprovació, executar alguna cosa sota demanda
Per una altra canonada Encadenar fases
Webhook extern Reaccionar a esdeveniments d'altres sistemes

Un patró molt estès i molt recomanable: bateria ràpida a cada enviament i a cada PR, bateria completa a la nit. Ho veurem a l'apartat 8.

  1. Quin commit es comprova realment en una pull request

Aquest apartat explica una de les sorpreses més freqüents de tot el mòdul.

L'Ana obre una PR des de funcionalitat/filtre-per-etiqueta cap a main. La CI s'executa i surt en verd. L'Ana integra… i main es posa en vermell. Com pot ser, si acabava de passar?

I de vegades passa el contrari: l'Ana no ha tocat res, però en tornar de dinar la CI de la seva PR ha passat de verd a vermell sense que ella hagi fet cap commit.

L'explicació és la mateixa en tots dos casos, i és aquesta:

En una pull request, la CI normalment no comprova la teva branca. Comprova el resultat de fusionar la teva branca amb la branca de destinació.

És a dir: no es prova D2 (la punta de la teva branca), es prova un commit de fusió efímer entre D2 i la punta actual de main.

gitGraph
   commit id: "C1"
   commit id: "C3 (base de la branca)"
   branch funcionalitat/filtre
   checkout funcionalitat/filtre
   commit id: "D1"
   commit id: "D2 (la teva punta)"
   checkout main
   commit id: "C4"
   commit id: "C5 (punta actual)"
   merge funcionalitat/filtre id: "M (el que prova la CI)"

A GitHub, aquell commit és exactament la referència refs/pull/<n>/merge que vam veure a la lliçó 07-01: el servidor la calcula i la manté actualitzada cada vegada que canvia qualsevol dels dos costats.

# Endur-te exactament el que la CI esta provant
git fetch origin pull/42/merge:el-que-prova-la-ci
git switch el-que-prova-la-ci

Aquella ordre és el millor diagnòstic quan la CI falla i tu no reprodueixes la fallada en local: probablement estàs executant pull/42/head mentre la CI executa pull/42/merge.

Per què es fa així, i és el correcte: el que importa no és si la teva branca funciona aïllada, sinó si funcionarà un cop integrada. Comprovar la fusió detecta els conflictes semàntics dels quals vam parlar a la lliçó 07-05: la Carla va reanomenar una funció a main, tu vas afegir una crida amb el nom antic a la teva branca, no hi ha conflicte textual, i el resultat està trencat. Provar només la teva branca no ho veuria mai.

Les tres conseqüències pràctiques, que expliquen els dos misteris del principi:

  1. El resultat de la teva PR pot canviar sense que tu facis res, perquè main ha avançat. No és una fallada del sistema: és informació valuosa que acabes de rebre de franc.
  2. Verd a la PR no garanteix verd a main en integrar si main ha avançat entre l'última comprovació i la fusió. És la cursa que resol la merge queue de l'apartat 9.
  3. Si hi ha conflictes, el commit de fusió no es pot calcular i la CI no s'executa gens ni mica. Primer es resolen els conflictes (lliçó 03-05), després torna a haver-hi senyal.

I un matís important per a la lliçó 07-01: a GitLab això s'anomena merged results pipelines i és configurable; algunes plataformes proven per defecte només la punta de la branca. Esbrina què fa la teva, perquè canvia completament com interpretes un vermell.

Restricció de seguretat, reprenent la 07-01. Quan la PR ve d'un fork, el codi l'ha escrit algú de fora. Si aquella execució tingués accés a les credencials del projecte, qualsevol les podria robar obrint una PR que imprimís les variables d'entorn. Per això les plataformes executen les PRs de forks en mode restringit, sense secrets i amb permisos de només lectura, i sovint exigeixen que un membre de l'equip autoritzi cada execució. És incòmode i és imprescindible.

  1. Una canonada d'exemple, explicada línia a línia

Construirem la canonada de gestor-tasques. El fitxer fa servir la sintaxi i els noms d'esdeveniments més habituals, però els conceptes són idèntics a qualsevol plataforma: el que canvia són les paraules, no les idees.

# .ci/canonada.yml — Integracio continua de gestor-tasques
name: Integracio continua

# 1. DISPARADORS: quan s'executa aixo
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

# 2. CONCURRENCIA: cancellar execucions obsoletes de la mateixa branca
concurrency:
  group: ci-${{ github.ref }}
  cancel-in-progress: true

jobs:
  # 3. PRIMERA TASCA: comprovacions rapides
  comprovacions-rapides:
    runs-on: ubuntu-latest
    timeout-minutes: 10
    steps:
      # 3.1 Portar el codi
      - name: Obtenir el codi
        uses: actions/checkout@v4
        with:
          fetch-depth: 0        # historial complet: cal per al pas 3.5

      # 3.2 Preparar l'entorn
      - name: Preparar Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '22'
          cache: 'npm'          # cachejar ~/.npm entre execucions

      # 3.3 Installar dependencies de manera reproduible
      - name: Installar dependencies
        run: npm ci

      # 3.4 Analisi estatica
      - name: Executar el linter
        run: npm run lint

      # 3.5 Comprovar el format dels missatges de confirmacio
      - name: Validar els missatges de confirmacio
        if: github.event_name == 'pull_request'
        run: |
          BASE="${{ github.event.pull_request.base.sha }}"
          git log --format=%s "$BASE..HEAD" | while read -r assumpte; do
            if ! echo "$assumpte" | grep -qE '^(GT-[0-9]+|Fusiona) '; then
              echo "Missatge sense referencia de tiquet: $assumpte"
              exit 1
            fi
          done

  # 4. SEGONA TASCA: proves en diversos sistemes, en parallel
  proves:
    runs-on: ${{ matrix.so }}
    timeout-minutes: 20
    strategy:
      fail-fast: false          # que un sistema no cancelli els altres
      matrix:
        so: [ubuntu-latest, macos-latest, windows-latest]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '22'
          cache: 'npm'
      - run: npm ci
      - name: Executar la bateria de proves
        run: npm test -- --coverage

      # 4.1 Desar resultats encara que les proves fallin
      - name: Publicar l'informe de cobertura
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: cobertura-${{ matrix.so }}
          path: cobertura/

  # 5. TERCERA TASCA: comprovar els submoduls
  submoduls:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          submodules: recursive    # portar components-ui
      - name: Comprovar que el submodul apunta a un commit publicat
        run: |
          cd components-ui
          git fetch origin main
          git merge-base --is-ancestor HEAD origin/main \
            || { echo "components-ui apunta a un commit que no es a main"; exit 1; }

  # 6. PORTA FINAL: una unica tasca que resumeix el resultat
  tot-correcte:
    runs-on: ubuntu-latest
    needs: [comprovacions-rapides, proves, submoduls]
    if: always()
    steps:
      - name: Verificar que no ha fallat res
        run: |
          [ "${{ contains(needs.*.result, 'failure') }}" = "false" ] || exit 1
          echo "Totes les comprovacions han passat."

Ara, per què cada peça és on és.

1. Disparadors. S'executa a cada enviament a main i a cada PR contra main. No a qualsevol branca: la PR ja cobreix les branques de treball, i executar les dues coses duplica la despesa sense aportar senyal.

2. Concurrència. Si l'Ana envia tres commits en cinc minuts, sense això es llancen tres execucions completes i només importa l'última. cancel-in-progress cancel·la les obsoletes. En un equip actiu això sol ja redueix la despesa de màquina a la meitat. Compte: no ho apliquis a main si la teva canonada té efectes (publicar un artefacte, per exemple); cancel·lar a mitges pot deixar coses a mig fer.

3.1 fetch-depth: 0. Per defecte, els sistemes de CI fan un clon superficial (git clone --depth 1, lliçó 02-02): porten només l'últim commit. És molt més ràpid, però trenca tot el que necessiti historial: git log entre dues referències, git describe --tags, git merge-base, git blame. Com que el pas 3.5 recorre els commits de la PR, aquí cal l'historial complet. Aquesta és probablement la fallada de configuració més comuna de tota la CI: una ordre de Git que funciona en local i a la CI diu fatal: bad revision.

3.3 npm ci i no npm install. ci instal·la exactament el que diu el fitxer de bloqueig i falla si no coincideix amb el manifest. install pot resoldre versions noves i fer que l'execució d'avui difereixi de la d'ahir sense que ningú hagi canviat res. Reproduïbilitat: la mateixa entrada ha de donar el mateix resultat.

3.5 Validació dels missatges de commit. Aquí hi ha la connexió amb la lliçó 06-01: el hook commit-msg ja valida el format GT-NNN al portàtil, però se salta amb --no-verify. Aquesta comprovació ho verifica al servidor, on ningú no la pot esquivar. Fixa't en el rang: $BASE..HEAD, és a dir, només els commits que la PR aporta (lliçó 07-02). Les regles concretes del format són el tema de la lliçó 08-01; aquí només muntem el mecanisme que les fa complir.

4. Matriu. L'Ana fa servir Ubuntu, el Bruno macOS i la Carla Windows 11. Comprovar els tres sistemes en paral·lel detecta el clàssic problema de rutes amb \ davant de / o de majúscules als noms de fitxer abans que arribi a main. fail-fast: false és important: sense ell, el primer sistema que falli cancel·la els altres i perds informació útil.

4.1 if: always(). Sense això, l'informe de cobertura no es desa quan les proves fallen, que és justament quan més el necessites per diagnosticar.

5. Submòduls. Reprèn la lliçó 06-05. gestor-tasques fa servir components-ui com a submòdul, i l'error clàssic és confirmar un punter a un commit que només existeix al portàtil de qui el va fer. Qualsevol altra persona clonarà i no podrà inicialitzar el submòdul. git merge-base --is-ancestor comprova que el commit apuntat sigui abastable des de la branca principal del submòdul. És una comprovació de tres línies que estalvia tardes senceres.

6. La porta final. Una única tasca que depèn de totes les altres. La seva utilitat és pràctica: a la branca protegida s'exigeix una sola comprovació obligatòria, tot-correcte, en lloc de mantenir una llista de cinc noms que cal actualitzar cada vegada que s'afegeix una tasca. Amb la matriu és encara més valuós, perquè els noms generats (proves (ubuntu-latest), etc.) canvien en tocar la matriu i les regles de protecció es queden esperant comprovacions que ja no existeixen.

  1. Comprovacions d'estat i branques protegides

Ja tenim les comprovacions. Ara cal convertir-les en un requisit, i aquesta és la part que respon definitivament la pregunta que va deixar oberta la lliçó 06-01.

La comprovació d'estat

Quan la canonada acaba, publica el seu resultat associat al commit, no a la branca ni a la PR. És una dada del tipus: «el commit a7c2e91 ha superat la comprovació tot-correcte». Això és una comprovació d'estat (status check).

Consultable des de la plataforma mateixa, i visible a la interfície com el tic verd o la creu vermella al costat del commit.

La branca protegida

Per si sola, una comprovació d'estat és informativa. El que la converteix en obligació és la configuració de la branca protegida, que ja vam presentar a la lliçó 07-04:

Regla Efecte sobre main
Prohibir l'enviament directe Tot entra per PR
Exigir N aprovacions Res no entra sense revisió
Exigir comprovacions en verd Res no entra amb la CI en vermell
Exigir la branca actualitzada amb main Es prova la combinació real
Descartar aprovacions en arribar commits No s'aprova una versió i se n'integra una altra
Prohibir l'enviament forçat Ningú no reescriu l'historial publicat (05-06)
Prohibir l'esborrat de la branca main no desapareix
Exigir signatura dels commits Només entra codi amb autoria verificada

Quan algú intenta saltar-se això, el rebuig arriba del servidor:

git push origin main
remote: error: GH006: Protected branch update failed for refs/heads/main.
remote: error: Required status check "tot-correcte" is expected.
To [email protected]:equip/gestor-tasques.git
 ! [remote rejected] main -> main (protected branch hook declined)
error: failed to push some refs to 'git.exemple.cat:equip/gestor-tasques.git'

Per què això sí que és un control efectiu

Aquí hi ha la resposta que la lliçó 06-01 va deixar pendent, i convé enunciar-la amb precisió:

Un hook de client viu al disc de qui l'executa, a .git/hooks/, que no es versiona. Qualsevol el pot esborrar, modificar o esquivar amb --no-verify. És una ajuda contra el despistament.

Una comprovació d'estat s'executa a la infraestructura del projecte i la decisió d'acceptar l'enviament la pren el servidor. --no-verify és una opció del client: li diu al teu Git que no executi els teus hooks. No viatja al protocol de xarxa i el servidor no s'assabenta que existeix. No hi ha res per saltar-se.

La mateixa diferència, en una frase: el hook de client t'avisa; el servidor decideix.

Un git push --force sobre una branca protegida es rebutja igual, i aquí hi ha la garantia tècnica de la regla d'or de la lliçó 05-06: no reescriure historial publicat deixa de ser una recomanació i passa a ser una impossibilitat.

Un avís sobre les excepcions

Gairebé totes les plataformes permeten que certs rols se saltin les proteccions. És temptador deixar aquella porta oberta «per si de cas». Però pensa quan es fa servir a la pràctica: durant un incident en producció, amb pressa, a les onze de la nit, per algú cansat. És a dir, exactament quan més falta fan les comprovacions. Si de debò cal una via d'emergència, que sigui explícita, que deixi registre i que es revisi després.

  1. Els hooks de servidor: pre-receive, update i post-receive

I ara, per fi, la família completa que la lliçó 06-01 va deixar anunciada.

Quan algú fa git push, el servidor rep els objectes i, abans d'actualitzar cap referència, executa els seus propis hooks. Viuen al directori hooks/ del repositori bare (lliçó 04-01), no en un .git/hooks/ de cap portàtil.

Hook Quan Entrada Efecte del codi de sortida
pre-receive Una vegada, abans d'acceptar res Per stdin: <sha-antic> <sha-nou> <ref>, una línia per referència Sortida diferent de 0 rebutja el push sencer
update Una vegada per cada referència Com a arguments: <ref> <sha-antic> <sha-nou> Rebutja només aquella referència; les altres poden passar
post-receive Després d'acceptar, amb tot actualitzat Igual que pre-receive, per stdin S'ignora: ja no es pot rebutjar res

pre-receive: la porta

Exemple real per a gestor-tasques: rebutjar enviaments que introdueixin fitxers de més de cinc megues, que és la causa habitual de repositoris que es tornen inmanejables (i el motiu d'existir de Git LFS, lliçó 10-03).

#!/bin/bash
# hooks/pre-receive al repositori bare del servidor
LIMIT=$((5 * 1024 * 1024))

while read -r antic nou ref; do
    # Branca nova: comparar contra l'arbre buit
    if [ "$antic" = "0000000000000000000000000000000000000000" ]; then
        rang="$nou"
    else
        rang="$antic..$nou"
    fi

    # Recorrer els objectes nous que arriben en aquest push
    while read -r mode tipus sha mida ruta; do
        [ "$tipus" = "blob" ] || continue
        if [ "$mida" -gt "$LIMIT" ]; then
            echo "REBUTJAT: '$ruta' ocupa $mida bytes (limit: $LIMIT)."
            echo "Fes servir Git LFS per a fitxers grans."
            exit 1
        fi
    done < <(git rev-list --objects "$rang" |
             git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' |
             awk '{print "-", $1, $2, $3, substr($0, index($0,$4))}')
done
exit 0

Fixa't en dos detalls. El primer: la comprovació del hash de zeros, que és com Git indica «aquesta referència no existia abans» (una branca nova). El segon: exit 1 en qualsevol punt rebutja el push complet, incloses les altres branques que vinguessin al mateix enviament. Aquella atomicitat és deliberada: o entra tot o no entra res.

Altres usos típics de pre-receive:

  • Rebutjar force pushes sobre branques concretes.
  • Rebutjar commits el missatge dels quals no segueixi la convenció GT-NNN.
  • Rebutjar commits l'autor dels quals no tingui un correu del domini de l'empresa.
  • Detectar secrets (claus d'API, credencials) al contingut que arriba. Això és matèria de la lliçó 08-05, però convé saber que el lloc correcte per a aquella barrera és aquí.

update: control per referència

S'executa una vegada per cada referència i pot rebutjar només aquella. És el lloc natural per a regles específiques d'una branca o de les etiquetes:

#!/bin/bash
# hooks/update — nomes l'equip de publicacio pot crear etiquetes de versio
ref="$1"; antic="$2"; nou="$3"

case "$ref" in
  refs/tags/v*)
    if [ "$antic" != "0000000000000000000000000000000000000000" ]; then
        echo "REBUTJAT: les etiquetes de versio no es modifiquen ni es mouen."
        exit 1
    fi
    if ! id -nG "$USER" 2>/dev/null | grep -qw publicacio; then
        echo "REBUTJAT: nomes el grup 'publicacio' pot crear etiquetes v*."
        exit 1
    fi
    ;;
esac
exit 0

Aquest exemple protegeix una cosa important que vam veure a la lliçó 05-05: una etiqueta publicada no es mou. Si algú mou v2.0.0 a un altre commit, qui ja se l'havia baixat té una versió diferent de la de qui se la baixi demà, i això és una font de fallades impossibles de diagnosticar.

post-receive: notificar

S'executa amb tot ja acceptat, així que no pot rebutjar res. El seu paper és avisar la resta del món.

#!/bin/bash
# hooks/post-receive
while read -r antic nou ref; do
    branca="${ref#refs/heads/}"
    [ "$branca" = "$ref" ] && continue      # ignorar etiquetes

    autor=$(git log -1 --format='%an' "$nou")
    assumpte=$(git log -1 --format='%s' "$nou")

    # Avisar el xat de l'equip
    curl -sS -X POST "https://xat.exemple.cat/hooks/gestor-tasques" \
         -H 'Content-Type: application/json' \
         -d "{\"text\": \"$autor ha enviat a $branca: $assumpte\"}" >/dev/null

    # I disparar la canonada d'integracio continua
    curl -sS -X POST "https://ci.exemple.cat/api/executar" \
         -H "Authorization: Bearer $TOKEN_CI" \
         -d "{\"branca\": \"$branca\", \"commit\": \"$nou\"}" >/dev/null
done

Aquell segon curl és literalment el disparador de l'apartat 2. Quan fas servir una plataforma allotjada, això passa per sota: el post-receive del servidor emet un webhook que el sistema de CI rep. És també el hook que genera el missatge remote: Crea una pull request per a... que vam veure a la lliçó 07-01.

La limitació de sempre

Ja ho vam apuntar a la 06-01 i cal repetir-ho: en una plataforma allotjada no tens accés al directori hooks/. GitHub, GitLab o Bitbucket no et deixen posar un script al seu servidor. El que ofereixen són els seus equivalents gestionats:

Concepte Servidor propi Plataforma allotjada
Rebutjar un push pel seu contingut pre-receive Push rules (en plans de pagament) o comprovació a la CI
Controlar qui actualitza quina referència update Branques protegides, regles d'etiqueta, CODEOWNERS
Notificar i disparar post-receive Webhooks, integracions natives
Exigir que les proves passin Difícil: el push és síncron Comprovacions d'estat obligatòries

L'última fila és interessant i explica el repartiment real. Un pre-receive s'executa durant el push, amb l'usuari esperant al terminal: no pot llançar una bateria de vint minuts. Per això les comprovacions lentes no viuen en un hook, sinó a la CI, i la seva obligatorietat s'imposa en el moment de fusionar la PR, no en el del push. Els hooks de servidor són per a regles ràpides i estructurals; la CI, per a verificacions lentes i substantives.

  1. Client, servidor i CI: qui comprova què

La foto completa de les tres barreres, que és el resum pràctic de tot el mòdul 6 i d'aquest:

Hook de client Hook de servidor CI / comprovació d'estat
On s'executa Portàtil de cada persona Servidor Git Infraestructura de CI
Quan Abans de commit / push Durant el push Després del push, en paral·lel
Es pot esquivar? : --no-verify, o esborrant el fitxer No No
Es versiona? No (llevat de core.hooksPath + Husky) No, és del servidor : la canonada és al repositori
Velocitat exigida Segons Segons Minuts
Bloqueja l'usuari Sí, mentre s'executa Sí, mentre s'executa No: és asíncron
Què posar-hi Formatatge, lint del que s'ha modificat, format del missatge Mida de fitxers, permisos sobre refs, secrets, force push Proves, compilació, matriu de sistemes, cobertura, seguretat
Paper Estalviar el viatge al servidor Regles estructurals innegociables La verificació real

L'estratègia sana no és triar-ne una, sinó encadenar les tres, cada vegada més lentes i més completes:

flowchart LR
    A["pre-commit<br/>2 segons<br/>format i lint"] --> B["pre-push<br/>30 segons<br/>proves ràpides"]
    B --> C["pre-receive<br/>1 segon<br/>regles estructurals"]
    C --> D["CI a la PR<br/>8 minuts<br/>bateria completa"]
    D --> E["Branca protegida<br/>decideix si s'integra"]

Cada baula detecta el que li toca com abans millor. El hook local t'estalvia un viatge al servidor; el servidor rebutja el que és estructuralment inacceptable; la CI verifica de debò; i la branca protegida converteix el resultat de la CI en la decisió final.

I la regla que resumeix la relació entre les tres: allò obligatori es comprova on l'usuari no mana.

  1. Mantenir la CI ràpida

Una CI lenta no és un inconvenient menor: destrueix la pràctica que pretén sostenir.

La cadena causal és directa. Si la comprovació triga quaranta minuts, la gent deixa d'esperar el resultat i canvia de tasca. En tornar, cal reconstruir el context. Com que esperar és car, s'acumulen canvis per amortitzar l'espera: PRs més grans, branques més llargues, integracions menys freqüents. És a dir, exactament el contrari de la integració contínua. I pitjor: un vermell triga quaranta minuts a detectar-se, durant els quals altres cinc persones hi han construït a sobre.

Referències pràctiques àmpliament acceptades:

Durada Efecte sobre l'equip
< 5 min S'espera el resultat sense canviar de tasca. Ideal
5–10 min Acceptable. El límit pràctic per a TBD
10–20 min Es canvia de context. Comença a fer mal
20–60 min S'acumulen canvis. La integració contínua es degrada
> 60 min La gent ignora la CI. Ha deixat de servir

Les cinc tècniques que més rendeixen:

  1. Memòria cau

El més rendible, de bon tros. Instal·lar dependències sol ser el pas més lent i el més repetitiu.

- name: Cachejar les dependencies
  uses: actions/cache@v4
  with:
    path: ~/.npm
    # La clau inclou el hash del fitxer de bloqueig:
    # si no canvien les dependencies, es reutilitza la memoria cau
    key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
    restore-keys: |
      npm-${{ runner.os }}-

La clau de la qüestió és la clau: ha de canviar exactament quan canviï allò que s'ha desat. Un restore-keys com a xarxa de seguretat permet reutilitzar una memòria cau parcial quan l'exacta no existeix.

  1. Paral·lelisme

Les tasques independents s'executen alhora. Amb la matriu de l'apartat 4, tres sistemes operatius costen el mateix en temps de rellotge que un. També es poden partir les proves en fragments:

strategy:
  matrix:
    fragment: [1, 2, 3, 4]
steps:
  - run: npm test -- --shard=${{ matrix.fragment }}/4

Quatre màquines, un quart del temps. Ull: hi ha un cost fix per tasca (arrencar la màquina, portar el codi, instal·lar) que posa un terra. Partir en vint fragments de trenta segons cadascun no accelera res si arrencar costa un minut.

  1. Executar només allò afectat

Si la PR només toca README.md, no cal la bateria completa. Git dona la informació necessària:

- name: Detectar que ha canviat
  id: canvis
  run: |
    BASE="${{ github.event.pull_request.base.sha }}"
    FITXERS=$(git diff --name-only "$BASE...HEAD")
    echo "$FITXERS"
    if echo "$FITXERS" | grep -qvE '\.(md|txt)$'; then
      echo "codi=true" >> "$GITHUB_OUTPUT"
    else
      echo "codi=false" >> "$GITHUB_OUTPUT"
    fi

- name: Executar les proves
  if: steps.canvis.outputs.codi == 'true'
  run: npm test

Fixa't en el "$BASE...HEAD" amb tres punts: és el diff des de l'ancestre comú, exactament per la raó que vam explicar a la lliçó 07-02. Amb dos punts apareixerien com a canviats els fitxers que ha tocat main, i la detecció seria incorrecta.

Avís important: si aquesta comprovació és obligatòria a la branca protegida i decideixes saltar-te-la, la PR es quedarà esperant per sempre una comprovació que no arriba mai. El patró correcte és que la tasca s'executi sempre i acabi ràpid quan no hi ha res a fer, no que no s'executi.

  1. Esglaonar: ràpid sempre, complet a la nit

No tot s'ha d'executar a cada enviament.

Bateria Quan Durada Contingut
Ràpida Cada enviament i cada PR < 10 min Lint, proves unitàries, compilació
Mitjana En integrar a main < 30 min Proves d'integració, matriu completa
Completa Cada nit Sense límit Extrem a extrem, rendiment, seguretat, navegadors
on:
  schedule:
    - cron: '0 3 * * *'      # cada dia a les 03:00 UTC

El compromís: una fallada detectada només per la nocturna triga fins a un dia a aparèixer. És acceptable si la bateria ràpida cobreix allò que es trenca amb freqüència.

  1. Clon superficial quan es pugui

El fetch-depth: 0 de l'apartat 4 és necessari per als passos que consulten l'historial, però és lent en repositoris grans. A les tasques que no el necessitin, deixa el clon superficial per defecte. I per a casos extrems existeixen els clons parcials (--filter=blob:none), que veurem a la lliçó 10-04.

I una última recomanació que no és tècnica: mesura. Gairebé totes les plataformes mostren la durada per tasca i per pas. Deu minuts mirant aquell desglossament solen revelar que el 70 % del temps se'n va en un pas concret que es pot cachejar. Optimitzar a cegues és llençar el temps.

  1. Merge queue: integrar sense curses

Recorda la conseqüència 2 de l'apartat 3: verd a la teva PR no garanteix verd a main, perquè entre l'última comprovació i la fusió main pot haver avançat.

Amb tres persones i quatre integracions al dia, la probabilitat és baixa i s'assumeix. Amb trenta persones i cinquanta integracions al dia, passa cada dia, i la solució òbvia —exigir que la branca estigui actualitzada amb main abans d'integrar— produeix un efecte pervers: cada vegada que algú integra, totes les altres PRs queden desactualitzades i cal reactualitzar-les i tornar-les a provar. Es converteix en una cursa on només guanya qui prem el botó més ràpid.

La cua d'integració (merge queue, merge train a GitLab) resol això. En lloc de fusionar en prémer el botó, la PR entra en una cua, i el sistema:

  1. Pren la primera PR de la cua.
  2. Construeix un commit especulatiu: main + aquella PR.
  3. Executa les comprovacions sobre aquella combinació.
  4. Si passa, la fusiona a main de debò.
  5. Si falla, l'expulsa de la cua i avisa el seu autor, sense bloquejar les altres.

I l'optimització que la fa viable: se'n processen diverses en paral·lel de manera especulativa. Amb tres PRs a la cua es proven simultàniament main+A, main+A+B i main+A+B+C. Si les tres passen, s'integren les tres de cop. Si B falla, es descarta B i es reaprofita el resultat de main+A, reprocessant només el que ve després.

flowchart TD
    A["PR A aprovada"] --> Q["Cua d'integració"]
    B["PR B aprovada"] --> Q
    C["PR C aprovada"] --> Q
    Q --> S1["Provar main+A"]
    Q --> S2["Provar main+A+B"]
    Q --> S3["Provar main+A+B+C"]
    S1 --> R["Verd: integrar"]
    S2 --> R
    S3 --> X["Vermell: expulsar C<br/>i integrar només A i B"]

Avantatges: main mai no es trenca per una cursa, ningú no ha de reactualitzar la seva branca a mà, i el rendiment de l'equip deixa de degradar-se amb la seva mida.

Cost: consumeix bastant més temps de màquina (cada combinació especulativa és una execució completa) i afegeix latència entre aprovar i integrar.

Quan fa falta: quan l'equip integra tantes vegades al dia que les curses són habituals, normalment a partir de deu o quinze persones treballant sobre el mateix repositori. Per sota d'això és complexitat innecessària: amb exigir la branca actualitzada n'hi ha prou.

  1. Què queda fora: el desplegament

Hem arribat al límit d'aquesta lliçó, i convé marcar-lo bé.

El que hem construït respon a: «és correcte aquest canvi i es pot integrar?». Això és integració contínua.

El que ve després respon a: «com arriba aquest canvi a les mans dels usuaris?». Això és entrega i desplegament continus, i inclou entorns de preproducció, estratègies de promoció, desplegament progressiu, reversió, infraestructura com a codi, observabilitat i tot el que passa després que el commit entri a main.

Aquella meitat és el contingut de la lliçó 10-05: Git a DevOps. Des del punt de vista de Git, l'únic que cal retenir aquí és la frontera:

Integració contínua (aquesta lliçó) Entrega i desplegament (10-05)
Pregunta És correcte? Es pot integrar? Com arriba a l'usuari?
Disparadors a Git push, pull request Fusió a main, etiqueta
Resultat Verd o vermell, integrar o no Versió en funcionament
Si falla No s'integra Es reverteix el desplegament

Fixa't en la fila dels disparadors: les etiquetes de la lliçó 05-05 són la frontissa entre les dues meitats. És el que fa que git tag -a v2.4.0 && git push --follow-tags sigui, en molts projectes, l'acte que inicia una publicació.

Errors Habituals i Consells

Error 1: creure que «tenir CI» és fer integració contínua. Si cada persona integra cada tres setmanes, un servidor de proves no arregla res. La pràctica és integrar sovint; el servidor només la verifica.

Error 2: no saber que la CI prova la fusió, no la teva branca. És la causa de l'«en local em funciona» més freqüent a les PRs. Porta't pull/<n>/merge i reprodueix-ho sobre això.

Error 3: oblidar fetch-depth: 0 quan la canonada fa servir l'historial. El clon superficial trenca git log entre referències, git describe, git merge-base i git blame amb errors confusos.

Error 4: npm install en lloc de npm ci. Les execucions deixen de ser reproduïbles i apareixen fallades que ningú no ha causat.

Error 5: exigir com a obligatòries comprovacions els noms de les quals canvien. Amb matrius, els noms es generen. Fes servir una única tasca resum (tot-correcte) com a comprovació obligatòria.

Error 6: filtrar per rutes una comprovació obligatòria. Si no es llança, la PR espera per sempre. La tasca s'ha d'executar sempre i acabar ràpid quan no hi ha res a fer.

Error 7: conviure amb proves inestables. Un vermell aleatori ensenya l'equip a rellançar sense mirar, i amb això mor tot el senyal. Arregla-la, aïlla-la o esborra-la, però no la ignoris.

Error 8: tolerar una CI de quaranta minuts. Degrada el flux sencer cap a PRs grans i branques llargues. És una inversió amb retorn immediat.

Error 9: confiar en els hooks de client per a allò obligatori. --no-verify i ja està. Allò innegociable va al servidor o a una comprovació d'estat.

Error 10: deixar oberta l'excepció de les branques protegides. Es farà servir durant un incident, amb pressa, que és quan és pitjor idea.

Error 11: no enviar les etiquetes i esperar que es dispari la publicació. git push --follow-tags.

Consell 1: concurrency amb cancel-in-progress des del primer dia. Estalvi immediat de temps de màquina, cost zero.

Consell 2: una tasca resum de la qual depenguin totes les altres. Simplifica moltíssim la configuració de la branca protegida.

Consell 3: mesura abans d'optimitzar. El desglossament per passos gairebé sempre revela un únic culpable cachejable.

Consell 4: esglaona les bateries. Ràpida a cada enviament, mitjana en integrar, completa a la nit.

Consell 5: valida a la CI el que el hook commit-msg valida en local. El hook avisa aviat; la CI obliga.

Consell 6: comprova els punters dels submòduls a la CI. Tres línies que eviten clons trencats per a tot l'equip (lliçó 06-05).

Consell 7: desa els artefactes amb if: always(). Els informes d'una fallada són justament els que més necessites.

Consell 8: --filter i clons parcials en repositoris grans. I si el problema són fitxers binaris pesats, la resposta és Git LFS (lliçó 10-03).

Exercicis

Exercici 1: comprovar el que comprova la CI

Sense plataforma, reproduint el mecanisme amb Git pur:

  1. Crea un repositori amb app.js i main en tres commits.
  2. Crea funcionalitat/filtres des del segon commit i fes dos commits que reanomenin una funció.
  3. Torna a main i afegeix un commit que cridi aquella funció amb el nom antic.
  4. Calcula a mà el commit que provaria la CI: crea una branca temporal des de main i fusiona-hi la branca de la funcionalitat.
  5. Comprova que git diff main...funcionalitat/filtres no revela el problema, i que el resultat de la fusió sí que el té.
  6. Escriu un script de tres línies que, donades dues branques, produeixi el commit de fusió sense deixar rastre (pista: git merge-tree).

Exercici 2: un hook de servidor efectiu

  1. Crea /tmp/servidor/gestor.git com a repositori bare i clona'l a /tmp/ana.
  2. Escriu un hooks/pre-receive al bare que rebutgi qualsevol push que contingui un commit l'assumpte del qual no comenci per GT-NNN o per Fusiona .
  3. Comprova que un commit amb missatge correcte passa i un d'incorrecte es rebutja, i que en el segon cas cap referència del push no s'actualitza.
  4. Instal·la a /tmp/ana un hook de client commit-msg amb la mateixa regla, i demostra que git commit --no-verify l'esquiva però que el pre-receive continua rebutjant el push.
  5. Escriu un hooks/update que impedeixi modificar o esborrar qualsevol etiqueta ja existent, i comprova-ho.

Exercici 3: una canonada mínima i la seva optimització

  1. Escriu un fitxer ci/canonada.yml per a gestor-tasques amb: disparadors per enviament a main i per PR, una tasca de lint, una tasca de proves en matriu de tres sistemes i una tasca resum de la qual depenguin les dues.
  2. Afegeix memòria cau de dependències amb una clau basada en el hash del fitxer de bloqueig.
  3. Afegeix un pas que validi els missatges de commit només de la PR, fent servir el rang correcte.
  4. Afegeix una detecció de canvis que eviti executar les proves si només s'han tocat fitxers .md, sense deixar de publicar el resultat de la comprovació.
  5. Explica, per a cada elecció, quin problema evita.

Solucions

Solució 1:

mkdir /tmp/ci-fusio && cd /tmp/ci-fusio
git init -qb main
printf 'function desaTasques(t) { return t; }\n' > app.js
git add . && git commit -q -m "Afegeix desaTasques"
echo 'const tasques = [];' >> app.js
git add . && git commit -q -m "Afegeix l'estat inicial"
BASE=$(git rev-parse HEAD)
echo 'desaTasques(tasques);' >> app.js
git commit -qam "Desa l'estat en arrencar"
# 2. La branca reanomena
git switch -qc funcionalitat/filtres "$BASE"
sed -i 's/function desaTasques/function persisteixTasques/' app.js
git commit -qam "Reanomena desaTasques a persisteixTasques"
echo 'function filtra(f) { return tasques.filter(f); }' >> app.js
git commit -qam "Afegeix el filtratge"
# 3. main ja tenia la crida antiga (commit del pas 1)
# 4. El commit que provaria la CI
git switch -q main
git switch -qc simulacio-merge-de-la-ci
git merge -q funcionalitat/filtres
cat app.js
function persisteixTasques(t) { return t; }
const tasques = [];
desaTasques(tasques);
function filtra(f) { return tasques.filter(f); }

Fusiona sense conflicte i el resultat crida desaTasques, que ja no existeix: conflicte semàntic.

# 5. El diff de la PR no ho revela
git switch -q main
git diff main...funcionalitat/filtres

El diff mostra el reanomenament i el filtratge, i res més: la crida trencada és a main, no a la branca, així que no apareix. Només provant la fusió es detecta.

# 6. El commit de fusio sense tocar l'arbre de treball
git merge-tree --write-tree main funcionalitat/filtres
4f8b1d3a9c2e7f0b5d8a1c4e7f0b3d6a9c2e5f81

git merge-tree (Git 2.38 o superior) retorna l'arbre resultant sense modificar el directori de treball ni crear cap commit. És, essencialment, el que fan les plataformes per calcular pull/<n>/merge.

Solució 2:

mkdir -p /tmp/servidor && git init -q --bare /tmp/servidor/gestor.git
git clone -q /tmp/servidor/gestor.git /tmp/ana
cd /tmp/ana && git switch -qc main
echo "inicial" > app.js && git add . && git commit -q -m "GT-001 Estat inicial"
git push -qu origin main
# 2. El hook de servidor
cat > /tmp/servidor/gestor.git/hooks/pre-receive <<'EOF'
#!/bin/bash
BUIT=0000000000000000000000000000000000000000
while read -r antic nou ref; do
    [ "$nou" = "$BUIT" ] && continue             # esborrat de branca
    if [ "$antic" = "$BUIT" ]; then
        rang="$nou"
    else
        rang="$antic..$nou"
    fi
    while read -r assumpte; do
        if ! echo "$assumpte" | grep -qE '^(GT-[0-9]{1,5} |Fusiona )'; then
            echo "REBUTJAT: '$assumpte' no segueix la convencio GT-NNN."
            exit 1
        fi
    done < <(git log --format=%s "$rang")
done
exit 0
EOF
chmod +x /tmp/servidor/gestor.git/hooks/pre-receive
# 3. Missatge correcte: passa
cd /tmp/ana
echo "ok" >> app.js && git commit -qam "GT-002 Afegeix el filtratge per etiqueta"
git push origin main
# Missatge incorrecte: es rebutja
echo "malament" >> app.js && git commit -qam "arranjaments diversos"
git push origin main
remote: REBUTJAT: 'arranjaments diversos' no segueix la convencio GT-NNN.
To /tmp/servidor/gestor.git
 ! [remote rejected] main -> main (pre-receive hook declined)
error: failed to push some refs to '/tmp/servidor/gestor.git'
# I cap referencia no s'ha mogut
git --git-dir=/tmp/servidor/gestor.git log --oneline -1 main
# 4. El hook de client i --no-verify
cat > /tmp/ana/.git/hooks/commit-msg <<'EOF'
#!/bin/bash
grep -qE '^(GT-[0-9]{1,5} |Fusiona )' "$1" || {
  echo "El missatge ha de comencar per GT-NNN."; exit 1; }
EOF
chmod +x /tmp/ana/.git/hooks/commit-msg

cd /tmp/ana
git reset -q --hard HEAD~1
echo "malament" >> app.js
git commit -qam "una altra vegada arranjaments"            # el hook el bloqueja
git commit -qam "una altra vegada arranjaments" --no-verify # l'esquiva
git push origin main                                       # el servidor NO s'esquiva

--no-verify desactiva el hook local perquè el fitxer és al disc de l'Ana. El pre-receive s'executa al servidor i no hi ha cap opció del client que l'abasti: això és el que el converteix en un control efectiu.

# 5. Protegir les etiquetes
git reset -q --hard HEAD~1
cat > /tmp/servidor/gestor.git/hooks/update <<'EOF'
#!/bin/bash
ref="$1"; antic="$2"
BUIT=0000000000000000000000000000000000000000
case "$ref" in
  refs/tags/*)
    if [ "$antic" != "$BUIT" ]; then
        echo "REBUTJAT: les etiquetes no es modifiquen ni es mouen."
        exit 1
    fi ;;
esac
exit 0
EOF
chmod +x /tmp/servidor/gestor.git/hooks/update

cd /tmp/ana
git tag -a v1.0.0 -m "Versio 1.0.0" && git push -q origin v1.0.0
git tag -f -a v1.0.0 -m "Moguda" && git push --force origin v1.0.0
remote: REBUTJAT: les etiquetes no es modifiquen ni es mouen.
 ! [remote rejected] v1.0.0 -> v1.0.0 (hook declined)

Solució 3:

# ci/canonada.yml
name: Integracio continua

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

concurrency:
  group: ci-${{ github.ref }}
  cancel-in-progress: true

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - uses: actions/setup-node@v4
        with:
          node-version: '22'
      # 2. Memoria cau amb clau basada en el fitxer de bloqueig
      - uses: actions/cache@v4
        with:
          path: ~/.npm
          key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
          restore-keys: npm-${{ runner.os }}-
      - run: npm ci
      - run: npm run lint

      # 3. Missatges de commit: nomes els que aporta la PR
      - name: Validar els missatges de confirmacio
        if: github.event_name == 'pull_request'
        run: |
          BASE="${{ github.event.pull_request.base.sha }}"
          git log --format=%s "$BASE..HEAD" | while read -r a; do
            echo "$a" | grep -qE '^(GT-[0-9]+ |Fusiona )' || {
              echo "Missatge invalid: $a"; exit 1; }
          done

  proves:
    runs-on: ${{ matrix.so }}
    strategy:
      fail-fast: false
      matrix:
        so: [ubuntu-latest, macos-latest, windows-latest]
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      # 4. Deteccio de canvis: la tasca SEMPRE s'executa,
      #    pero se salta el pas car si nomes hi ha documentacio.
      - name: Detectar canvis de codi
        id: canvis
        shell: bash
        run: |
          if [ "${{ github.event_name }}" = "pull_request" ]; then
            BASE="${{ github.event.pull_request.base.sha }}"
            FITXERS=$(git diff --name-only "$BASE...HEAD")
          else
            FITXERS=$(git diff --name-only HEAD~1 HEAD)
          fi
          if echo "$FITXERS" | grep -qvE '\.(md|txt)$'; then
            echo "codi=true" >> "$GITHUB_OUTPUT"
          else
            echo "codi=false" >> "$GITHUB_OUTPUT"
          fi

      - uses: actions/setup-node@v4
        if: steps.canvis.outputs.codi == 'true'
        with:
          node-version: '22'
          cache: 'npm'
      - run: npm ci
        if: steps.canvis.outputs.codi == 'true'
      - run: npm test
        if: steps.canvis.outputs.codi == 'true'
      - name: Sense canvis de codi
        if: steps.canvis.outputs.codi == 'false'
        run: echo "Nomes documentacio: no s'executen les proves."

  tot-correcte:
    runs-on: ubuntu-latest
    needs: [lint, proves]
    if: always()
    steps:
      - run: |
          [ "${{ contains(needs.*.result, 'failure') }}" = "false" ] || exit 1
          echo "Totes les comprovacions han passat."

5. Què evita cada elecció:

Elecció Problema que evita
concurrency + cancel-in-progress Malgastar màquines en execucions que ja no importen
fetch-depth: 0 fatal: bad revision en recórrer els commits de la PR
Memòria cau amb hash del bloqueig Reinstal·lar dependències idèntiques a cada execució
npm ci Execucions no reproduïbles per resolució de versions
fail-fast: false Perdre la informació dels altres sistemes en fallar-ne un
Rang $BASE..HEAD Validar commits de main que no són d'aquesta PR
Diff amb tres punts a la detecció Comptar com a canviats fitxers que només va tocar main
La tasca s'executa sempre, se salta el pas car Que una comprovació obligatòria no es llanci i la PR esperi per sempre
Tasca tot-correcte Haver de llistar noms de matriu a la branca protegida

Conclusió

Amb aquesta lliçó es tanca el mòdul 7. L'essencial:

  • La integració contínua no és tenir un servidor de CI: és integrar de debò i sovint, almenys diàriament, amb cada integració verificada automàticament. Sense integració freqüent, un servidor de proves només verifica versions divergents que encara no s'han trobat.
  • S'enganxa a Git mitjançant disparadors: per push, per pull request (inclòs synchronize, quan arriben commits nous) i per etiqueta, que és la frontissa cap a la publicació. I recorda que les etiquetes no viatgen amb git push a seques.
  • En una pull request, la CI comprova el resultat de la fusió, no la teva branca. És la referència pull/<n>/merge. Per això la teva PR es pot posar en vermell sense que hagis fet res, per això detecta conflictes semàntics, i per això verd a la PR no garanteix verd a main.
  • Una canonada ben construïda porta l'historial que necessita (fetch-depth: 0), instal·la de manera reproduïble (npm ci), paral·lelitza en matriu de sistemes, comprova també els punters dels submòduls, i acaba en una tasca resum de la qual depenguin les altres.
  • Una comprovació d'estat s'associa a un commit; una branca protegida la converteix en requisit. I això és el que la 06-01 va deixar pendent: --no-verify és una opció del client, no viatja per la xarxa i el servidor no s'assabenta que existeix. El hook local t'avisa; el servidor decideix.
  • Els hooks de servidor tanquen el cercle: pre-receive (una vegada, rebutja el push sencer), update (una vegada per referència, rebutja només aquella) i post-receive (després, només notifica i dispara). Són efectius perquè s'executen on l'usuari no mana. En plataformes allotjades no tens accés a hooks/, i els seus equivalents són les regles de push, les branques protegides i els webhooks.
  • El repartiment sa encadena les tres barreres: hook de client per al que és ràpid i local, hook de servidor per al que és estructural i innegociable, CI per a la verificació lenta i real. Allò obligatori es comprova on l'usuari no mana.
  • Una CI lenta destrueix el flux: empeny cap a PRs grans, branques llargues i integració tardana. Menys de deu minuts com a objectiu, amb memòria cau, paral·lelisme, execució de només allò afectat i bateries esglaonades.
  • La merge queue elimina la cursa entre PRs aprovades provant combinacions especulatives abans de fusionar. Val la pena a partir de deu o quinze persones sobre el mateix repositori.
  • I la frontera: aquí acaba la integració. Com aquell codi arriba als usuaris —entorns, promoció, desplegament progressiu, reversió— és la lliçó 10-05.

El mòdul, en una idea

L'equip de gestor-tasques va començar aquest mòdul amb una eina dominada i sense acords. Ara té un procés complet: el Diego, sense permís d'escriptura, proposa canvis des del seu fork mitjançant pull requests; l'Ana els revisa amb git diff main...branca i git range-diff; l'equip ha triat el seu flux de branques segons el producte —Git Flow per a la versió instal·lable, alguna cosa propera a GitHub Flow o TBD per al núvol—; i unes comprovacions automàtiques que ningú no pot esquivar decideixen què entra a la línia principal.

Tenen procés. El que els falta ara són hàbits.

Perquè un procés impecable conviu perfectament amb un historial il·legible. Es poden tenir branques protegides, revisió obligatòria i CI en verd, i tot i així acumular cent commits anomenats «canvis» que ningú no podrà interpretar d'aquí a un any; versionar la carpeta node_modules i un fitxer de configuració amb la contrasenya de la base de dades; patir conflictes absurds perquè un company de Windows desa els finals de línia d'una altra manera; i descobrir un dia que una clau d'API porta vuit mesos a l'historial públic.

Això és el mòdul 8. Com escriure missatges de confirmació que serveixin de documentació (08-01, on per fi desenvoluparem la convenció que aquí només hem validat); com mantenir un historial llegible i quina política d'integració triar (08-02, que tanca la decisió que vam deixar oberta a la 07-04); què no versionar mai (08-03 i 08-04); com no filtrar un secret i què fer si ja ha passat (08-05); i com mantenir el repositori ràpid quan creix (08-06).

Comencem pel més quotidià i el més desatès: la lliçó 08-01: Escrivint Bons Missatges de Confirmació.

Dominant Git: De Principiant a Avançat

Mòdul 1: Introducció a Git

Mòdul 2: Operacions Bàsiques de Git

Mòdul 3: Branques i Fusió

Mòdul 4: Treballant amb Repositoris Remots

Mòdul 5: Operacions Avançades de Git

Mòdul 6: Eines i Tècniques de Git

Mòdul 7: Estratègies de Col·laboració i Flux de Treball

Mòdul 8: Bones Pràctiques i Consells de Git

Mòdul 9: Resolució de Problemes i Depuració

Mòdul 10: Git al Món Real

© Copyright 2026. Tots els drets reservats