Aquesta lliçó tanca una promesa que el curs arrossega des del mòdul 7. A la lliçó 07-06 vam traçar una frontera explícita: allà parlàvem d'integració contínua —comprovar automàticament que el que s'integra funciona— i vam deixar per aquí com aquest codi arriba als usuaris. A la 07-04 vam ajornar la mecànica dels entorns i la promoció. I a la 05-05, en crear l'etiqueta anotada v1.4.0, vam dir que les etiquetes serien la peça que dispara un desplegament.

Tot convergeix aquí.

El canvi de perspectiva és gran. Fins ara, Git ha estat una eina que fan servir persones: l'Ana escriu codi, en Bruno revisa, la Carla integra. A partir d'aquest punt, Git és també —i sobretot— una eina que fan servir màquines. Canonades que clonen, llegeixen etiquetes, comparen hashos, construeixen artefactes i publiquen en producció sense que ningú premi res. En una organització amb desplegament continu, la immensa majoria dels git clone del dia els executa un servidor, no una persona.

I això canvia el pes de cada operació. Quan git push origin main significa "això serà a producció d'aquí a quatre minuts", el rigor deixa de ser una virtut professional i passa a ser un requisit operatiu.

Contingut

  1. Git com a font única de veritat
  2. Integració, lliurament i desplegament continus
  3. Què dispara un desplegament i com es lliga a Git
  4. L'artefacte s'identifica pel hash del commit
  5. Infraestructura com a codi
  6. GitOps: el repositori com a estat desitjat
  7. Secrets en un món automatitzat
  8. Versionatge i changelog automàtics
  9. Reversió en producció
  10. Que la canonada no depengui d'un humà

  1. Git com a font única de veritat

La idea de fons de DevOps, pel que fa a Git, és una ampliació del que ja saps: el repositori deixa de contenir només codi.

Històricament, un repositori contenia l'aplicació. Tota la resta —com es configura el servidor, quina versió de la base de dades hi ha, quines variables d'entorn existeixen, com es desplega— vivia al cap d'algú, en un document, en una consola web o en un guió a l'escriptori d'un administrador.

El canvi consisteix a ficar tot això també a Git:

gestor-tasques/
├── index.html
├── estils.css
├── app.js
├── README.md
├── .github/workflows/          <- definició de les canonades
│   ├── ci.yml
│   ├── lliurament.yml
│   └── desplegament.yml
├── infra/                      <- infraestructura com a codi
│   ├── produccio.tf
│   ├── preproduccio.tf
│   └── moduls/
├── entorns/                    <- configuració per entorn
│   ├── produccio.yaml
│   ├── preproduccio.yaml
│   └── proves.yaml
├── Containerfile               <- com es construeix l'artefacte
└── migracions/                 <- canvis d'esquema versionats
    ├── 001-crear-tasques.sql
    └── 002-afegir-oculta.sql

Què s'hi guanya

Propietat Què significa a la pràctica
Historial "Quan va canviar el límit de memòria del servei?" és un git log sobre infra/produccio.tf
Revisió Un canvi d'infraestructura passa pel mateix procés de petició d'extracció i revisió que el codi (07-02)
Reversió Tornar a una configuració anterior és git revert
Reproductibilitat Un entorn nou s'aixeca des del repositori, sense coneixement tàcit
Traçabilitat Cada canvi en producció té autor, data, missatge i tiquet
Atomicitat Un commit pot canviar el codi i la configuració que necessita, alhora

Aquesta última fila és la més valuosa i la que més se subestima. Si app.js comença a necessitar una variable d'entorn nova, el commit que introdueix el codi i el que declara la variable són el mateix commit. Mai no existeix una revisió del repositori on el codi demani alguna cosa que la configuració no dona.

La regla

Si alguna cosa pot trencar producció, ha d'estar a Git.

Configuració, infraestructura, migracions de base de dades, definició de les canonades, polítiques d'accés. Tot allò que sigui un fitxer de text i afecti el comportament del sistema.

I el corol·lari, que és igual d'important:

Si alguna cosa és a Git, no s'ha de canviar fora de Git.

Modificar la configuració de producció des d'una consola web crea una deriva: el repositori diu una cosa i la realitat en diu una altra. Tan bon punt la deriva existeix, el repositori deixa de ser la font de veritat i tot l'edifici cau. És el problema que GitOps ataca directament (apartat 6).

L'únic que no va a Git

Amb una excepció absoluta, ja establerta a la lliçó 08-05: els secrets, mai. Contrasenyes, claus d'API, certificats privats, tokens. Van en un gestor de secrets i s'injecten en el moment del desplegament. Hi tornarem a l'apartat 7.

  1. Integració, lliurament i desplegament continus

Tres termes que es confonen constantment, i la diferència dels quals és exactament on és el botó humà.

flowchart LR
    A["Commit<br/>en una branca"] --> B["Compilar<br/>i provar"]
    B --> C["Integrar<br/>a main"]
    C --> D["Construir<br/>l'artefacte"]
    D --> E["Publicar al<br/>registre"]
    E --> F{"Aprovació<br/>humana"}
    F --> G["Desplegar a<br/>producció"]

    subgraph CI["Integració contínua (07-06)"]
        A
        B
        C
    end
    subgraph CD1["Lliurament continu"]
        D
        E
        F
    end
    subgraph CD2["Desplegament continu"]
        G
    end
Integració contínua Lliurament continu Desplegament continu
Què garanteix El que s'integra compila i passa les proves El que hi ha a main es pot desplegar en qualsevol moment El que hi ha a main està desplegat
On acaba A main, en verd En un artefacte publicat i llest A producció
Botó humà No n'hi ha : algú aprova el desplegament No n'hi ha
Disparador push a una branca, petició d'extracció Integració a main Integració a main
Freqüència típica Desenes al dia Desenes al dia Desenes al dia
Què cal Proves ràpides i fiables Artefactes reproduïbles, entorns de preproducció Tot l'anterior + confiança total en les proves + reversió ràpida
Risc si falla Es bloqueja la petició d'extracció Es bloqueja la publicació Producció trencada

La diferència entre lliurament i desplegament és exactament un clic. I la decisió de treure aquest clic no és tècnica sinó de confiança: es treu quan el conjunt de proves és tan bo i la reversió tan ràpida que un humà mirant la pantalla no aporta seguretat, només latència.

Què cal per arribar al desplegament continu

  1. Proves en què es confia de debò. Si l'equip executa proves manuals "per si de cas" abans de desplegar, no està a punt.
  2. Reversió en minuts. L'assegurança no és "no fallar mai", sinó "arreglar-ho abans que importi".
  3. Desplegament progressiu. Treure la versió nova a un percentatge del trànsit i observar abans de completar.
  4. Observabilitat. Mètriques i alertes que detectin un problema abans que els usuaris.
  5. Canvis petits. És la connexió amb Trunk Based Development (lliçó 07-05): com més petit és el canvi, menor el risc i més fàcil identificar-ne la causa.

Aquest últim punt mereix èmfasi, perquè és on Git i DevOps es toquen directament. El desplegament continu obliga a commits petits i ben acotats. Si integres un canvi de 2.000 línies i producció es trenca, no saps quina de les 2.000 va ser. Tota la disciplina del mòdul 8 —commits atòmics, missatges que expliquen el perquè, historial llegible— passa de ser una bona pràctica a ser una eina d'operació.

  1. Què dispara un desplegament i com es lliga a Git

Hi ha tres mecanismes, i convé entendre quin model mental implica cadascun.

Mecanisme 1: per branca

name: Desplegar a preproducció

on:
  push:
    branches: [main]

jobs:
  desplegar:
    runs-on: ubuntu-latest
    environment: preproduccio
    steps:
      - uses: actions/checkout@v4

      - name: Construir l'artefacte
        run: |
          docker build -t registre.exemple.cat/gestor-tasques:${{ github.sha }} .
          docker push registre.exemple.cat/gestor-tasques:${{ github.sha }}

      - name: Desplegar
        run: ./infra/desplegar.sh preproduccio ${{ github.sha }}

Model mental: "la branca main és el que hi ha a preproducció". La branca deixa de ser només una línia de desenvolupament i passa a ser el punter a un entorn.

És el mecanisme natural per a entorns no productius. Cada integració a main actualitza preproducció, sense cerimònia.

Mecanisme 2: per etiqueta anotada

Aquí es tanca el que vam prometre a la lliçó 05-05.

