La lliçó anterior va acabar amb una pregunta incòmoda. Has construït un pipeline d'integració contínua que es dispara amb cada push, que etiqueta les imatges amb el SHA del commit i que publica a Artifact Registry tot el que passa les proves. I des de quin repositori es dispara exactament?

Perquè l'inventari real del codi d'AlpinaShop en aquest moment és el següent. El catàleg web és en un repositori Git local al portàtil d'en Dani, amb un remot que apunta a un servidor que l'empresa va deixar de mantenir fa dos anys; funciona perquè ningú no fa git push. Els startup scripts de les plantilles d'instància del MIG estan enganxats al camp de metadades de la plantilla, i enlloc més. Els manifestos de Kubernetes del namespace tienda són en una carpeta compartida de Drive anomenada "GKE definitiu (v3) FINAL". Els notebooks de la Lucía viuen a la seva instància de Workbench. I les comandes de gcloud amb què la Marta va crear la VPC, el balancejador i les regles de Cloud Armor no són enlloc, llevat de l'historial del seu terminal, que es trunca als 10.000 ordres.

Res del que es construeixi a sobre —ni CI/CD, ni infraestructura com a codi, ni revisió de canvis— és possible sense resoldre això primer. Aquesta lliçó ho resol, i ho fa amb una dosi gran d'honestedat sobre quina eina convé fer servir de debò el 2026.

Contingut

  1. Per què el control de versions és el fonament de tot
  2. Què és Cloud Source Repositories i com es fa servir
  3. La conversa honesta: CSR està en desús
  4. Connectar GitHub o GitLab amb GCP
  5. Workload Identity Federation: accedir a GCP sense claus JSON
  6. Taula de decisió: CSR, GitHub o GitLab
  7. Estratègia de branques per a un equip petit
  8. Què ha d'estar i què no ha d'estar al repositori
  9. L'estructura de repositoris d'AlpinaShop
  10. Revisió de codi i protecció de branques
  11. Missatges de commit i versionatge semàntic
  12. Ganxos de qualitat abans del commit
  13. Connectar els repositoris amb els activadors de Cloud Build

  1. Per què el control de versions és el fonament de tot

Val la pena dir explícitament què proporciona un sistema de control de versions, perquè quan es porten anys utilitzant-lo s'oblida que resol diversos problemes diferents alhora:

Capacitat Què permet Què es trenca sense ella a AlpinaShop
Història Saber què va canviar, quan i per què Ningú no sap per què el timeout del catàleg és 45 s
Atribució Saber qui va fer cada canvi Davant d'una fallada, no hi ha a qui preguntar
Reversió Tornar a un estat anterior conegut L'única tornada enrere és reescriure a mà
Ramificació Treballar en paral·lel sense trepitjar-se En Dani i la Marta es passen fitxers per Slack
Revisió Que una altra persona miri abans de fusionar Ningú no revisa res
Identitat immutable Un SHA identifica un estat exacte El $COMMIT_SHA de 06-01 no existeix
Automatització Reaccionar a esdeveniments del repositori No hi ha activadors possibles

Fixa't en les dues últimes files, perquè són les que connecten amb la lliçó anterior. El pipeline de Cloud Build etiqueta cada imatge amb $COMMIT_SHA, i aquella etiqueta és el que fa possible saber quin codi és en producció i tornar enrere. Sense repositori remot, no hi ha SHA, no hi ha activador i no hi ha pipeline. Tot el que s'ha construït a 06-01 depèn de resoldre això.

I hi ha un aspecte cultural que importa tant com el tècnic: el control de versions converteix el codi de propietat personal en propietat de l'equip. Mentre el catàleg sigui al portàtil d'en Dani, és d'en Dani —amb el que això implica quan se'n va de vacances, quan es posa malalt o quan canvia d'empresa—. Tan bon punt és en un repositori compartit amb història i revisió, és d'AlpinaShop.

  1. Què és Cloud Source Repositories i com es fa servir

Cloud Source Repositories (CSR) és el servei de repositoris Git privats allotjats a Google Cloud. Un repositori de CSR és un repositori Git normal i corrent: clone, commit, push, pull, branques i etiquetes funcionen exactament igual. El que hi afegeix és la seva integració amb la resta de GCP.

Crear i clonar un repositori:

# Crear el repositori dins del projecte de CI/CD
gcloud source repos create alpinashop-catalogo --project=alpinashop-cicd

# Clonar-lo: gcloud configura l'autenticació per tu
gcloud source repos clone alpinashop-catalogo --project=alpinashop-cicd
cd alpinashop-catalogo

# A partir d'aquí és Git normal
git add .
git commit -m "Importacio inicial del cataleg web"
git push origin main

El gcloud source repos clone fa alguna cosa més que un git clone: configura un ajudant de credencials que utilitza la teva identitat de gcloud per autenticar-te. No hi ha contrasenyes ni claus SSH per gestionar; l'accés es controla amb IAM, igual que qualsevol altre recurs de GCP:

# En Dani pot llegir i escriure al repositori
gcloud projects add-iam-policy-binding alpinashop-cicd \
  --member='group:[email protected]' \
  --role=roles/source.writer

# L'equip de dades només llegeix
gcloud projects add-iam-policy-binding alpinashop-cicd \
  --member='group:[email protected]' \
  --role=roles/source.reader

Si prefereixes autenticar-te sense gcloud —per exemple des d'una màquina que no el té— existeixen les credencials generades manualment i l'accés per SSH amb una clau registrada al teu perfil.

El que CSR aporta de debò, i que explica per què va existir:

  • Integració nativa amb Cloud Build: els activadors de 06-01 funcionen sense connectar res extern ni instal·lar aplicacions de tercers.
  • Els permisos són IAM: els mateixos grups, rols i condicions de 03-04 governen l'accés al codi. No hi ha un sistema de permisos paral·lel per mantenir sincronitzat.
  • Cerca de codi a tots els repositoris del projecte des de la consola.
  • Integració amb Error Reporting i Cloud Debugger: quan una excepció s'agrupa a Error Reporting, la consola pot mostrar la línia de codi concreta del repositori.
  • Rèplica automàtica des de GitHub o Bitbucket, per tenir una còpia dins de GCP.
  • Les dades no surten de la teva organització de GCP, cosa que en alguns contextos regulats simplifica una conversa amb el departament legal.

Tot això és real i funciona. I tot i així, no és el que AlpinaShop farà servir.

  1. La conversa honesta: CSR està en desús

Cal dir-ho amb claredat, perquè el nom d'aquesta lliçó porta el servei al títol i seria fàcil donar la impressió equivocada:

Cloud Source Repositories està en desús. Des del 2024 no s'habilita per a clients nous que no l'haguessin utilitzat prèviament, no rep funcionalitat nova i Google recomana explícitament utilitzar GitHub, GitLab o un altre proveïdor extern connectat a Cloud Build.