name: Desplegar a producció

on:
  push:
    tags: ['v*.*.*']

jobs:
  desplegar:
    runs-on: ubuntu-latest
    environment: produccio
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Verificar que l'etiqueta és anotada i està signada
        run: |
          TIPUS=$(git cat-file -t "${{ github.ref_name }}")
          if [ "$TIPUS" != "tag" ]; then
            echo "ERROR: ${{ github.ref_name }} és una etiqueta lleugera."
            echo "Producció només accepta etiquetes anotades."
            exit 1
          fi
          git tag --verify "${{ github.ref_name }}"

      - name: Verificar que l'etiqueta és a main
        run: |
          git merge-base --is-ancestor "${{ github.ref_name }}" origin/main \
            || { echo "ERROR: l'etiqueta no és a main"; exit 1; }

      - name: Desplegar l'artefacte ja construït
        run: |
          SHA=$(git rev-list -n1 "${{ github.ref_name }}")
          ./infra/desplegar.sh produccio "$SHA"

Model mental: "l'etiqueta v1.4.0 és una versió publicable, i publicar-la és desplegar-la".

Tres detalls que importen molt:

1. S'exigeix etiqueta anotada, no lleugera. Recorda de la lliçó 05-05 que una etiqueta lleugera és només una referència a un commit, mentre que una d'anotada és un objecte propi amb autor, data, missatge i possibilitat de signatura. Per a producció, això és exactament el que vols: un registre de qui va declarar aquella versió i quan. La comprovació git cat-file -t retorna tag per a les anotades i commit per a les lleugeres.

2. Es verifica la signatura. git tag --verify comprova la signatura GPG (lliçó 08-05). Així, crear una etiqueta de producció requereix una clau privada, no només permisos d'escriptura.

3. Es comprova que l'etiqueta és a main. git merge-base --is-ancestor impedeix desplegar una etiqueta posada sobre una branca que mai no es va integrar. És una salvaguarda contra l'error d'etiquetar des de la branca equivocada, que passa més del que sembla.

Molt important: el pas de desplegament no reconstrueix res. Recupera l'artefacte que ja es va construir quan aquell commit va entrar a main. Hi tornarem a l'apartat següent, perquè és una de les regles d'or del lliurament.

Mecanisme 3: aprovació manual sobre un commit concret

name: Desplegar a producció (manual)

on:
  workflow_dispatch:
    inputs:
      commit:
        description: 'Hash complet del commit a desplegar'
        required: true

jobs:
  desplegar:
    runs-on: ubuntu-latest
    environment: produccio    # amb revisors obligatoris configurats
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ inputs.commit }}
          fetch-depth: 0

      - name: Comprovar que el commit és a main i té artefacte
        run: |
          git merge-base --is-ancestor "${{ inputs.commit }}" origin/main \
            || { echo "ERROR: aquest commit no és a main"; exit 1; }
          docker manifest inspect \
            "registre.exemple.cat/gestor-tasques:${{ inputs.commit }}" > /dev/null \
            || { echo "ERROR: no hi ha artefacte per a aquest commit"; exit 1; }

      - name: Desplegar
        run: ./infra/desplegar.sh produccio "${{ inputs.commit }}"

Model mental: "qualsevol commit de main és candidat; algú decideix quin i quan".

És el mecanisme més flexible i el que cal per tornar enrere ràpid, com veurem a l'apartat 9.

Taula comparativa

Per branca Per etiqueta Manual sobre commit
Ús típic Preproducció, entorns de prova Producció amb versions Producció amb desplegament continu; reversions
Qui decideix El procés d'integració Qui crea l'etiqueta Qui llança l'execució
Traçabilitat Bona Excel·lent: objecte amb autor, data, missatge i signatura Bona, si es registra l'execució
Reversió Difícil: cal revertir el commit Fàcil: desplegar l'etiqueta anterior Molt fàcil: desplegar el commit anterior
Risc de desplegament accidental Alt: qualsevol integració desplega Baix: cal crear l'etiqueta expressament Molt baix
Encaixa amb GitHub Flow, Trunk Based Git Flow, productes amb versions Trunk Based amb desplegament continu

El muntatge habitual de gestor-tasques

Els tres mecanismes combinats, cadascun al seu lloc:

flowchart TD
    A["Petició d'extracció GT-231"] -->|"CI: proves"| B["Integració a main"]
    B -->|"automàtic<br/>per branca"| C["Preproducció"]
    C -->|"validació"| D["git tag -a -s v1.4.0"]
    D -->|"automàtic<br/>per etiqueta"| E["Producció"]
    E -.->|"si alguna cosa falla:<br/>desplegament manual<br/>de v1.3.0"| F["Reversió"]

  1. L'artefacte s'identifica pel hash del commit

Aquesta és probablement la pràctica més important de tota la lliçó, i la que més equips fan malament.

La regla

Cada artefacte construït —imatge de contenidor, paquet, binari— s'etiqueta amb el hash complet del commit del qual va sortir.

SHA=$(git rev-parse HEAD)

docker build -t "registre.exemple.cat/gestor-tasques:$SHA" .
docker push "registre.exemple.cat/gestor-tasques:$SHA"

# I a més, etiquetes llegibles que APUNTEN al mateix artefacte
docker tag "registre.exemple.cat/gestor-tasques:$SHA" \
           "registre.exemple.cat/gestor-tasques:v1.4.0"
docker tag "registre.exemple.cat/gestor-tasques:$SHA" \
           "registre.exemple.cat/gestor-tasques:main"

El hash és l'identificador canònic; v1.4.0 i main són àlies llegibles que hi apunten. Exactament la mateixa relació que a Git entre un hash de commit i les referències que l'apunten (lliçó 03-01): les etiquetes es mouen, el hash no.

Per què

1. Elimina l'ambigüitat. gestor-tasques:latest no significa res: és una etiqueta mòbil que apunta a l'últim que algú va construir. gestor-tasques:4f8a2e6c9d3b... identifica un contingut exacte i reproduïble.

2. Dona traçabilitat completa. Davant d'un error en producció, la cadena es recorre sencera i sense preguntar a ningú:

# 1. Què hi ha desplegat?
kubectl get deployment gestor-tasques -o jsonpath='{.spec.template.spec.containers[0].image}'
# registre.exemple.cat/gestor-tasques:4f8a2e6c9d3b1a5e7f2c8b0d4a6e9f1c3b5d7a0e

# 2. Quin commit és?
git show 4f8a2e6c

# 3. Què ha canviat respecte a la versió anterior?
git log --oneline 9e2f7a4c..4f8a2e6c

# 4. Qui i per què?
git log -1 --format='%an <%ae>%n%n%B' 4f8a2e6c

# 5. Quin tiquet?
git log -1 --format=%B 4f8a2e6c | grep -oE 'GT-[0-9]+'

Del contenidor en producció al tiquet, en cinc ordres i sense obrir cap interfície.

3. Permet verificar la coherència. Està desplegat el que ens pensem?

DESPLEGAT=$(kubectl get deployment gestor-tasques \
  -o jsonpath='{.spec.template.spec.containers[0].image}' | cut -d: -f2)

git merge-base --is-ancestor "$DESPLEGAT" origin/main \
  && echo "OK: el que està desplegat és a main" \
  || echo "ALERTA: s'ha desplegat una cosa que no és a main"

# Quants commits van per davant de producció?
git rev-list --count "$DESPLEGAT"..origin/main

4. Fa possible el desplegament idempotent. Desplegar el mateix hash dues vegades produeix exactament el mateix resultat. Amb latest, no hi ha manera de saber-ho.

El corol·lari: construir una vegada, desplegar moltes

L'artefacte es construeix UNA sola vegada i aquest mateix artefacte recorre tots els entorns.

flowchart LR
    C["commit 4f8a2e6"] --> B["Construcció<br/>ÚNICA"]
    B --> A["Artefacte<br/>:4f8a2e6"]
    A --> E1["Proves"]
    A --> E2["Preproducció"]
    A --> E3["Producció"]

Si cada entorn reconstrueix l'artefacte, el que has provat no és el que desplegues. Entre una construcció i una altra poden canviar les versions de les dependències, la imatge base o la versió del compilador. L'artefacte que va passar les proves i el que arriba a producció serien dues coses diferents, i tota la validació prèvia deixa de significar res.

El que sí que canvia entre entorns és la configuració, que s'injecta en el moment del desplegament: variables d'entorn, secrets, adreces de serveis. L'artefacte és el mateix binari; l'entorn el parametritza.

Incrustar la procedència a l'artefacte

Convé que el mateix artefacte sàpiga d'on va venir:

FROM node:22-alpine
ARG COMMIT_SHA
ARG VERSION
ARG DATA_CONSTRUCCIO
LABEL org.opencontainers.image.revision="${COMMIT_SHA}"
LABEL org.opencontainers.image.version="${VERSION}"
LABEL org.opencontainers.image.created="${DATA_CONSTRUCCIO}"
ENV COMMIT_SHA=${COMMIT_SHA}
COPY . /app
WORKDIR /app
RUN npm ci --omit=dev
CMD ["node", "app.js"]
docker build \
  --build-arg COMMIT_SHA="$(git rev-parse HEAD)" \
  --build-arg VERSION="$(git describe --tags --always --dirty)" \
  --build-arg DATA_CONSTRUCCIO="$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
  -t "registre.exemple.cat/gestor-tasques:$(git rev-parse HEAD)" .

Aquest git describe --tags --always --dirty és una joia per a això. Produeix cadenes com:

Sortida Què significa
v1.4.0 Exactament l'etiqueta v1.4.0
v1.4.0-7-g4f8a2e6 7 commits després de v1.4.0, al commit 4f8a2e6
v1.4.0-7-g4f8a2e6-dirty El mateix, però amb canvis sense confirmar: no hauria d'arribar a producció

Aquest sufix -dirty és una alarma valuosa. Un artefacte marcat com a dirty es va construir des d'una còpia de treball bruta, cosa que significa que no és reproduïble des del repositori. La canonada l'hauria de rebutjar:

if git describe --always --dirty | grep -q -- '-dirty$'; then
  echo "ERROR: la còpia de treball té canvis sense confirmar."
  exit 1
fi

I a l'aplicació, exposar aquesta informació:

// app.js
app.get('/version', (req, res) => {
  res.json({
    commit: process.env.COMMIT_SHA,
    version: process.env.VERSION,
    construit: process.env.DATA_CONSTRUCCIO
  });
});

Un punt d'accés /version que diu el hash exacte que s'està executant resol per si sol la pregunta més freqüent durant una incidència: "què hi ha realment aquí?".

  1. Infraestructura com a codi

La infraestructura —servidors, xarxes, bases de dades, balancejadors, permisos— es declara en fitxers de text versionats, i una eina s'encarrega que la realitat coincideixi amb el que s'ha declarat.

# infra/produccio.tf
resource "servei_web" "gestor_tasques" {
  nom        = "gestor-tasques"
  imatge     = "registre.exemple.cat/gestor-tasques:${var.commit_sha}"
  repliques  = 4
  memoria_mb = 512
  cpu        = "500m"

  variables = {
    ENTORN     = "produccio"
    NIVELL_LOG = "warn"
  }

  # El secret NO és aquí: és una referència al gestor de secrets
  secrets = {
    BD_PASSWORD = "gestor-secrets://produccio/bd/password"
  }
}

Què hi guanya Git

Tot el que Git sap fer amb el codi s'aplica ara a la infraestructura:

# Quan i per què vam pujar a 4 rèpliques?
git log -p --follow infra/produccio.tf | grep -B 20 'repliques'

# Qui va canviar el límit de memòria?
git blame infra/produccio.tf

# Què canvia aquesta petició d'extracció respecte a producció?
git diff origin/main...HEAD -- infra/

# Tornar a la configuració d'abans de l'incident
git revert 4f8a2e6c

Aquest git blame sobre un fitxer d'infraestructura és una capacitat que fa deu anys no existia. La pregunta "per què aquest servei té 512 MB i no 256?" passa de ser arqueologia a ser una ordre.

El patró de pla i aplicació

La pràctica estàndard consisteix a calcular el pla de canvis a la petició d'extracció —perquè es pugui revisar— i aplicar-lo només després de la integració:

name: Infraestructura

on:
  pull_request:
    paths: ['infra/**']
  push:
    branches: [main]
    paths: ['infra/**']

jobs:
  pla:
    if: github.event_name == 'pull_request'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Calcular el pla
        run: |
          terraform init
          terraform plan -no-color -out=pla.bin | tee pla.txt
      - name: Publicar el pla com a comentari a la petició d'extracció
        run: ./eines/comentar-pla.sh pla.txt

  aplicar:
    if: github.event_name == 'push'
    runs-on: ubuntu-latest
    environment: produccio
    steps:
      - uses: actions/checkout@v4
      - name: Aplicar
        run: |
          terraform init
          terraform apply -auto-approve

Publicar el pla a la petició d'extracció és el que converteix la revisió d'infraestructura en una cosa real: qui revisa no llegeix un fitxer declaratiu i s'imagina l'efecte, llegeix l'efecte: "es destruiran 2 recursos, se'n crearà 1 i se'n modificaran 3".

Les migracions de base de dades

Menció a part, perquè és on el model es trenca si no es va amb compte. Els fitxers de migració es versionen com tota la resta:

migracions/
├── 001-crear-tasques.sql
├── 002-afegir-oculta.sql
└── 003-index-per-prioritat.sql

Però hi ha una asimetria fonamental: el codi es reverteix fàcilment; les dades, no. Un git revert del commit que va afegir una columna no torna les dades que aquella columna contenia.

D'aquí la regla d'or de les migracions, que condiciona com s'escriuen els commits:

Tota migració ha de ser compatible cap enrere durant almenys un desplegament. La versió antiga del codi ha de continuar funcionant amb l'esquema nou.

A la pràctica, això converteix "reanomenar una columna" en una seqüència de tres desplegaments:

  1. Afegir la columna nova; el codi escriu a totes dues i llegeix de la vella.
  2. Migrar les dades; el codi llegeix de la nova.
  3. Eliminar la columna vella.

Tres commits, tres desplegaments, i en cap moment un estat en què revertir el codi trenqui l'aplicació. És un exemple perfecte de com les restriccions d'operació es tradueixen en com es parteixen els commits.

  1. GitOps: el repositori com a estat desitjat

GitOps és un model operatiu amb una premissa molt concreta:

L'estat desitjat del sistema està declarat en un repositori Git. Un agent que corre dins del sistema compara contínuament l'estat real amb el declarat i reconcilia les diferències.

La diferència amb l'anterior és subtil però important: no és la canonada la que empeny els canvis al sistema, sinó un agent dins del sistema que estira d'ells.

flowchart TD
    subgraph git["Repositori d'estat"]
        A["entorns/produccio/<br/>servei.yaml<br/>imatge: :4f8a2e6"]
    end
    subgraph cluster["Sistema en producció"]
        B["Agent<br/>reconciliador"]
        C["Estat real"]
    end
    A -->|"l'agent observa<br/>(pull)"| B
    B -->|"compara"| C
    B -->|"aplica les diferències"| C
    C -.->|"informa de l'estat<br/>i de la deriva"| A

Model d'empenta enfront de model d'estirada

Empenta (la canonada desplega) Estirada (GitOps)
Qui aplica els canvis La canonada de CI Un agent dins del sistema
Credencials de producció Les té la canonada Les té l'agent; la canonada no
Deriva de configuració No es detecta Es detecta i es corregeix sola
Estat després d'un canvi manual Es queda així fins al pròxim desplegament Es reverteix automàticament
Superfície d'atac La canonada és un objectiu valuós Menor: res extern no entra al sistema
Auditoria Registres de la canonada L'historial de Git és l'auditoria
Desplegar Un push a la branca que dispara Un commit al repositori d'estat

L'avantatge de seguretat és contundent i mereix explicar-se: en el model d'empenta, el teu sistema de CI necessita credencials d'escriptura sobre producció. Això converteix la CI en l'objectiu més valuós de l'organització. A GitOps, la canonada només necessita permís per escriure en un repositori Git; l'agent, que viu dins del sistema, estira dels canvis. Res extern no té accés a producció.

Com es veu un desplegament a GitOps

Desplegar és literalment fer un commit:

# La canonada, després de construir i publicar l'artefacte:
git clone https://git.exemple.cat/gestor-tasques-estat.git
cd gestor-tasques-estat

# Actualitzar la referència de la imatge a l'entorn corresponent
sed -i "s|imatge: .*|imatge: registre.exemple.cat/gestor-tasques:$SHA|" \
  entorns/preproduccio/servei.yaml