Els repositoris existents continuen funcionant i no hi ha una data d'apagada anunciada. Però no s'ha de començar un projecte nou amb CSR el 2026, i convé entendre per què va desaparèixer, perquè la raó és interessant i no és tècnica.

CSR allotjava Git perfectament. El que mai no va tenir va ser la resta: revisió de codi amb comentaris en línia, plantilles de pull request, discussions, gestió d'incidències, wiki, integració amb l'ecosistema d'eines que tothom fa servir. I resulta que el repositori no és el producte: el producte és el flux de col·laboració al voltant del repositori. Google va competir en la part fàcil —emmagatzemar objectes Git— contra plataformes que havien construït la part difícil.

Per això el patró dominant avui és: el codi a GitHub o GitLab, l'execució a GCP. I per això aquesta lliçó dedica la resta del seu espai a fer bé aquella connexió.

I per què continua valent la pena conèixer CSR? Per tres raons pràctiques: perquè te'l trobaràs en infraestructures heretades i l'hauràs d'operar o migrar; perquè el seu model de permisos basat en IAM continua sent una idea excel·lent que convé entendre; i perquè la rèplica des de GitHub continua sent útil quan algú exigeix una còpia del codi dins de l'organització de GCP.

Si et toca migrar de CSR a GitHub, el procediment és curt perquè Git és Git:

# Clon complet amb totes les branques i etiquetes
git clone --mirror https://source.developers.google.com/p/alpinashop-cicd/r/alpinashop-catalogo
cd alpinashop-catalogo.git

# Empènyer-ho tot a la destinació nova
git remote set-url origin [email protected]:alpinashop/alpinashop-catalogo.git
git push --mirror origin

El que no migra aquella comanda: els permisos IAM (cal refer-los al proveïdor nou) i els activadors de Cloud Build, que cal recrear apuntant a la connexió nova.

  1. Connectar GitHub o GitLab amb GCP

AlpinaShop tria GitHub, i necessita que Cloud Build reaccioni als seus esdeveniments. La connexió ha evolucionat i el 2026 convé fer servir la forma moderna.

Existeixen dues generacions de connexió de repositoris, i la diferència importa:

Aspecte 1a generació (aplicació de GitHub) 2a generació (Cloud Build repositories)
Mecanisme Aplicació de GitHub instal·lada a l'organització Recurs Connection + Repository gestionat
Gestió Només per consola Consola, gcloud i Terraform
Credencials Gestionades per l'aplicació Testimoni a Secret Manager, controlat per tu
Repositoris S'enllacen un a un per consola Enllaçables per API, en lot
Proveïdors GitHub, GitHub Enterprise GitHub, GitHub Enterprise, GitLab, Bitbucket
Recomanació Heretat La que cal fer servir

La segona generació és la correcta perquè l'enllaç del repositori deixa de ser un clic irreproduïble en una consola i passa a ser un recurs declarable —cosa que encaixa amb tot el que arriba a 06-05 i 06-07—.

El procediment, pas a pas:

# 1. Habilitar les API necessàries
gcloud services enable cloudbuild.googleapis.com secretmanager.googleapis.com \
  --project=alpinashop-cicd

# 2. Guardar el testimoni d'accés personal de GitHub a Secret Manager
#    (amb permisos: repo, read:user, read:org — res més)
printf 'ghp_XXXXXXXXXXXXXXXXXXXX' | gcloud secrets create github-token-alpinashop \
  --data-file=- --project=alpinashop-cicd

# 3. Donar accés al secret al compte de servei de Cloud Build
NUM=$(gcloud projects describe alpinashop-cicd --format='value(projectNumber)')
gcloud secrets add-iam-policy-binding github-token-alpinashop \
  --member="serviceAccount:service-${NUM}@gcp-sa-cloudbuild.iam.gserviceaccount.com" \
  --role=roles/secretmanager.secretAccessor --project=alpinashop-cicd

# 4. Crear la connexió (requereix una autorització interactiva al navegador)
gcloud builds connections create github alpinashop-github \
  --region=europe-west1 --project=alpinashop-cicd

# 5. Enllaçar el repositori concret
gcloud builds repositories create alpinashop-catalogo \
  --remote-uri=https://github.com/alpinashop/alpinashop-catalogo.git \
  --connection=alpinashop-github \
  --region=europe-west1 --project=alpinashop-cicd

Fixa't en el pas 2: el testimoni de GitHub va a Secret Manager, exactament igual que la contrasenya de la base de dades a 03-06. Un testimoni amb permís d'escriptura sobre tots els teus repositoris és una credencial de primer nivell, i mereix el mateix tractament: emmagatzematge xifrat, accés concedit a una identitat concreta i rotació periòdica. Anotar la data de caducitat del testimoni al calendari de l'equip evita el clàssic "el pipeline va deixar de funcionar el dimarts i ningú no sap per què".

Amb la connexió creada, un activador es defineix contra el recurs Repository:

gcloud builds triggers create github \
  --name=catalogo-main \
  --repository=projects/alpinashop-cicd/locations/europe-west1/connections/alpinashop-github/repositories/alpinashop-catalogo \
  --branch-pattern='^main$' \
  --build-config=cloudbuild.yaml \
  --region=europe-west1 \
  --service-account=projects/alpinashop-cicd/serviceAccounts/[email protected] \
  --project=alpinashop-cicd

Amb GitLab el procediment és anàleg: gcloud builds connections create gitlab, amb un testimoni d'accés de GitLab a Secret Manager. I per a GitLab autoallotjat darrere d'un tallafoc existeix la connexió mitjançant un agent de servei a la VPC, que és exactament el cas que cal quan el servidor de Git és intern.

  1. Workload Identity Federation: accedir a GCP sense claus JSON

Hi ha un segon sentit de la connexió, i és el que més problemes de seguretat causa: que les GitHub Actions accedeixin a recursos de GCP. AlpinaShop ho necessita perquè un workflow pugui publicar a Artifact Registry o consultar BigQuery sense dependre de Cloud Build.

La manera antiga de fer-ho, i per què és dolenta:

# NO FACIS AIXÒ
gcloud iam service-accounts keys create clave.json \
  [email protected]
# ...i enganxar el contingut de clave.json en un secret de GitHub

Aquella clau JSON és una credencial permanent, sense caducitat i transferible. Qui l'obté és aquell compte de servei, per sempre, des de qualsevol lloc del món. No hi ha manera de saber si s'ha copiat. Es filtra en registres, en captures de pantalla, en commits accidentals, i la seva rotació és un procés manual que ningú no fa. Les claus JSON de compte de servei són, amb diferència, la causa més comuna d'incidents de seguretat greus a GCP.

Workload Identity Federation elimina el problema d'arrel. La idea, que connecta directament amb el que s'ha vist a 03-04:

GCP confia en l'emissor de testimonis de GitHub. Quan una GitHub Action s'executa, GitHub li lliura un testimoni OIDC de curta durada que diu "soc el workflow X del repositori Y de l'organització Z". GCP verifica aquell testimoni, comprova que coincideix amb una condició que tu has definit, i l'intercanvia per credencials temporals.