git add entorns/preproduccio/servei.yaml
git commit -m "deploy: preproduccio a ${SHA:0:7}

Origen: gestor-tasques@$SHA
Tiquet: $(git -C ../gestor-tasques log -1 --format=%B "$SHA" | grep -oE 'GT-[0-9]+' | head -1)"

git push origin main

L'agent detecta el commit en qüestió de segons i reconcilia.

I la promoció a producció és igual de simple:

# Promocionar exactament el que hi ha a preproducció
IMATGE=$(grep 'imatge:' entorns/preproduccio/servei.yaml)
sed -i "s|imatge: .*|$IMATGE|" entorns/produccio/servei.yaml
git commit -am "deploy: promou a produccio allo validat a preproduccio"
git push

Fixa't en l'elegància: la promoció entre entorns és copiar una línia d'un fitxer a un altre. No es reconstrueix res, no es torna a etiquetar res. És la materialització literal de "construir una vegada, desplegar moltes".

Què implica per als permisos i les branques protegides

Si un commit al repositori d'estat desplega en producció, aleshores el control d'accés al repositori ÉS el control d'accés a producció. Això obliga a prendre's seriosament el que vam veure a la lliçó 07-06:

Mesura Per què és obligatòria aquí
Branca protegida a main del repositori d'estat Un push directe seria un desplegament sense revisió
Revisió obligatòria, i de més d'una persona per a producció És l'única barrera humana que queda
CODEOWNERS sobre entorns/produccio/ Qui aprova producció ha de ser qui correspon (10-01)
Prohibir l'enviament forçat Un push --force reescriuria l'historial d'auditoria
Commits signats (08-05) L'agent pot exigir signatura abans d'aplicar
Historial lineal L'auditoria s'ha de poder llegir sense ambigüitat

I una conseqüència organitzativa: la separació entre "repositori d'aplicació" i "repositori d'estat" deixa de ser una mania i es converteix en una decisió de seguretat. Al primer es desenvolupa; al segon es desplega. Cadascun amb els seus permisos.

La deriva

La propietat més valuosa de GitOps és la detecció de deriva. Si algú canvia alguna cosa a mà en producció —per "arreglar-ho ràpid" a les tres de la matinada—, l'agent detecta que l'estat real no coincideix amb el declarat i ho reverteix.

És incòmode la primera vegada que et passa. I és exactament el que ha de passar: obliga que tot canvi passi per Git, que és el que fa que el repositori continuï sent la font de veritat. Sense aquesta reconciliació, "tot és a Git" és una afirmació que es degrada silenciosament fins a ser falsa.

  1. Secrets en un món automatitzat

Reprenem la lliçó 08-05 amb el matís de l'automatització.

La regla, sense excepcions

Els secrets mai no van a Git. Ni xifrats amb una contrasenya feble, ni "només aquest de proves", ni "és un repositori privat".

La raó, que ja coneixes bé: a Git, esborrar no elimina. Un secret confirmat és a l'historial per sempre llevat que es reescrigui i es coordini amb tothom, i tot i així ja ha estat exposat a tothom que clonés.

Què va a Git i què no

Va a Git No va a Git
El nom de la variable (BD_PASSWORD) El seu valor
Una referència al secret (gestor-secrets://produccio/bd/password) El secret
La política de qui el pot llegir La clau que el desbloqueja
Certificats públics Claus privades
Secrets xifrats amb una clau que no és a Git (patró acceptable) Secrets xifrats amb una clau que sí que hi és

Com arriba el secret al procés

name: Desplegar

on:
  push:
    tags: ['v*.*.*']

jobs:
  desplegar:
    runs-on: ubuntu-latest
    environment: produccio
    permissions:
      id-token: write      # per a autenticació federada, sense secrets de llarga vida
      contents: read
    steps:
      - uses: actions/checkout@v4

      - name: Obtenir credencials temporals del gestor de secrets
        uses: proveidor/autenticar@v2
        with:
          rol: rol-desplegament-produccio

      - name: Desplegar
        run: ./infra/desplegar.sh produccio "${{ github.sha }}"
        env:
          # Injectat pel sistema, mai escrit en cap fitxer
          BD_PASSWORD: ${{ secrets.BD_PASSWORD_PRODUCCIO }}

Aquest bloc permissions: id-token: write mereix explicació, perquè és la pràctica moderna: en lloc de guardar una credencial de llarga vida al sistema de CI, la canonada obté un token de curta durada demostrant la seva identitat. Si algú roba aquest token, caduca en minuts i només serveix per a aquell repositori i aquella branca.

Quan cal versionar secrets: xifratge amb clau externa

Hi ha un patró acceptable, molt utilitzat a GitOps, on els secrets que viuen al repositori però xifrats amb una clau que no hi és:

# entorns/produccio/secrets.enc.yaml
apiVersion: v1
kind: Secret
metadata:
  name: gestor-tasques-bd
data:
  password: ENC[AES256_GCM,data:8fK2mN...,type:str]
sops:
  kms:
    - arn: 'arn:proveidor:kms:regio:compte:clau/id-de-la-clau'

El valor xifrat és a Git; la clau per desxifrar-lo viu en un servei de gestió de claus al qual només l'agent de desplegament té accés. Avantatges: els secrets es versionen, es revisen i s'auditen com tota la resta. Requisit: que la clau mai no entri al repositori, i que rotar-la sigui un procediment provat.

Defensa en profunditat

Com a la lliçó 08-05, tres capes:

- name: Buscar secrets a la petició d'extracció
  run: |
    # Anàlisi de l'històric complet de la branca
    gitleaks detect --source . --log-opts="origin/main..HEAD" --verbose

- name: Comprovar que no hi ha fitxers de configuració amb credencials
  run: |
    git diff --name-only origin/main...HEAD | while read -r f; do
      case "$f" in
        *.env|*.pem|*.key|*credencials*|*secret*)
          echo "ERROR: $f no hauria de ser al repositori"; exit 1 ;;
      esac
    done

Més el hook de servidor (06-01) i el .gitignore (08-03). I el procediment per si tot i així passa, que comença sempre pel mateix: revocar el secret primer, netejar l'historial després.

  1. Versionatge i changelog automàtics

Aquí es cobra la inversió de la lliçó 08-01. Els Conventional Commits no eren només una convenció estètica: són dades estructurades que una màquina pot llegir.

Dels commits a la versió

Tipus de commit Efecte en SemVer (05-05)
fix: Incrementa PATCH (1.3.41.3.5)
feat: Incrementa MINOR (1.3.41.4.0)
feat!: o peu BREAKING CHANGE: Incrementa MAJOR (1.3.42.0.0)
docs:, chore:, test:, refactor:, style: No incrementa res
# Què hi ha des de l'última versió publicada
ULTIMA=$(git describe --tags --abbrev=0)
git log "$ULTIMA"..main --format='%s'
feat(tasques): GT-231 filtrar les tasques ocultes del llistat
fix(interficie): GT-238 corregir el comptador després d'eliminar una tasca
docs: GT-240 documentar el filtre d'ocultes
chore: actualitzar dependències

Un feat i un fix sense canvis incompatibles: la versió següent és 1.4.0.

Calcular-ho automàticament

#!/bin/bash
# eines/seguent-versio.sh
set -euo pipefail

ULTIMA=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
COMMITS=$(git log "$ULTIMA"..HEAD --format='%s%n%b')

IFS=. read -r MAJOR MENOR PEDAC <<< "${ULTIMA#v}"

if echo "$COMMITS" | grep -qE '^(BREAKING CHANGE|BREAKING-CHANGE):|^[a-z]+(\(.+\))?!:'; then
  MAJOR=$((MAJOR + 1)); MENOR=0; PEDAC=0
elif echo "$COMMITS" | grep -qE '^feat(\(.+\))?!?:'; then
  MENOR=$((MENOR + 1)); PEDAC=0
elif echo "$COMMITS" | grep -qE '^fix(\(.+\))?!?:'; then
  PEDAC=$((PEDAC + 1))
else
  echo "Sense canvis que justifiquin una versió nova." >&2
  exit 1
fi

echo "v$MAJOR.$MENOR.$PEDAC"

Generar el changelog

#!/bin/bash
# eines/changelog.sh
ULTIMA=$(git describe --tags --abbrev=0)
NOVA=$1

{
  echo "## $NOVA — $(date +%Y-%m-%d)"
  echo

  echo "### Novetats"
  git log "$ULTIMA"..HEAD --format='%s' \
    | grep -E '^feat' \
    | sed -E 's/^feat(\(([^)]+)\))?!?: /- (\2) /' \
    | sed 's/- () /- /'
  echo

  echo "### Correccions"
  git log "$ULTIMA"..HEAD --format='%s' \
    | grep -E '^fix' \
    | sed -E 's/^fix(\(([^)]+)\))?!?: /- (\2) /' \
    | sed 's/- () /- /'
  echo

  if git log "$ULTIMA"..HEAD --format='%B' | grep -q 'BREAKING CHANGE:'; then
    echo "### Canvis incompatibles"
    git log "$ULTIMA"..HEAD --format='%B' \
      | grep -A 5 'BREAKING CHANGE:' | sed 's/^/  /'
    echo
  fi

  echo "### Detalls"
  echo "- Commits: $(git rev-list --count "$ULTIMA"..HEAD)"
  echo "- Rang: \`$ULTIMA..$NOVA\`"
} > /tmp/changelog-nou.md

# Anteposar al fitxer existent
cat /tmp/changelog-nou.md CHANGELOG.md > /tmp/c && mv /tmp/c CHANGELOG.md

La canonada de publicació completa

name: Publicar versió

on:
  workflow_dispatch:

jobs:
  publicar:
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0        # imprescindible: describe necessita les etiquetes

      - name: Calcular la versió
        id: version
        run: echo "v=$(./eines/seguent-versio.sh)" >> "$GITHUB_OUTPUT"

      - name: Generar el changelog
        run: ./eines/changelog.sh "${{ steps.version.outputs.v }}"

      - name: Confirmar el changelog i crear l'etiqueta anotada
        run: |
          git config user.name  "Canonada de publicació"
          git config user.email "[email protected]"

          git add CHANGELOG.md
          git commit -m "chore(release): ${{ steps.version.outputs.v }}"

          git tag -a "${{ steps.version.outputs.v }}" \
            -m "$(sed -n '/^## /,/^## /p' CHANGELOG.md | head -n -1)"

          git push origin main
          git push origin "${{ steps.version.outputs.v }}"

Aquest git push origin "$VERSION" del final és el que dispara la canonada de desplegament a producció de l'apartat 3. El circuit es tanca: dels missatges de commit surt la versió, de la versió surt l'etiqueta, i de l'etiqueta surt el desplegament.

És la millor justificació possible per a la disciplina del mòdul 8. Un missatge mal escrit ja no és només lleig: produeix una versió incorrecta i un changelog que menteix.

  1. Reversió en producció

Alguna cosa falla. Què es fa?

Les dues opcions

Opció A: git revert i tornar a desplegar.

git revert 4f8a2e6c
git push origin main
# La canonada construeix i desplega el commit nou

Opció B: redesplegar l'artefacte anterior.

./infra/desplegar.sh produccio 9e2f7a4c
# o, amb etiquetes:
./infra/desplegar.sh produccio v1.3.0

Per què gairebé sempre és la B

git revert + desplegar Redesplegar l'artefacte anterior
Temps fins a la recuperació Minuts: cal construir, provar i desplegar Segons: l'artefacte ja existeix
Risc El commit de reversió és codi nou sense provar en producció Zero: aquell artefacte ja va estar funcionant
Pot fallar la reversió? : pot haver-hi conflictes, o revertir de més No
Estat de l'historial Net i explícit main continua contenint el canvi dolent
Dependència de la CI Total: si la CI està caiguda, no pots revertir Cap
Interacció amb migracions Perillosa si la migració no és reversible Igual de perillosa, però més ràpida

La fila decisiva és la segona: un git revert genera un commit que mai no ha estat en producció. Estàs desplegant codi nou per arreglar un incident. Redesplegar l'artefacte anterior és tornar a un estat que saps amb certesa que funcionava, perquè va estar funcionant fins fa deu minuts.

I la primera fila és la que decideix a la pràctica: durant un incident, la diferència entre segons i minuts és la diferència entre un ensurt i una postmortem.

El procediment correcte

flowchart TD
    A["Alerta:<br/>producció falla"] --> B["1. REDESPLEGAR<br/>la versió anterior"]
    B --> C["Servei restablert"]
    C --> D["2. Investigar<br/>sense pressa"]
    D --> E{"S'arregla<br/>ràpid?"}
    E -->|sí| F["3a. Corregir cap endavant:<br/>commit fix + desplegar"]
    E -->|no| G["3b. git revert a main,<br/>investigar amb calma"]
    F --> H["4. Postmortem"]
    G --> H

Primer es restableix el servei. Després s'arregla el codi. Són dues activitats diferents i barrejar-les allarga la incidència.

I el pas 3 no és opcional: si només redesplegues la versió anterior, main continua contenint el canvi dolent, i el pròxim desplegament de qualsevol persona el tornarà a portar a producció. Cal deixar main en un estat sa, ja sigui amb un revert o amb una correcció.

Com es veu a GitOps

Encara més simple, perquè revertir el desplegament és revertir un commit del repositori d'estat:

cd gestor-tasques-estat
git revert HEAD          # desfà el commit que va canviar la imatge
git push origin main
# L'agent reconcilia en segons

Aquí sí que és un git revert, però fixa't en la diferència: es reverteix el commit de desplegament, no el commit de codi. El resultat és que el fitxer d'estat torna a apuntar a l'artefacte anterior, que ja existeix i ja funcionava. És l'opció B expressada com un commit, amb la traçabilitat de l'opció A.

Preparar la reversió abans de necessitar-la

#!/bin/bash
# eines/revertir-produccio.sh
set -euo pipefail

ACTUAL=$(kubectl get deployment gestor-tasques \
  -o jsonpath='{.spec.template.spec.containers[0].image}' | cut -d: -f2)

# L'etiqueta anterior a la que correspon al que està desplegat
ETIQUETA_ACTUAL=$(git tag --points-at "$ACTUAL" | head -1)
ANTERIOR=$(git tag --sort=-v:refname | grep -A1 "^$ETIQUETA_ACTUAL$" | tail -1)
SHA_ANTERIOR=$(git rev-list -n1 "$ANTERIOR")

echo "Desplegat ara:    $ACTUAL ($ETIQUETA_ACTUAL)"
echo "Es revertirà a:   $SHA_ANTERIOR ($ANTERIOR)"
echo
echo "Canvis que es desfaran:"
git log --oneline "$SHA_ANTERIOR".."$ACTUAL"
echo
read -rp "Ho confirmes? (escriu SI): " R
[ "$R" = "SI" ] || exit 1

./infra/desplegar.sh produccio "$SHA_ANTERIOR"

Un guió així, provat i amb permisos clars, és la diferència entre una reversió de trenta segons i una de vint minuts buscant a l'historial del terminal a les tres de la matinada. Escriu-lo i prova'l un dimarts al matí, no durant un incident.

Quan el revert sí que és el correcte

  • La fallada no és urgent: un error visual, alguna cosa que afecta poca gent.
  • L'artefacte anterior no és desplegable: hi va haver una migració de dades irreversible pel mig.
  • El canvi dolent es detecta abans d'arribar a producció, a preproducció.
  • Es vol que l'historial reflecteixi explícitament que aquell canvi es va desfer.

  1. Que la canonada no depengui d'un humà

Recopilació final de pràctiques, cadascuna amb el seu motiu.

  1. Tot al repositori, res a la interfície

Si la canonada està definida en un fitxer YAML versionat, els seus canvis es revisen, es reverteixen i tenen historial. Si està configurada fent clic en un web, no hi ha historial, no hi ha revisió i ningú no sap qui va canviar què.

  1. Fixar les versions de tot

# Fràgil: "v4" pot canviar sota els teus peus
- uses: actions/checkout@v4

# Reproduïble: un hash concret
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11

És exactament el mateix principi que regeix tot el curs: identificar per contingut, no per nom mòbil. Una etiqueta es pot moure; un hash, no.

  1. Idempotència

Executar la canonada dues vegades sobre el mateix commit ha de produir el mateix resultat. Si la segona execució falla perquè "l'etiqueta ja existeix" o "l'artefacte ja està publicat", la canonada no és idempotent i qualsevol reintent es converteix en una intervenció manual.

  1. Que falli ràpid i amb un missatge clar

# Dolent
terraform apply -auto-approve

# Bo
if ! git merge-base --is-ancestor "$COMMIT" origin/main; then
  echo "ERROR: el commit $COMMIT no és a main."
  echo "Només es desplega el que està integrat. Integra la petició primer."
  exit 1