flowchart LR
    A[GitHub Action<br/>en execució] -->|1. demana testimoni OIDC| B[Emissor OIDC<br/>de GitHub]
    B -->|2. testimoni signat:<br/>repo, branca, workflow| A
    A -->|3. presenta el testimoni| C[Workload Identity Pool<br/>a GCP]
    C -->|4. verifica signatura<br/>i condició d'atributs| D{Coincideix?}
    D -->|Sí| E[Credencial temporal<br/>del compte de servei]
    D -->|No| F[Rebutjat]
    E --> G[Accés a Artifact Registry,<br/>BigQuery, etc.]

La configuració completa:

PROY=alpinashop-cicd
NUM=$(gcloud projects describe $PROY --format='value(projectNumber)')

# 1. Crear el pool d'identitats de càrrega de treball
gcloud iam workload-identity-pools create github-pool \
  --location=global --display-name="GitHub Actions" --project=$PROY

# 2. Crear el proveïdor OIDC, amb la condició d'atributs
gcloud iam workload-identity-pools providers create-oidc github-provider \
  --location=global --workload-identity-pool=github-pool \
  --issuer-uri="https://token.actions.githubusercontent.com" \
  --attribute-mapping="google.subject=assertion.sub,attribute.repository=assertion.repository,attribute.ref=assertion.ref" \
  --attribute-condition="assertion.repository_owner == 'alpinashop'" \
  --project=$PROY

# 3. Permetre que NOMÉS el repositori del catàleg suplanti el compte de servei
gcloud iam service-accounts add-iam-policy-binding \
  sa-github-catalogo@${PROY}.iam.gserviceaccount.com \
  --role=roles/iam.workloadIdentityUser \
  --member="principalSet://iam.googleapis.com/projects/${NUM}/locations/global/workloadIdentityPools/github-pool/attribute.repository/alpinashop/alpinashop-catalogo" \
  --project=$PROY

El pas 3 és el que cal entendre bé. El principalSet amb attribute.repository/alpinashop/alpinashop-catalogo significa que només els workflows d'aquell repositori concret poden obtenir credencials d'aquell compte de servei. Un workflow d'un altre repositori de la mateixa organització presenta un testimoni vàlid però amb un altre repository, no coincideix amb el principalSet, i és rebutjat.

Si a més vols restringir per branca —que només main pugui desplegar—, s'utilitza attribute.ref:

principalSet://.../workloadIdentityPools/github-pool/attribute.ref/refs/heads/main

I així es consumeix des del workflow:

# .github/workflows/publicar.yml
name: Publicar imatge
on:
  push:
    branches: [main]

permissions:
  contents: read
  id-token: write        # imprescindible: permet demanar el testimoni OIDC

jobs:
  publicar:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - id: auth
        uses: google-github-actions/auth@v2
        with:
          workload_identity_provider: 'projects/123456789/locations/global/workloadIdentityPools/github-pool/providers/github-provider'
          service_account: '[email protected]'
          # Fixa-t'hi: NO hi ha cap clau, cap secret, cap JSON

      - uses: google-github-actions/setup-gcloud@v2
      - run: |
          gcloud auth configure-docker europe-west1-docker.pkg.dev --quiet
          docker build -t europe-west1-docker.pkg.dev/alpinashop-prod/alpinashop/catalogo:${GITHUB_SHA} .
          docker push europe-west1-docker.pkg.dev/alpinashop-prod/alpinashop/catalogo:${GITHUB_SHA}

La línia id-token: write a permissions és obligatòria i s'oblida constantment; sense ella, GitHub no emet el testimoni OIDC i l'autenticació falla amb un missatge que no ajuda gaire.

Aspecte Clau JSON Workload Identity Federation
Caducitat Cap Minuts
Si es filtra Accés permanent Inútil fora de context
Rotació Manual, ningú no la fa Automàtica per disseny
Àmbit Qualsevol que la tingui Només el repositori/branca declarat
Auditoria "El compte X va fer alguna cosa" Quin repositori i quin workflow
Feina de manteniment Alta Cap

La regla pràctica: si estàs creant una clau JSON de compte de servei el 2026, gairebé sempre hi ha una alternativa millor. Dins de GCP, comptes de servei adjuntats al recurs. Fora de GCP, federació d'identitat. Les claus JSON queden per a casos residuals, i en aquells casos han de tenir caducitat, rotació i una organization policy que limiti on es poden crear —tema de 07-07—.

  1. Taula de decisió: CSR, GitHub o GitLab

La comparació sense adorns, que és el que AlpinaShop necessita per decidir:

Criteri Cloud Source Repositories GitHub GitLab
Estat En desús, sense novetats Actiu, líder de mercat Actiu, fort en autoallotjat
Cost Gratuït fins a un límit baix d'emmagatzematge i dades Gratis en privat; de pagament per funcions avançades Igual; edició comunitària autoallotjable gratis
Revisió de codi Pràcticament inexistent Pull requests madurs, suggeriments, propietaris de codi Merge requests, molt complets
CI/CD propi No, depèn de Cloud Build GitHub Actions, ecosistema enorme GitLab CI, molt potent i integrat
Integració amb GCP Nativa, permisos IAM Excel·lent (2a gen + federació) Molt bona (2a gen + federació)
Ecosistema i plantilles Nul El més gran amb diferència Ampli
Incidències i projectes No Sí Sí, inclòs DevSecOps
Autoallotjat No aplica GitHub Enterprise Server El seu punt fort
Dependència de proveïdor Alta amb Google Mitjana (Git és portable) Baixa, es pot autoallotjar
Contractació de talent Ningú no el coneix Tothom el coneix Molt conegut

La decisió d'AlpinaShop és GitHub, per quatre raons ordenades per pes:

  1. La revisió de codi és l'objectiu principal. El problema d'AlpinaShop no és on guardar bytes: és que ningú no mira els canvis abans que arribin a producció. GitHub ho resol; CSR, no.
  2. Qualsevol persona que s'incorpori ja el sap fer servir. Amb un equip de tres persones tècniques, el temps de formació compta.
  3. La integració amb GCP és de primera classe gràcies a la 2a generació i a la federació d'identitat. El suposat avantatge de CSR ha deixat de ser-ho.
  4. La dependència és baixa de debò. Un repositori Git és portable: git clone --mirror i ja ets en un altre lloc. El que sí que lliga són les incidències, les pull requests i els workflows d'Actions, i per això el pipeline principal d'AlpinaShop viu a cloudbuild.yaml —un fitxer neutral— en lloc de a GitHub Actions.

Quan triaries GitLab en el seu lloc: si necessites autoallotjar per requisits reguladors, si vols un CI/CD integrat sense dependre de Cloud Build, o si valores tenir incidències, codi, CI i registre de contenidors en un únic producte. És una elecció perfectament defensable.

Quan continuaries amb CSR: només si ja el fas servir i funciona. Migrar per migrar no és una prioritat; migrar quan calgui revisió de codi de debò, sí.

  1. Estratègia de branques per a un equip petit

Amb el repositori decidit, la següent pregunta és com s'organitza la feina a dins. Hi ha dues escoles dominants.

GitFlow defineix branques de llarga vida: main (el que és en producció), develop (integració), feature/*, release/* i hotfix/*. Un canvi neix en una branca de característica, es fusiona a develop, entra en una branca de versió, s'estabilitza i per fi arriba a main.

Trunk-based defineix una única branca de llarga vida, main, sempre desplegable. Els canvis es fan en branques curtes —hores o un parell de dies com a molt— que es fusionen a main tan bon punt passen la revisió i la CI.

Aspecte GitFlow Trunk-based
Branques de llarga vida main i develop Només main
Vida d'una branca de treball Dies o setmanes Hores o un parell de dies
Dolor en fusionar Alt: divergeixen molt Baix: divergeixen poc
Encaixa amb CI Malament: s'integra tard És la premissa de la CI
Complexitat Alta Baixa
Idoni per a Versions empaquetades, diversos mantinguts alhora Serveis web amb desplegament continu
Funcions a mitges Branca llarga sense fusionar Feature flags

AlpinaShop tria trunk-based, i les raons són concretes:

  • És un servei web, no un producte empaquetat. No hi ha clients amb la versió 2.3 que necessitin un pedaç mentre es desenvolupa la 3.0. Només hi ha una versió: la que és a alpinashop-prod.
  • L'equip són tres persones. La coordinació que justifica GitFlow no existeix quan cabeu en una trucada.
  • És coherent amb la CI de 06-01. La "I" d'integració contínua significa integrar contínuament; una branca de tres setmanes és exactament el contrari, i el pipeline que vas construir només aporta valor si els canvis arriben a main sovint.
  • Els conflictes de fusió creixen de manera no lineal amb el temps. Dues branques d'un dia gairebé mai no xoquen; dues branques de tres setmanes sempre ho fan, i resoldre aquell conflicte és on s'esmunyen els errors.

El flux de treball diari resultant:

git switch main && git pull                         # 1. Partir sempre de main actualitzat
git switch -c feat/filtro-por-marca                 # 2. Branca curta i amb nom clar
# ... feina, commits petits ...
git push -u origin feat/filtro-por-marca            # 3. Publicar i obrir la pull request
# 4. La CI de 06-01 s'executa sola sobre la PR
# 5. La Marta o en Dani revisen
# 6. Fusió amb squash a main → es desplega a alpinashop-dev
git switch main && git pull
git branch -d feat/filtro-por-marca                 # 7. Esborrar la branca

L'objecció evident: i si una funcionalitat triga tres setmanes? La resposta de trunk-based no és "mantén la branca tres setmanes", sinó fusionar codi incomplet però inactiu, protegit per un interruptor:

# catalogo/config.py — l'interruptor es llegeix de l'entorn, no del codi
NOU_CERCADOR = os.environ.get("FEATURE_NOU_CERCADOR", "false").lower() == "true"

# catalogo/rutas.py
@app.get("/buscar")
def cercar():
    if NOU_CERCADOR:
        return cercador_semantic(request.args["q"])    # 05-06, en proves
    return cercador_classic(request.args["q"])         # el que funciona avui

El codi nou arriba a main i a producció des del primer dia, desactivat. S'activa a alpinashop-dev per provar-lo, després per a un percentatge d'usuaris, i per fi per a tothom. I si alguna cosa va malament, desactivar-lo és canviar una variable d'entorn, no revertir un desplegament. És una reversió de segons.

La contrapartida honesta: els interruptors acumulen complexitat i branques mortes al codi. La disciplina associada és esborrar-los tan bon punt la funcionalitat està consolidada, amb una data de caducitat apuntada a la mateixa pull request que els introdueix.

  1. Què ha d'estar i què no ha d'estar al repositori

Una regla útil com a punt de partida: si és text que descriu com funciona o com es construeix el sistema, va al repositori.

Sí que va al repositori Per què
Codi de l'aplicació Evident
Dockerfile i .dockerignore La imatge ha de ser reproduïble des del codi
cloudbuild.yaml i variants El pipeline es revisa com el codi (06-01)
Manifestos de Kubernetes Avui són a Drive; allà no es revisen ni es versionen
Terraform (.tf) La infraestructura com a codi arriba a 06-07
Proves Sense elles la CI no comprova res
requirements.txt amb versions fixades Reproductibilitat de les dependències
Migracions de base de dades Canvis d'esquema revisables i ordenats
Definicions de pipelines de dades i ML Els components KFP de 05-07
README i decisions d'arquitectura DA-001, DA-002 i DA-003 han d'estar escrites
Configuració no sensible per entorn dev.tfvars, prod.tfvars
No va al repositori On va Per què
Contrasenyes, testimonis, claus d'API Secret Manager (03-06) Git no oblida mai
Claus JSON de compte de servei No haurien d'existir (apartat 5) —
Certificats i claus privades Secret Manager o KMS Igual que a dalt
Estat de Terraform (.tfstate) Bucket de Cloud Storage (06-07) Conté valors sensibles
Dades de clients, bolcats de BD Cloud Storage amb control d'accés RGPD
Artefactes compilats, imatges Artifact Registry El repositori no és un magatzem binari
Fitxers grans (models, CSV) Cloud Storage o Git LFS Inflen el repositori per sempre
.env amb valors reals Variables d'entorn o Secret Manager Es filtra en fer git add .

La frase clau sobre els secrets mereix subratllar-se: Git no oblida. Si puges una contrasenya i l'esborres al commit següent, continua sent a l'historial, a cada clon que algú hagi fet i a cada rèplica. Reescriure la història és dolorós i mai no és complet. L'única resposta correcta davant d'un secret filtrat és rotar-lo immediatament, i només després netejar l'historial si val la pena.

Un .gitignore sensat és la primera línia de defensa:

# Secrets i credencials
*.env
.env.*
*-key.json
*credentials*.json
*.pem
*.p12

# Estat de Terraform: MAI a Git
*.tfstate
*.tfstate.*
.terraform/
*.tfvars.secret

# Entorns i artefactes locals
venv/
__pycache__/
*.pyc
.pytest_cache/
dist/
build/

# Dades
*.csv
*.parquet
datos/

Però la defensa real és automàtica, i és el tema de l'apartat 12.

  1. L'estructura de repositoris d'AlpinaShop

El debat monorepo davant de polirepo té molta literatura i poca resposta universal:

Aspecte Monorepo Polirepo
Canvi que creua components Una sola PR atòmica Diverses PR coordinades
Permisos per component Difícil d'acotar Natural
CI Necessita filtres per ruta Simple
Descobriment de codi Tot a la vista Cal saber on mirar
Dependències entre parts Fàcils de compartir, fàcils d'embolicar Fronteres explícites
Eines necessàries Més Menys

Per a un equip de tres persones, la complexitat d'un monorepo amb eines de construcció avançades no es justifica. Però quaranta repositoris diminuts tampoc. AlpinaShop es queda al punt intermedi: quatre repositoris agrupats per cicle de vida i per qui els toca.

Repositori Contingut Qui el manté Pipeline
alpinashop-catalogo Flask, Dockerfile, manifestos de tienda, cloudbuild.yaml Dani catalogo-main, catalogo-pr
alpinashop-infra Terraform de la VPC, balancejador, IAM, Cloud SQL, buckets Marta plan en PR, apply amb aprovació
alpinashop-datos Pipelines de Dataflow, consultes SQL, DAG/Workflows de 04-06 Lucía Proves + desplegament de plantilles
alpinashop-ml Components KFP, codi d'entrenament, notebooks nets Lucía i Dani cloudbuild-ml.yaml de 06-01

El criteri de divisió no és tècnic, és de ritme i de responsabilitat. El catàleg canvia diverses vegades al dia i el toca en Dani. La infraestructura canvia un cop per setmana, requereix aprovació de la Marta i el seu pipeline és completament diferent —un plan que es revisa, no unes proves que passen—. Ficar-los junts obligaria a executar el pipeline d'infraestructura per cada canvi d'una plantilla HTML.

I un advertiment sobre el repositori d'infraestructura: és el més perillós dels quatre. Qui pot fusionar una PR allà pot modificar regles de tallafoc, polítiques d'IAM i bases de dades. La seva protecció de branques ha de ser la més estricta, i els seus revisors obligatoris han d'incloure sempre algú de gcp-seguridad@.

  1. Revisió de codi i protecció de branques

Que existeixi un repositori no impedeix que algú empenyi directament a main sense que ningú ho miri. Això ho impedeix la protecció de branques.

Les regles que AlpinaShop activa sobre main als quatre repositoris:

Regla Efecte Per què
Prohibit el push directe Tot canvi passa per pull request Sense això, la resta és decoratiu
Almenys una aprovació Una altra persona ha mirat el canvi L'objectiu principal de la lliçó
La CI ha d'estar en verd No es fusiona amb proves vermelles Dona sentit al pipeline de 06-01
Les aprovacions caduquen en haver-hi canvis nous No s'aprova una versió i se'n fusiona una altra Evita un truc molt utilitzat
Historial lineal Fusió amb squash o rebase Història llegible, revertir és trivial
Ningú no se salta les regles, ni els administradors Sense excepcions Les excepcions es tornen norma
Escaneig de secrets Bloqueja el push si detecta credencials Última xarxa abans del desastre

Al repositori alpinashop-infra s'hi afegeix una regla més: revisor obligatori de l'equip de seguretat, mitjançant el fitxer CODEOWNERS:

# CODEOWNERS del repositori alpinashop-infra
*                       @alpinashop/infraestructura
/iam/                   @alpinashop/seguridad @alpinashop/infraestructura
/red/firewall.tf        @alpinashop/seguridad @alpinashop/infraestructura
/produccion/            @alpinashop/seguridad @alpinashop/infraestructura

Qualsevol PR que toqui IAM, regles de tallafoc o el directori de producció exigeix l'aprovació d'algú de gcp-seguridad@. És l'equivalent en el codi de la separació de funcions de 03-04.

Què mirar en una revisió, en ordre d'importància real: primer, és correcte? —fa el que diu, contempla els casos límit—; segon, és segur? —secrets, validació d'entrades, permisos que s'amplien—; tercer, està provat? —hi ha una prova que fallaria sense aquest canvi—; quart, s'entén? —ho llegirà algú d'aquí a un any—; i finalment, l'estil, que hauria d'estar automatitzat i no discutir-se a la revisió. Aquell últim punt estalvia una quantitat enorme de fricció: si el formatatge el decideix una eina, ningú no discuteix sobre comes.

I dues normes de convivència que valen més que qualsevol llista: pull requests petites —una PR de 40 línies rep comentaris útils, una de 2.000 rep un "lgtm"— i revisions ràpides, perquè una PR que espera dos dies bloqueja qui la va escriure i acumula conflictes.

  1. Missatges de commit i versionatge semàntic

Un historial de commits amb "canvis", "fix", "una altra vegada" i "ara sí" no serveix de res. Un historial bo respon a per què es va fer cada cosa, que és el que no es pot deduir del codi.

AlpinaShop adopta Conventional Commits, un format senzill amb un benefici gran:

<tipus>(<àmbit>): <resum en imperatiu, menys de 72 caràcters>

<cos: PER QUÈ es va fer, no què es va fer — això ja és al diff>

<peu: referències, canvis incompatibles>
feat(cercador): afegir filtre per marca al cataleg

El 18 % de les cerques incloïen un nom de marca al text
lliure, cosa que produïa resultats pobres. El filtre redueix la
consulta a la marca abans d'aplicar el rànquing.

Refs: #142
fix(comandes): reintentar la connexio a Cloud SQL despres d'una fallada de xarxa

El pool de connexions no es recuperava després d'un tall breu i
l'aplicació retornava 500 fins que es reiniciava el pod. S'afegeix
reintent amb retrocés exponencial, coherent amb 04-04.

Refs: #158

Els tipus habituals: feat (funcionalitat nova), fix (correcció), docs, refactor, test, chore (manteniment), perf i ci.

El benefici concret: el format és analitzable per màquines. D'allà surten automàticament el registre de canvis i la versió següent, segons versionatge semàntic (MAJOR.MENOR.PEDAÇ):

Tipus de commit Increment Exemple
fix: PEDAÇ 2.4.1 → 2.4.2
feat: MENOR 2.4.1 → 2.5.0
BREAKING CHANGE: al peu o feat!: MAJOR 2.4.1 → 3.0.0

I aquí hi ha un matís honest que convé dir: el versionatge semàntic està pensat per a artefactes que altres consumeixen —biblioteques, API públiques—. Per al catàleg web d'AlpinaShop, que és un servei intern desplegat contínuament, la versió importa molt menys que el SHA del commit, que ja és un identificador exacte. AlpinaShop utilitza etiquetes semàntiques per marcar candidates a producció —és l'activador per etiqueta de 06-01— i per tenir noms pronunciables en una reunió, no com a mecanisme de compatibilitat.

  1. Ganxos de qualitat abans del commit

Una fallada detectada a la CI costa minuts d'espera. La mateixa fallada detectada abans de fer el commit costa segons. Els ganxos de pre-commit executen comprovacions ràpides en local abans que el commit es creï.

# .pre-commit-config.yaml, a l'arrel de cada repositori
repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.6.0
    hooks:
      - id: trailing-whitespace
      - id: end-of-file-fixer
      - id: check-yaml
      - id: check-added-large-files       # evita pujar models i CSV enormes
        args: ['--maxkb=500']
      - id: check-merge-conflict

  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.6.9
    hooks:
      - id: ruff                          # errors i estil
        args: [--fix]
      - id: ruff-format                   # formatatge automàtic

  - repo: https://github.com/gitleaks/gitleaks
    rev: v8.21.0
    hooks:
      - id: gitleaks                      # EL MÉS IMPORTANT: busca secrets
pip install pre-commit
pre-commit install          # instal·la el ganxo a .git/hooks
pre-commit run --all-files  # primera passada sobre tot el repositori

El ganxo de gitleaks és el que justifica tot el mecanisme per si sol. Busca patrons de credencials —claus d'API, testimonis, claus privades, claus JSON de GCP— i bloqueja el commit si en troba algun. Recorda el de l'apartat 8: Git no oblida. És infinitament més barat bloquejar el commit que rotar una clau de la passarel·la de pagament filtrada.

Dos advertiments sobre els ganxos locals. El primer: són opcionals per naturalesa, perquè qualsevol se'ls pot saltar amb git commit --no-verify. Per això les mateixes comprovacions s'han de repetir a la CI, on no es poden esquivar. Els ganxos són comoditat per a qui desenvolupa, no un control de seguretat. El segon: han de ser ràpids. Un ganxo que triga trenta segons fa que tothom aprengui a fer servir --no-verify. El que és lent —la bateria completa de proves— va a la CI, no aquí.

  1. Connectar els repositoris amb els activadors de Cloud Build

El tancament de la lliçó és unir el de 06-01 amb el d'aquí. Aquest és el mapa complet d'AlpinaShop:

flowchart TD
    A[Dani obre PR<br/>feat/filtro-por-marca] --> B[Activador catalogo-pr]
    B --> C[cloudbuild-pr.yaml:<br/>instal·lar, pytest, ruff, bandit]
    C --> D{Verd?}
    D -->|No| E[PR bloquejada<br/>per protecció de branca]
    D -->|Sí| F[Marta revisa]
    F --> G[Squash a main]
    G --> H[Activador catalogo-main]
    H --> I[cloudbuild.yaml:<br/>construir + publicar :SHA]
    I --> J[Desplegament a<br/>alpinashop-dev]
    J --> K{Etiqueta v2.5.0}
    K --> L[Activador de promoció<br/>amb aprovació]
    L --> M[alpinashop-prod]

Els quatre activadors que AlpinaShop deixa configurats:

Activador Repositori Esdeveniment Configuració Què fa
catalogo-pr alpinashop-catalogo PR contra main cloudbuild-pr.yaml Prova, no desplega
catalogo-main alpinashop-catalogo Push a main cloudbuild.yaml Construeix, publica, desplega a dev
catalogo-prod alpinashop-catalogo Etiqueta ^v\d+\.\d+\.\d+$ cloudbuild-prod.yaml, --require-approval Promociona a producció
ml-main alpinashop-ml Push a main cloudbuild-ml.yaml Compila i llança el pipeline de Vertex AI

I una comprovació final que val la pena fer explícita: l'activador de producció s'activa en crear una etiqueta, així que qui pugui crear etiquetes pot iniciar un desplegament a producció. La protecció d'etiquetes és tan necessària com la de branques:

# Regla de protecció d'etiquetes a GitHub
Patró: v*
Qui pot crear: només l'equip @alpinashop/infraestructura

Sense aquella regla, l'aprovació manual de 06-01 continua protegint el desplegament —ningú no arriba a producció sense que la Marta aprovi—, però qualsevol podria omplir la cua de compilacions pendents. Amb ella, el camí a producció està acotat de principi a fi.

Errors Habituals i Consells

Començar un projecte nou amb Cloud Source Repositories el 2026. Està en desús, no rep funcionalitat nova i no té revisió de codi, que és just el que es necessita. Coneix-lo per operar el que és heretat; tria GitHub o GitLab per al que és nou.

Crear claus JSON de compte de servei per a la CI. Credencials permanents i transferibles, la causa més comuna d'incidents greus a GCP. Fes servir Workload Identity Federation. I si trobes claus JSON antigues per la infraestructura, planifica'n l'eliminació com si fos deute de seguretat, perquè ho és.

Oblidar id-token: write als permisos del workflow de GitHub Actions. Sense aquella línia no s'emet el testimoni OIDC i l'autenticació falla amb un error confús. És el primer entrebanc de tothom.

Branques de característica que viuen setmanes. Cada dia que una branca viu augmenta la probabilitat d'un conflicte dolorós i endarrereix el senyal de la CI. Branques curtes i interruptors de funcionalitat per al que no està llest.

Pujar un secret i "arreglar-ho" esborrant-lo al commit següent. Continua a l'historial i a tots els clons. Rota el secret immediatament; la neteja de l'historial és secundària.

Confiar només en els ganxos de pre-commit. Se salten amb --no-verify. Repeteix sempre les comprovacions a la CI, on són obligatòries.

Pull requests enormes. Una PR de 2.000 línies no es revisa, s'aprova. Divideix en canvis petits amb una intenció cadascun; la qualitat dels comentaris és inversament proporcional a la mida del diff.

Discutir estil a les revisions. Automatitza el formatatge amb ruff-format o equivalent i dedica la revisió al que una màquina no pot avaluar: si l'enfocament és correcte i si és segur.

No protegir les etiquetes. Si el desplegament a producció es dispara amb una etiqueta, qui crea etiquetes inicia desplegaments. Protegeix-les igual que les branques.

Consell final: la migració es fa per parts. No intentis portar els quatre repositoris a GitHub el mateix dia. Comença pel catàleg, que és el que més es toca, munta el seu pipeline, comprova que l'equip s'hi ha adaptat, i després ves amb la infraestructura. I comença per treure de Drive els manifestos de Kubernetes: és la mitja hora de feina amb millor relació benefici/cost de tot el mòdul.

Exercicis

Exercici 1: rescatar el codi dispers

Fes un pla concret de tres fases per portar al control de versions tots els actius dispersos d'AlpinaShop: el repositori local d'en Dani, els startup scripts als metadades de les plantilles d'instància, els manifestos de Kubernetes a Drive, els notebooks de la Lucía i les comandes de gcloud de la Marta. Per a cada actiu, indica a quin repositori va, què cal revisar abans de pujar-lo i quin risc concret té aquella pujada.

Exercici 2: configurar l'accés de GitHub Actions amb mínim privilegi

AlpinaShop vol un workflow de GitHub Actions a alpinashop-datos que, en fer push a main, validi les consultes SQL executant un dry run contra BigQuery a alpinashop-datos i publiqui un informe al bucket alpinashop-datalake. Escriu la configuració de Workload Identity Federation i els rols IAM exactes, i explica què impedeix que un workflow del repositori alpinashop-catalogo faci servir aquella mateixa identitat.

Exercici 3: triar estratègia de branques amb un cas incòmode

AlpinaShop signa un contracte amb una cadena de botigues que necessita una versió personalitzada del catàleg, amb el seu propi disseny i algunes regles de preus diferents, mantinguda durant almenys dos anys en paral·lel a la versió pública. En Dani proposa crear una branca cliente-x de llarga vida. Analitza la proposta, digues si trunk-based continua sent vàlid i proposa la solució que recomanaries, amb les seves contrapartides.

Solucions

Solució 1

Fase 1 — Rescatar el que corre risc de perdre's (aquesta setmana).

Actiu Destinació Revisar abans Risc
Repositori local d'en Dani alpinashop-catalogo Historial complet a la recerca de secrets Alt: si en algun commit antic hi ha una contrasenya, pujar-lo la publica a l'equip
Manifestos de Drive alpinashop-catalogo/k8s/ Quina versió és la que està realment aplicada Mitjà: hi ha tres versions i no se sap quina corre
Startup scripts alpinashop-infra/scripts/ Credencials incrustades a l'script Alt: els startup scripts són un lloc clàssic per enganxar contrasenyes

Per al repositori d'en Dani, el procediment correcte és escanejar l'historial complet, no només l'estat actual:

git clone --mirror /ruta/local/catalogo catalogo-auditoria.git
gitleaks detect --source=catalogo-auditoria.git --report-path=hallazgos.json

Si apareixen secrets, hi ha dos camins. El pragmàtic: rotar totes les credencials trobades i pujar l'historial tal qual, acceptant que conté valors ja invàlids. El purista: reescriure l'historial amb git filter-repo. Per a AlpinaShop recomano el pragmàtic —rotar és obligatori en tots dos casos, i la reescriptura aporta poc si els valors ja no serveixen—, llevat que hi hagi dades personals, on la reescriptura sí que és necessària.

Per als manifestos, la comprovació imprescindible és què està aplicat de debò, no quin fitxer es diu FINAL:

kubectl get deployment catalogo-web -n tienda -o yaml > k8s/catalogo-web.actual.yaml

Es netegen els camps generats pel clúster (status, metadata.uid, resourceVersion, anotacions d'última configuració) i allò és la versió que va al repositori. La veritat és al clúster, no a Drive.

Fase 2 — Estructurar i protegir (setmana següent). Crear els quatre repositoris, activar la protecció de branques i CODEOWNERS, instal·lar els ganxos de pre-commit amb gitleaks i connectar els activadors de Cloud Build.

Fase 3 — El difícil (setmanes següents).

Actiu Destinació Com Risc
Notebooks de la Lucía alpinashop-ml/notebooks/ Netejar sortides amb nbstripout; extreure el reutilitzable a mòduls Alt: els notebooks solen contenir dades de clients a les sortides
Comandes de gcloud de la Marta alpinashop-infra/ com a Terraform No transcriure comandes: exportar l'estat real, 06-05 i 06-07 Mitjà: laboriós, però sense pèrdua

El detall dels notebooks mereix èmfasi: una cel·la executada amb df.head() sobre alpinashop_analitica deixa noms, correus i adreces guardats dins del .ipynb. Pujar un notebook sense netejar sortides és una fuita de dades personals al repositori, amb Git no oblidant res. nbstripout com a ganxo de pre-commit ho resol de manera automàtica i no és negociable a alpinashop-ml.

I l'observació sobre les comandes de la Marta: transcriure-les a un script seria l'error. Aquell historial està incomplet i conté ordres que es van executar i després es van desfer. El correcte és exportar la configuració real de la infraestructura viva, que és exactament el procediment de 06-05.

Solució 2

Configuració de la federació, reutilitzant el pool ja creat:

PROY=alpinashop-datos
NUM=$(gcloud projects describe $PROY --format='value(projectNumber)')

gcloud iam service-accounts create sa-gha-datos \
  --display-name="GitHub Actions - validacio SQL" --project=$PROY

# Només el repositori alpinashop-datos, i només la branca main
gcloud iam service-accounts add-iam-policy-binding \
  sa-gha-datos@${PROY}.iam.gserviceaccount.com \
  --role=roles/iam.workloadIdentityUser \
  --member="principalSet://iam.googleapis.com/projects/${NUM}/locations/global/workloadIdentityPools/github-pool/attribute.repository/alpinashop/alpinashop-datos" \
  --project=$PROY

Rols exactes, aplicant mínim privilegi de debò:

Rol Àmbit Per què aquell i no un altre
roles/bigquery.jobUser Projecte alpinashop-datos Permet llançar treballs; un dry run no llegeix dades
roles/bigquery.metadataViewer Dataset alpinashop_analitica El dry run necessita els esquemes per validar
roles/storage.objectCreator Bucket alpinashop-datalake, prefix d'informes Crear, no llegir ni esborrar

Fixa't en el que no porta: ni bigquery.dataViewer —un dry run valida sintaxi i esquema, no executa la consulta ni llegeix una sola fila— ni storage.objectAdmin —crear informes no requereix poder esborrar els existents—. I l'objectCreator acotat al prefix mitjançant una condició IAM (03-04) impedeix que un workflow compromès sobreescrigui dades del data lake.

Què impedeix que un workflow d'alpinashop-catalogo faci servir aquesta identitat: el principalSet està lligat a attribute.repository/alpinashop/alpinashop-datos. El testimoni OIDC que GitHub emet porta signat el nom del repositori d'origen, i GitHub no permet falsificar-lo —l'emet el seu propi servei, no el workflow—. Un workflow d'alpinashop-catalogo presentaria un testimoni amb repository: alpinashop/alpinashop-catalogo, que no coincideix amb el principalSet, i GCP rebutjaria l'intercanvi. Aquella és la propietat que fa que la federació sigui substancialment més segura que una clau JSON: la identitat la certifica un tercer de confiança i està lligada al context d'execució, no a la possessió d'un fitxer.

I una millora addicional: l'attribute-condition del proveïdor (assertion.repository_owner == 'alpinashop') actua com a segona barrera a nivell de proveïdor. Sense ella, qualsevol repositori de GitHub del món podria almenys intentar l'intercanvi, i encara que el principalSet el rebutjaria igualment, és millor filtrar-lo abans. Defensa en profunditat.

Solució 3

La proposta de la branca de llarga vida és dolenta, i convé explicar per què abans de proposar-ne una alternativa. Una branca cliente-x mantinguda dos anys produeix, amb total certesa:

  • Divergència creixent: cada correcció de seguretat cal aplicar-la dues vegades, i en algun moment se n'oblidarà una. Aquell oblit serà just l'important.
  • Fusions cada vegada més cares: als sis mesos, portar els canvis de main a cliente-x és un projecte en si mateix.
  • Duplicació de tota la resta: dos pipelines, dos conjunts d'imatges, dos desplegaments, dues sèries de mètriques.
  • Un sistema que ningú no prova sencer: la CI provaria main, i cliente-x quedaria amb proves velles.

Però trunk-based continua sent vàlid, perquè el problema real no és d'estratègia de branques: és d'arquitectura. La pregunta correcta no és "com mantinc dues branques?", sinó "com faig que una sola base de codi serveixi dos clients?".

La solució recomanada: multiinquilí per configuració. Una única branca main, una única imatge, i el comportament diferent resolt en temps d'execució:

# catalogo/inquilino.py
@dataclass(frozen=True)
class Inquili:
    id: str
    tema: str                  # plantilles i fulls d'estil
    regles_preu: str           # estratègia de preus a aplicar
    domini: str

INQUILINS = {
    "public":  Inquili("public",  "alpina",   "estandard", "www.alpinashop.example"),
    "cliente-x": Inquili("cliente-x", "clientex", "majorista", "tienda.cliente-x.example"),
}

def inquili_actual() -> Inquili:
    # Resolució per domini: el mateix desplegament serveix els dos
    return INQUILINS.get(DOMINIS.get(request.host), INQUILINS["public"])

El que canvia per inquilí són dades i configuració: el tema visual, les regles de preu, el domini, potser el subconjunt de catàleg. Res d'això justifica una branca.

Enfocament Cost de manteniment Risc de divergència Complexitat del codi Recomanat?
Branca de llarga vida Molt alt i creixent Segur Baixa No
Multiinquilí per configuració Baix Cap Mitjana Sí
Repositori i producte separats Molt alt N/A: són productes diferents Baixa Només si divergeixen de debò
Nucli compartit com a biblioteca Mitjà Mitjana Alta Si hi ha molts clients

Les contrapartides, dites amb honestedat. El multiinquilí no és gratis: introdueix condicionals al codi, cal provar els dos camins, i existeix un risc nou i seriós —una fuita entre inquilins, que el client X vegi dades del públic o al revés—. Això exigeix que l'identificador d'inquilí s'apliqui a la capa d'accés a dades i es provi explícitament, no que es confiï que cada consulta se'n recordi de filtrar.

I hi ha un punt de ruptura que cal reconèixer: si d'aquí a un any el client X demana un flux de compra completament diferent, un catàleg amb una altra estructura i un procés de facturació propi, ja no són dues configuracions del mateix producte, sinó dos productes. En aquell moment la separació és correcta, i es fa com a productes separats amb una biblioteca comuna, no com una branca.

La regla que se n'endú un d'aquí: les branques serveixen per separar la feina en el temps, no per separar variants en l'espai. Quan algú proposa una branca de llarga vida per a una variant de producte, el senyal és que hi ha una decisió d'arquitectura pendent de prendre. I la conversa amb comercial també importa: si el contracte amb el client X permet una mica de flexibilitat en el disseny i en les regles de preu, el multiinquilí surt gairebé gratis; si exigeix un producte completament diferent, el cost real d'aquell contracte és molt més gran del que semblava en signar-lo, i això convé saber-ho abans.

Conclusió

AlpinaShop té per fi un fonament sobre el qual construir.

Saps què aporta el control de versions —història, atribució, reversió, ramificació, revisió, identitat immutable i automatització— i que les dues últimes són les que fan possible tot el de 06-01: sense repositori remot no hi ha $COMMIT_SHA, no hi ha activador i no hi ha pipeline.

Coneixes Cloud Source Repositories: repositoris Git privats a GCP, gcloud source repos clone amb autenticació per identitat de gcloud, permisos governats per IAM amb els mateixos grups de 03-04, cerca de codi i integració nativa amb Cloud Build i Error Reporting. I saps la veritat incòmoda: està en desús, no admet clients nous, no rep funcionalitat i mai no va tenir revisió de codi, que és el que de debò es necessita. El coneixes per operar infraestructures heretades i per migrar-les amb git clone --mirror, no per començar res nou.

Saps connectar GitHub o GitLab amb GCP utilitzant la 2a generació de repositoris de Cloud Build —connexió i repositori com a recursos declarables, testimoni a Secret Manager— i per què és preferible a l'aplicació de GitHub de la primera generació. I sobretot saps fer servir Workload Identity Federation perquè les GitHub Actions accedeixin a GCP sense cap clau JSON: el pool, el proveïdor OIDC, l'attribute-condition, el principalSet lligat a un repositori i a una branca, i l'id-token: write que tothom oblida. Amb la regla que resumeix l'apartat: si el 2026 estàs creant una clau JSON de compte de servei, gairebé sempre hi ha una alternativa millor.

Tens la taula de decisió honesta entre CSR, GitHub i GitLab, i la decisió d'AlpinaShop —GitHub— amb les seves quatre raons, juntament amb els casos en què GitLab seria millor elecció. Tens l'estratègia de branques: trunk-based amb branques curtes, perquè és un servei web amb una sola versió viva, perquè l'equip són tres persones, perquè és la premissa mateixa de la integració contínua i perquè els conflictes creixen de manera no lineal amb el temps. Amb els interruptors de funcionalitat com a resposta a "això triga tres setmanes", i amb la disciplina d'esborrar-los després.

Saps què va al repositori —codi, Dockerfile, pipelines, manifestos, Terraform, proves, migracions, decisions d'arquitectura— i què no —secrets, estat de Terraform, dades, artefactes, fitxers grans—, amb l'advertiment que val per tota la lliçó: Git no oblida, i davant d'un secret filtrat l'única resposta correcta és rotar-lo. Tens l'estructura de quatre repositoris d'AlpinaShop dividida per ritme i responsabilitat, amb alpinashop-infra assenyalat com el més perillós. Tens la protecció de branques amb les seves set regles, CODEOWNERS exigint revisió de seguretat per a IAM i tallafoc, els missatges de commit convencionals, el versionatge semàntic amb el seu matís honest, i els ganxos de pre-commit amb gitleaks com a xarxa de seguretat que no substitueix la CI.

I tens els quatre activadors connectats de principi a fi: pull request que prova, main que construeix i desplega en desenvolupament, etiqueta protegida que promociona a producció amb aprovació, i el pipeline d'ML compilant-se sol.

Amb això, el catàleg es construeix, es prova i es desplega sol. Però queden peces del sistema que no són ni una aplicació web ni un pipeline de dades: són reaccions a esdeveniments. Quan arriba un missatge al topic imagenes-subidas, alguna cosa ha de despertar, cridar la Vision API, escriure a BigQuery i tornar-se a adormir. Muntar una màquina virtual o un pod per a això seria absurd.

A 06-03 arriben les Cloud Functions, i amb elles es tanca per fi la punta solta que va deixar el mòdul 5.

Curs de Google Cloud Platform (GCP)

Mòdul 1: Introducció a Google Cloud Platform

Mòdul 2: Serveis principals de GCP

Mòdul 3: Xarxes i seguretat

Mòdul 4: Dades i anàlisi

Mòdul 5: Aprenentatge automàtic i IA

Mòdul 6: DevOps i monitoratge

Mòdul 7: Temes avançats de GCP

Mòdul 8: Projecte final

© Copyright 2026. Tots els drets reservats