fi

El missatge ha de dir què ha passat i què fer. Un error que només diu exit 1 obliga a llegir el guió.

  1. Registrar el perquè, no només el què

git commit -m "deploy: produccio a v1.4.0

Commit:  4f8a2e6c9d3b1a5e7f2c8b0d4a6e9f1c3b5d7a0e
Tiquets: GT-231, GT-238
Aprovat per: Ana Ferrer
Execució: https://ci.exemple.cat/execucions/8842"

Un desplegament anotat així s'investiga en un minut sis mesos després.

  1. Comprovacions prèvies explícites

Abans de tocar producció, verificar:

# El commit és a main?
git merge-base --is-ancestor "$COMMIT" origin/main

# La còpia de treball està neta?
git describe --always --dirty | grep -q -- '-dirty$' && exit 1

# L'etiqueta és anotada i està signada?
[ "$(git cat-file -t "$ETIQUETA")" = "tag" ] || exit 1
git tag --verify "$ETIQUETA"

# Existeix l'artefacte?
docker manifest inspect "registre.exemple.cat/gestor-tasques:$COMMIT" >/dev/null

  1. Les mateixes ordres en local i a la canonada

Si la CI executa npm test i tu també, reproduir una fallada és trivial. Si la CI executa dotze passos escrits al YAML que no existeixen en cap guió, no pots reproduir res. Extreu la lògica a guions del repositori i que la canonada només els invoqui.

  1. Branques protegides de debò

La protecció de main (07-06) és l'última barrera. Amb desplegament continu, un push --force a main pot desplegar qualsevol cosa. Prohibit, sense excepcions ni "comptes d'emergència".

Errors Habituals i Consells

Error 1: reconstruir l'artefacte a cada entorn. El que proves deixa de ser el que desplegues. Construir una vegada, desplegar moltes; el que canvia entre entorns és la configuració injectada.

Error 2: fer servir latest com a identificador. No identifica res. El hash complet del commit com a etiqueta canònica, i els noms llegibles com a àlies.

Error 3: git revert com a primera reacció a un incident. Desplega codi nou sense provar per arreglar un problema urgent. Primer redesplegar l'anterior, després arreglar main.

Error 4: oblidar fetch-depth: 0 quan la canonada fa servir git describe o compara branques. És l'error de la lliçó 10-04: amb un clon superficial no hi ha etiquetes ni base de fusió. I en repositoris grans, fetch-depth: 0 + filter: blob:none.

Error 5: secrets al repositori "perquè és privat". Un repositori privat té desenes de clons, rèpliques, còpies de seguretat i sistemes de CI amb accés. La regla no admet matisos.

Error 6: etiquetes lleugeres per a producció. Sense autor, sense data, sense missatge, sense signatura. I es poden moure amb git tag -f, cosa que un objecte d'etiqueta anotada i signada fa molt més difícil de dissimular.

Error 7: canviar producció a mà. Crea deriva i el repositori deixa de ser la font de veritat. Si un canvi urgent és inevitable, el commit corresponent es fa immediatament després, sense excepció.

Error 8: migracions de base de dades no compatibles cap enrere. Fan impossible la reversió. És la restricció operativa que més condiciona com es parteixen els commits.

Consell 1: exposa /version a l'aplicació. Amb el hash del commit. Respon a "què hi ha realment a producció?" sense accés al sistema de desplegament.

Consell 2: escriu el guió de reversió abans de necessitar-lo. I prova'l. Un dimarts al matí.

Consell 3: fes servir git describe --dirty per detectar construccions no reproduïbles. El sufix -dirty significa que aquell artefacte no es pot reconstruir des del repositori: rebutja'l.

Consell 4: separa el repositori d'aplicació del d'estat. Permisos diferents, historial d'auditoria separat, i menys possibilitats que un canvi de codi toqui producció per accident.

Consell 5: un plafó que compari el que està desplegat amb main. git rev-list --count $DESPLEGAT..origin/main diu quants commits esperen. És la mètrica de "quant valor està parat".

Consell 6: signa les etiquetes de producció. Així crear una versió requereix una clau privada, no només permisos d'escriptura al repositori.

Exercicis

Exercici 1: dissenyar la canonada de desplegament de gestor-tasques

L'equip vol:

  • Cada integració a main desplega automàticament a preproducció.
  • Producció es desplega només en crear una etiqueta anotada i signada vX.Y.Z.
  • L'artefacte es construeix una vegada i es reutilitza.
  • Ha de ser possible revertir producció en menys d'un minut.

Escriu la canonada (o canonades) en YAML, indicant quines comprovacions de Git inclous a cada punt i per què. Explica el flux complet d'un canvi, des de la petició d'extracció fins a producció.

Exercici 2: traçabilitat inversa des d'un incident

Són les 03:12. Alerta: gestor-tasques retorna error 500 en producció. L'única cosa que saps és que el contenidor desplegat és:

registre.exemple.cat/gestor-tasques:4f8a2e6c9d3b1a5e7f2c8b0d4a6e9f1c3b5d7a0e

Escriu la seqüència d'ordres per a:

  1. Identificar quina versió i quins canvis conté.
  2. Determinar si el problema va entrar amb l'últim desplegament.
  3. Trobar el commit sospitós.
  4. Revertir producció.
  5. Deixar main en un estat sa.

Explica en quin ordre ho faries i per què.

Exercici 3: detectar problemes en una canonada aliena

Revisa aquesta canonada i assenyala tots els problemes, ordenats per gravetat, explicant què pot fallar i com ho corregiries:

name: Desplegar

on:
  push:
    branches: [main]

jobs:
  desplegar:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@main

      - name: Construir
        run: |
          docker build -t gestor-tasques:latest .
          docker push registre.exemple.cat/gestor-tasques:latest

      - name: Etiquetar la versió
        run: |
          VERSION=$(cat VERSION)
          git tag $VERSION
          git push origin $VERSION

      - name: Desplegar a producció
        run: |
          ssh [email protected] \
            "docker pull registre.exemple.cat/gestor-tasques:latest && \
             DB_PASS=Estiu2026! docker compose up -d"

      - name: Notificar
        run: curl -X POST https://chat.exemple.cat/hook -d "Desplegat"

Solucions

Solució 1

Canonada 1: integració (ja existeix, de la lliçó 07-06). Proves i linter a cada petició d'extracció, main protegida.

Canonada 2: construcció i desplegament a preproducció.

name: Construir i desplegar a preproducció

on:
  push:
    branches: [main]

jobs:
  construir:
    runs-on: ubuntu-latest
    outputs:
      sha: ${{ steps.info.outputs.sha }}
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0        # describe necessita les etiquetes

      - id: info
        name: Recopilar la procedència
        run: |
          SHA=$(git rev-parse HEAD)
          DESC=$(git describe --tags --always --dirty)

          # COMPROVACIÓ: res de construccions no reproduïbles
          case "$DESC" in
            *-dirty) echo "ERROR: còpia de treball bruta"; exit 1 ;;
          esac

          echo "sha=$SHA"   >> "$GITHUB_OUTPUT"
          echo "desc=$DESC" >> "$GITHUB_OUTPUT"

      - name: Construir l'artefacte UNA sola vegada
        run: |
          docker build \
            --build-arg COMMIT_SHA="${{ steps.info.outputs.sha }}" \
            --build-arg VERSION="${{ steps.info.outputs.desc }}" \
            --build-arg DATA_CONSTRUCCIO="$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
            -t "registre.exemple.cat/gestor-tasques:${{ steps.info.outputs.sha }}" .

          docker push "registre.exemple.cat/gestor-tasques:${{ steps.info.outputs.sha }}"

  preproduccio:
    needs: construir
    runs-on: ubuntu-latest
    environment: preproduccio
    steps:
      - name: Desplegar l'artefacte ja construït
        run: ./infra/desplegar.sh preproduccio "${{ needs.construir.outputs.sha }}"

      - name: Comprovació de vida
        run: |
          sleep 10
          DESPLEGAT=$(curl -sf https://pre.exemple.cat/version | jq -r .commit)
          [ "$DESPLEGAT" = "${{ needs.construir.outputs.sha }}" ] \
            || { echo "ERROR: s'esperava un altre commit a /version"; exit 1; }

Comprovacions incloses i per què:

Comprovació Motiu
fetch-depth: 0 git describe necessita les etiquetes; un clon superficial les omet (10-04)
Rebutjar -dirty Un artefacte construït des d'una còpia bruta no és reproduïble
Etiquetar amb el hash complet Identificador canònic i no ambigu
Incrustar procedència com a LABEL i ENV Permet el /version i l'auditoria de l'artefacte
Comprovació de vida contra /version Verifica que es va desplegar el que es creia, no una cosa de la memòria cau

Canonada 3: producció per etiqueta.

name: Desplegar a producció

on:
  push:
    tags: ['v*.*.*']

jobs:
  desplegar:
    runs-on: ubuntu-latest
    environment: produccio        # amb revisors obligatoris
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Verificacions prèvies
        id: verif
        run: |
          ETIQUETA="${{ github.ref_name }}"

          # 1. Ha de ser anotada, no lleugera
          [ "$(git cat-file -t "$ETIQUETA")" = "tag" ] \
            || { echo "ERROR: $ETIQUETA és lleugera. Producció exigeix anotada."; exit 1; }

          # 2. Ha d'estar signada
          git tag --verify "$ETIQUETA" \
            || { echo "ERROR: signatura no vàlida a $ETIQUETA"; exit 1; }

          # 3. Ha de ser a main
          git fetch origin main
          git merge-base --is-ancestor "$ETIQUETA" origin/main \
            || { echo "ERROR: $ETIQUETA no és a main"; exit 1; }

          # 4. L'artefacte ha d'existir JA (no es reconstrueix)
          SHA=$(git rev-list -n1 "$ETIQUETA")
          docker manifest inspect "registre.exemple.cat/gestor-tasques:$SHA" >/dev/null \
            || { echo "ERROR: no hi ha artefacte per a $SHA"; exit 1; }

          echo "sha=$SHA" >> "$GITHUB_OUTPUT"

      - name: Desplegar
        run: ./infra/desplegar.sh produccio "${{ steps.verif.outputs.sha }}"

      - name: Verificar i registrar
        run: |
          sleep 15
          DESPLEGAT=$(curl -sf https://gestor-tasques.exemple.cat/version | jq -r .commit)
          [ "$DESPLEGAT" = "${{ steps.verif.outputs.sha }}" ] || exit 1

          echo "Producció: ${{ github.ref_name }} (${{ steps.verif.outputs.sha }})"
          git log --oneline "$(git describe --tags --abbrev=0 '${{ github.ref_name }}^')"..'${{ github.ref_name }}'

Canonada 4: reversió ràpida (el requisit de "menys d'un minut").

name: Revertir producció

on:
  workflow_dispatch:
    inputs:
      etiqueta:
        description: 'Etiqueta a la qual revertir (p. ex. v1.3.0)'
        required: true

jobs:
  revertir:
    runs-on: ubuntu-latest
    environment: produccio-urgent    # sense revisors: és una emergència
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Desplegar l'artefacte EXISTENT d'aquesta etiqueta
        run: |
          SHA=$(git rev-list -n1 "${{ inputs.etiqueta }}")
          docker manifest inspect "registre.exemple.cat/gestor-tasques:$SHA" >/dev/null \
            || { echo "ERROR: no existeix artefacte per a aquesta etiqueta"; exit 1; }
          ./infra/desplegar.sh produccio "$SHA"

És ràpida precisament perquè no construeix res: l'artefacte ja existeix des del dia en què aquell commit va entrar a main.

Flux complet d'un canvi:

  1. L'Ana crea GT-231-filtre-ocultes, treballa, obre la petició d'extracció.
  2. La CI executa proves i linter (07-06). En Bruno revisa i aprova.
  3. S'integra a main (branca protegida, condensat).
  4. La canonada 2 construeix l'artefacte :4f8a2e6... una vegada i desplega a preproducció.
  5. L'equip valida a preproducció.
  6. Quan toca publicar: ./eines/seguent-versio.sh calcula v1.4.0 des dels Conventional Commits, es genera el changelog i es crea git tag -a -s v1.4.0.
  7. El push de l'etiqueta dispara la canonada 3, que verifica i desplega l'artefacte ja construït al pas 4.
  8. Si alguna cosa falla: canonada 4 amb v1.3.0, en segons. Després, git revert a main amb calma.

Solució 2

L'ordre importa moltíssim, i és contraintuïtiu: primer es restableix el servei, després s'investiga. Però amb una excepció: els passos 1 a 3 costen segons i donen la informació necessària per revertir amb criteri.

# ========== FASE 1: SITUAR-SE (30 segons) ==========

# 1. Quin commit és i quina versió?
git show --stat 4f8a2e6c
git tag --points-at 4f8a2e6c
git describe --tags 4f8a2e6c
v1.4.0
# 2. Què ha canviat respecte a la versió anterior?
ANTERIOR=$(git describe --tags --abbrev=0 4f8a2e6c^)
echo "Versió anterior: $ANTERIOR"
git log --oneline "$ANTERIOR"..4f8a2e6c
Versió anterior: v1.3.0
4f8a2e6 feat(tasques): GT-231 filtrar les tasques ocultes del llistat
8a1f6c3 fix(interficie): GT-238 corregir el comptador després d'eliminar
2e9f4c7 chore: actualitzar dependències
# 3. Quan es va desplegar? Coincideix amb l'inici de l'alerta?
git log -1 --format='%ci' 4f8a2e6c
git for-each-ref --format='%(refname:short) %(creatordate:iso)' refs/tags/v1.4.0

Si l'etiqueta es va crear a les 03:05 i l'alerta va saltar a les 03:12, el problema va entrar amb aquest desplegament. Aquesta correlació és el que justifica revertir en lloc d'investigar.

# ========== FASE 2: RESTABLIR (immediat) ==========

# 4. Revertir a la versió anterior, JA
./eines/revertir-produccio.sh
# o directament:
./infra/desplegar.sh produccio "$(git rev-list -n1 v1.3.0)"

# 5. Verificar
curl -sf https://gestor-tasques.exemple.cat/version | jq

Per què aquí i no després: cada minut d'investigació és un minut de servei caigut. L'artefacte de v1.3.0 va estar funcionant fins fa vint minuts: és l'opció de risc zero.

Per què NO fer git revert ara: generaria un commit nou, caldria construir-lo (minuts), provar-lo i desplegar-lo, i aquell artefacte mai no ha estat en producció. És exactament el que no es vol durant un incident.

# ========== FASE 3: INVESTIGAR (amb el servei restablert) ==========

# 6. Només tres commits sospitosos. Veure què fa cadascun
git log -p "$ANTERIOR"..4f8a2e6c

# 7. Si no és evident, bisect entre les dues versions (lliçó 06-02)
git bisect start 4f8a2e6c v1.3.0
git bisect run ./proves/reproduir-error-500.sh

# 8. Quan aparegui el culpable, entendre la intenció (lliçó 06-03)
git log -1 --format=%B <commit-culpable>
git blame -L 40,60 app.js

Amb només tres commits, bisect és exagerat; es faria servir en un rang de cinquanta. Aquí, llegir els tres diffs és més ràpid.

# ========== FASE 4: DEIXAR main SA ==========

# 9a. Si la causa és clara i és petita: corregir cap endavant
git switch -c GT-245-corregir-error-500 main
# ... arreglar ...
git commit -m "fix(tasques): GT-245 corregeix l'error 500 en filtrar tasques ocultes

El filtre introduït a GT-231 no contemplava el cas de tasques sense
la marca 'oculta' definida, cosa que provocava una excepció no capturada
a renderitzaTasques().

S'afegeix una comprovació explícita i una prova de regressió.

Refs: GT-231, GT-245"
# Petició d'extracció, revisió, integració, versió nova v1.4.1

# 9b. Si no està clara o hi ha pressa: revertir el commit problemàtic
git revert 4f8a2e6c
git push origin main

Per què el pas 9 és obligatori: si només es va redesplegar v1.3.0, main continua contenint la fallada. La pròxima persona que publiqui una versió la portarà un altre cop a producció sense saber-ho. La reversió del desplegament restableix el servei; només el pas 9 arregla el problema.

# ========== FASE 5: APRENDRE ==========
# Per què les proves no ho van detectar? -> afegir la prova que falta.
# Per què no ho vam veure a preproducció? -> falten dades representatives?
# Quant va trigar la reversió? -> si va ser molt, millorar el guió.

Solució 3

Vuit problemes, de més greu a menys.

1. CRÍTIC — Secret en text clar al guió.

DB_PASS=Estiu2026! docker compose up -d

La contrasenya de la base de dades de producció és en un fitxer versionat, a l'historial per sempre, visible per a tothom qui cloni, i probablement als registres de cada execució de la canonada. Viola la lliçó 08-05 de la manera més directa possible.

Correcció: revocar la contrasenya ara mateix (abans que res), desar-la al gestor de secrets, injectar-la com a variable d'entorn del sistema de CI i, si el repositori és públic o ha estat clonat, netejar l'historial amb git filter-repo (08-05).

env:
  DB_PASS: ${{ secrets.BD_PASSWORD_PRODUCCIO }}

2. CRÍTIC — Desplegament a producció sense cap barrera.

Qualsevol integració a main va directa a producció, sense environment, sense aprovació, sense comprovacions prèvies, sense preproducció. Un canvi d'una línia mal revisat arriba als usuaris en dos minuts.

Correcció: separar en dues canonades (integració a preproducció per branca; producció per etiqueta anotada i signada), i declarar environment: produccio amb revisors obligatoris.

3. GREU — latest com a identificador.

docker build -t gestor-tasques:latest .

No identifica res. Impossible saber què s'està executant, impossible revertir a una versió concreta, impossible reproduir un problema. I una condició de cursa evident: si dues integracions passen seguides, la segona pot sobreescriure latest mentre la primera desplega.

Correcció: etiquetar amb el hash complet del commit i fer servir els noms llegibles només com a àlies.

4. GREU — Es reconstrueix a cada desplegament i no hi ha separació construcció/desplegament.

No hi ha entorn de preproducció, així que el que arriba a producció no s'ha provat mai desplegat. I com que es construeix aquí mateix, no hi ha manera de garantir que l'artefacte de producció sigui el mateix que va passar les proves.

Correcció: construir una vegada amb el hash, desplegar aquest mateix artefacte a preproducció i després a producció.

5. GREU — La versió surt d'un fitxer VERSION i l'etiqueta és lleugera.

VERSION=$(cat VERSION)
git tag $VERSION

Tres problemes en tres línies:

  • Etiqueta lleugera (git tag sense -a): sense autor, sense data, sense missatge, sense signatura.
  • Falla si la versió no ha canviat: git tag sobre una etiqueta existent dona error i trenca la canonada. No és idempotent.
  • El fitxer VERSION s'actualitza a mà: algú se n'oblidarà.

Correcció: calcular la versió des dels Conventional Commits (apartat 8) i crear etiqueta anotada i signada:

VERSION=$(./eines/seguent-versio.sh)
git tag -a -s "$VERSION" -m "Versió $VERSION"

6. MITJÀ — actions/checkout@main.

Referència a una branca mòbil: el que la canonada executa pot canviar sense que ningú ho decideixi. És un risc de cadena de subministrament i de reproductibilitat.

Correcció: fixar per hash. I afegir fetch-depth: 0 si es faran servir etiquetes o comparacions.

7. MITJÀ — Sense comprovacions ni verificació posterior.

No comprova res abans de tocar producció, i no verifica res després. La notificació diu "Desplegat" passi el que passi, perquè el curl s'executa igualment: no hi ha if: success(), ni es comprova que el servei respongui.

Correcció: comprovacions prèvies (commit a main, còpia neta, artefacte existent) i una comprovació de vida contra /version que confirmi el hash desplegat.

8. MENOR — ssh amb docker compose com a mecanisme de desplegament.

Sense control de versions de l'estat, sense reversió, sense desplegament progressiu, sense registre. Si la connexió SSH es talla a mitges, l'estat queda indeterminat.

Correcció: un guió de desplegament versionat al repositori (infra/desplegar.sh) que sigui idempotent i registri el que fa; i a mitjà termini, valorar GitOps (apartat 6), que a més elimina la necessitat que la canonada tingui credencials de producció.

Resum de la correcció: aquesta canonada fa tot en una sola tasca sense barreres, amb un secret en clar i sense identificació reproduïble de l'artefacte. La reescriptura correcta és la de l'exercici 1: separar construcció de desplegament, identificar pel hash, exigir etiqueta anotada i signada per a producció, injectar secrets des del gestor, i afegir verificació abans i després.

Conclusió

Git deixa de ser una eina de desenvolupadors i passa a ser la peça central de l'operació.

  • El repositori es converteix en la font única de veritat: no només codi, també infraestructura, configuració per entorn, migracions i la definició de les mateixes canonades. La regla: si alguna cosa pot trencar producció, ha d'estar a Git; i si és a Git, no s'ha de canviar fora de Git. Amb una excepció absoluta: els secrets.
  • Integració, lliurament i desplegament continus es distingeixen per on és el botó humà: la integració garanteix que el que entra funciona, el lliurament que el que hi ha a main es pot desplegar, i el desplegament que està desplegat. Treure el botó és una decisió de confiança que exigeix proves fiables, reversió en minuts i canvis petits.
  • Un desplegament es dispara per branca (preproducció), per etiqueta anotada i signada (producció, tancant la 05-05) o per aprovació manual sobre un commit. Les verificacions que importen són de Git pur: git cat-file -t per exigir etiqueta anotada, git tag --verify per a la signatura, i git merge-base --is-ancestor per exigir que sigui a main.
  • L'artefacte s'identifica pel hash complet del commit, i es construeix una vegada i es desplega moltes. És la mateixa idea que sosté tot Git: identificar per contingut, no per un nom que es mou. D'aquí surt la traçabilitat completa —del contenidor al tiquet en cinc ordres— i el git describe --dirty que detecta construccions no reproduïbles.
  • La infraestructura com a codi dona als servidors tot el que Git sap fer: historial, blame, revisió, revert. I les migracions de base de dades imposen la restricció que més condiciona els commits: han de ser compatibles cap enrere durant almenys un desplegament, perquè el codi es reverteix i les dades no.
  • GitOps inverteix el sentit: un agent dins del sistema estira de l'estat declarat i reconcilia. Guanya en seguretat (la canonada no té credencials de producció), detecta i corregeix la deriva, i converteix l'historial de Git en el registre d'auditoria. A canvi, el control d'accés al repositori és el control d'accés a producció, i les branques protegides deixen de ser una recomanació.
  • Els secrets s'injecten, mai no es versionen; i la pràctica moderna substitueix les credencials de llarga vida per tokens de curta durada obtinguts per identitat federada.
  • El versionatge i el changelog automàtics cobren la inversió de la 08-01: els Conventional Commits són dades estructurades de les quals surten la versió SemVer, el changelog i —via etiqueta— el desplegament.
  • I la reversió en producció és gairebé sempre redesplegar l'artefacte anterior, no git revert: l'artefacte anterior va estar funcionant fa deu minuts i està llest en segons, mentre que un revert genera codi nou sense provar que cal construir i desplegar. Primer es restableix el servei; després es deixa main sa, cosa que no és opcional.

La idea que travessa tota la lliçó:

En una organització amb desplegament automatitzat, git push deixa de significar "he desat la meva feina" i passa a significar "això arribarà als usuaris". Tot el rigor del curs —commits atòmics, missatges que expliquen el perquè, historial net, etiquetes anotades, branques protegides— deixa de ser una virtut professional i es converteix en un requisit operatiu.

El que ve

Ja saps fer servir Git en tots els contextos que existeixen avui: sol, en equip, en projectes enormes i en producció automatitzada. Queda una última pregunta, que és la més honesta que es pot fer al final d'un curs tècnic: quant d'això continuarà sent cert d'aquí a deu anys?

Git no està quiet. Hi ha coses que ja existeixen i pots fer servir avui encara que gairebé ningú no les conegui: un format de referències nou que substitueix refs/ i packed-refs, un git que pot funcionar amb SHA-256 en lloc de SHA-1, els clons parcials i l'índex dispers que acabem de veure, i la consolidació de switch i restore enfront del vell checkout. I hi ha coses que són tendència o especulació, que convé distingir amb claredat de les anteriors.

També hi ha alguna cosa que gairebé amb seguretat no canviarà, i és precisament el que vas aprendre al mòdul 1: el model de dades adreçable per contingut, el graf dirigit acíclic de commits i la naturalesa distribuïda. Per això el que portes après no caduca.

La lliçó 10-06: El Futur de Git tanca el curs: què està madur i què és promesa, cap a on apunten les eines, com continuar aprenent pel teu compte, i una recapitulació completa del camí recorregut.

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