La lección anterior terminó con una pregunta incómoda. Has construido un pipeline de integración continua que se dispara con cada push, que etiqueta las imágenes con el SHA del commit y que publica en Artifact Registry todo lo que pasa las pruebas. ¿Y desde qué repositorio se dispara exactamente?

Porque el inventario real del código de AlpinaShop en este momento es el siguiente. El catálogo web está en un repositorio Git local en el portátil de Dani, con un remoto que apunta a un servidor que la empresa dejó de mantener hace dos años; funciona porque nadie hace git push. Los startup scripts de las plantillas de instancia del MIG están pegados en el campo de metadatos de la plantilla, y en ningún otro sitio. Los manifiestos de Kubernetes del namespace tienda están en una carpeta compartida de Drive llamada "GKE definitivo (v3) FINAL". Los notebooks de Lucía viven en su instancia de Workbench. Y los comandos de gcloud con los que Marta creó la VPC, el balanceador y las reglas de Cloud Armor no están en ninguna parte, salvo en el historial de su terminal, que se trunca a los 10.000 comandos.

Nada de lo que se construya encima —ni CI/CD, ni infraestructura como código, ni revisión de cambios— es posible sin resolver esto primero. Esta lección lo resuelve, y lo hace con una dosis grande de honestidad sobre qué herramienta conviene usar de verdad en 2026.

Contenido

  1. Por qué el control de versiones es el cimiento de todo
  2. Qué es Cloud Source Repositories y cómo se usa
  3. La conversación honesta: CSR está en desuso
  4. Conectar GitHub o GitLab con GCP
  5. Workload Identity Federation: acceder a GCP sin claves JSON
  6. Tabla de decisión: CSR, GitHub o GitLab
  7. Estrategia de ramas para un equipo pequeño
  8. Qué debe y qué no debe estar en el repositorio
  9. La estructura de repositorios de AlpinaShop
  10. Revisión de código y protección de ramas
  11. Mensajes de commit y versionado semántico
  12. Ganchos de calidad antes del commit
  13. Conectar los repositorios con los disparadores de Cloud Build

  1. Por qué el control de versiones es el cimiento de todo

Merece la pena decir explícitamente qué proporciona un sistema de control de versiones, porque cuando se lleva años usándolo se olvida que resuelve varios problemas distintos a la vez:

Capacidad Qué permite Qué se rompe sin ella en AlpinaShop
Historia Saber qué cambió, cuándo y por qué Nadie sabe por qué el timeout del catálogo es 45 s
Atribución Saber quién hizo cada cambio Ante un fallo, no hay a quién preguntar
Reversión Volver a un estado anterior conocido La única vuelta atrás es reescribir a mano
Ramificación Trabajar en paralelo sin pisarse Dani y Marta se pasan ficheros por Slack
Revisión Que otra persona mire antes de fusionar Nadie revisa nada
Identidad inmutable Un SHA identifica un estado exacto El $COMMIT_SHA de 06-01 no existe
Automatización Reaccionar a eventos del repositorio No hay disparadores posibles

Fíjate en las dos últimas filas, porque son las que conectan con la lección anterior. El pipeline de Cloud Build etiqueta cada imagen con $COMMIT_SHA, y esa etiqueta es lo que hace posible saber qué código está en producción y volver atrás. Sin repositorio remoto, no hay SHA, no hay disparador y no hay pipeline. Todo lo construido en 06-01 depende de resolver esto.

Y hay un aspecto cultural que importa tanto como el técnico: el control de versiones convierte el código de propiedad personal en propiedad del equipo. Mientras el catálogo esté en el portátil de Dani, es de Dani —con lo que eso implica cuando se va de vacaciones, cuando se pone enfermo o cuando cambia de empresa—. En cuanto está en un repositorio compartido con historia y revisión, es de AlpinaShop.

  1. Qué es Cloud Source Repositories y cómo se usa

Cloud Source Repositories (CSR) es el servicio de repositorios Git privados alojados en Google Cloud. Un repositorio de CSR es un repositorio Git normal y corriente: clone, commit, push, pull, ramas y etiquetas funcionan exactamente igual. Lo que añade es su integración con el resto de GCP.

Crear y clonar un repositorio:

# Crear el repositorio dentro del proyecto de CI/CD
gcloud source repos create alpinashop-catalogo --project=alpinashop-cicd

# Clonarlo: gcloud configura la autenticación por ti
gcloud source repos clone alpinashop-catalogo --project=alpinashop-cicd
cd alpinashop-catalogo

# A partir de aquí es Git normal
git add .
git commit -m "Importación inicial del catálogo web"
git push origin main

El gcloud source repos clone hace algo más que un git clone: configura un ayudante de credenciales que usa tu identidad de gcloud para autenticarte. No hay contraseñas ni claves SSH que gestionar; el acceso se controla con IAM, igual que cualquier otro recurso de GCP:

# Dani puede leer y escribir en el repositorio
gcloud projects add-iam-policy-binding alpinashop-cicd \
  --member='group:[email protected]' \
  --role=roles/source.writer

# El equipo de datos solo lee
gcloud projects add-iam-policy-binding alpinashop-cicd \
  --member='group:[email protected]' \
  --role=roles/source.reader

Si prefieres autenticarte sin gcloud —por ejemplo desde una máquina que no lo tiene— existen las credenciales generadas manualmente y el acceso por SSH con una clave registrada en tu perfil.

Lo que CSR aporta de verdad, y que explica por qué existió:

  • Integración nativa con Cloud Build: los disparadores de 06-01 funcionan sin conectar nada externo ni instalar aplicaciones de terceros.
  • Los permisos son IAM: los mismos grupos, roles y condiciones de 03-04 gobiernan el acceso al código. No hay un sistema de permisos paralelo que mantener sincronizado.
  • Búsqueda de código en todos los repositorios del proyecto desde la consola.
  • Integración con Error Reporting y Cloud Debugger: cuando una excepción se agrupa en Error Reporting, la consola puede mostrar la línea de código concreta del repositorio.
  • Réplica automática desde GitHub o Bitbucket, para tener una copia dentro de GCP.
  • Los datos no salen de tu organización de GCP, lo que en algunos contextos regulados simplifica una conversación con el departamento legal.

Todo eso es real y funciona. Y aun así, no es lo que AlpinaShop va a usar.

  1. La conversación honesta: CSR está en desuso

Hay que decirlo con claridad, porque el nombre de esta lección lleva el servicio en el título y sería fácil dar la impresión equivocada:

Cloud Source Repositories está en desuso. Desde 2024 no se habilita para clientes nuevos que no lo hubieran usado previamente, no recibe funcionalidad nueva y Google recomienda explícitamente usar GitHub, GitLab u otro proveedor externo conectado a Cloud Build.

Los repositorios existentes siguen funcionando y no hay una fecha de apagado anunciada. Pero no se debe empezar un proyecto nuevo con CSR en 2026, y conviene entender por qué desapareció, porque la razón es interesante y no es técnica.

CSR alojaba Git perfectamente. Lo que nunca tuvo fue el resto: revisión de código con comentarios en línea, plantillas de pull request, discusiones, gestión de incidencias, wiki, integración con el ecosistema de herramientas que todo el mundo usa. Y resulta que el repositorio no es el producto: el producto es el flujo de colaboración alrededor del repositorio. Google compitió en la parte fácil —almacenar objetos Git— contra plataformas que habían construido la parte difícil.

Por eso el patrón dominante hoy es: el código en GitHub o GitLab, la ejecución en GCP. Y por eso esta lección dedica el resto de su espacio a hacer bien esa conexión.

¿Y por qué sigue mereciendo la pena conocer CSR? Por tres razones prácticas: porque te lo encontrarás en infraestructuras heredadas y tendrás que operarlo o migrarlo; porque su modelo de permisos basado en IAM sigue siendo una idea excelente que conviene entender; y porque la réplica desde GitHub sigue siendo útil cuando alguien exige una copia del código dentro de la organización de GCP.

Si te toca migrar de CSR a GitHub, el procedimiento es corto porque Git es Git:

# Clon completo con todas las ramas y etiquetas
git clone --mirror https://source.developers.google.com/p/alpinashop-cicd/r/alpinashop-catalogo
cd alpinashop-catalogo.git

# Empujar todo al destino nuevo
git remote set-url origin [email protected]:alpinashop/alpinashop-catalogo.git
git push --mirror origin

Lo que no migra ese comando: los permisos IAM (hay que rehacerlos en el proveedor nuevo) y los disparadores de Cloud Build, que hay que recrear apuntando a la conexión nueva.

  1. Conectar GitHub o GitLab con GCP

AlpinaShop elige GitHub, y necesita que Cloud Build reaccione a sus eventos. La conexión ha evolucionado y en 2026 conviene usar la forma moderna.

Existen dos generaciones de conexión de repositorios, y la diferencia importa:

Aspecto 1ª generación (aplicación de GitHub) 2ª generación (Cloud Build repositories)
Mecanismo Aplicación de GitHub instalada en la organización Recurso Connection + Repository gestionado
Gestión Solo por consola Consola, gcloud y Terraform
Credenciales Gestionadas por la aplicación Token en Secret Manager, controlado por ti
Repositorios Se enlazan uno a uno por consola Enlazables por API, en lote
Proveedores GitHub, GitHub Enterprise GitHub, GitHub Enterprise, GitLab, Bitbucket
Recomendación Heredado La que hay que usar

La segunda generación es la correcta porque el enlace del repositorio deja de ser un clic irreproducible en una consola y pasa a ser un recurso declarable —lo que encaja con todo lo que llega en 06-05 y 06-07—.

El procedimiento, paso a paso:

# 1. Habilitar las APIs necesarias
gcloud services enable cloudbuild.googleapis.com secretmanager.googleapis.com \
  --project=alpinashop-cicd

# 2. Guardar el token de acceso personal de GitHub en Secret Manager
#    (con permisos: repo, read:user, read:org — nada más)
printf 'ghp_XXXXXXXXXXXXXXXXXXXX' | gcloud secrets create github-token-alpinashop \
  --data-file=- --project=alpinashop-cicd

# 3. Dar acceso al secreto a la cuenta de servicio 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 conexión (requiere una autorización interactiva en el navegador)
gcloud builds connections create github alpinashop-github \
  --region=europe-west1 --project=alpinashop-cicd

# 5. Enlazar el repositorio concreto
gcloud builds repositories create alpinashop-catalogo \
  --remote-uri=https://github.com/alpinashop/alpinashop-catalogo.git \
  --connection=alpinashop-github \
  --region=europe-west1 --project=alpinashop-cicd

Fíjate en el paso 2: el token de GitHub va a Secret Manager, exactamente igual que la contraseña de la base de datos en 03-06. Un token con permiso de escritura sobre todos tus repositorios es una credencial de primer nivel, y merece el mismo tratamiento: almacenamiento cifrado, acceso concedido a una identidad concreta y rotación periódica. Anotar la fecha de caducidad del token en el calendario del equipo evita el clásico "el pipeline dejó de funcionar el martes y nadie sabe por qué".

Con la conexión creada, un disparador se define contra el recurso 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

Con GitLab el procedimiento es análogo: gcloud builds connections create gitlab, con un token de acceso de GitLab en Secret Manager. Y para GitLab autoalojado detrás de un firewall existe la conexión mediante un agente de servicio en la VPC, que es exactamente el caso que hace falta cuando el servidor de Git es interno.

  1. Workload Identity Federation: acceder a GCP sin claves JSON

Hay un segundo sentido de la conexión, y es el que más problemas de seguridad causa: que las GitHub Actions accedan a recursos de GCP. AlpinaShop lo necesita para que un workflow pueda publicar en Artifact Registry o consultar BigQuery sin depender de Cloud Build.

La forma antigua de hacerlo, y por qué es mala:

# NO HAGAS ESTO
gcloud iam service-accounts keys create clave.json \
  [email protected]
# ...y pegar el contenido de clave.json en un secreto de GitHub

Esa clave JSON es una credencial permanente, sin caducidad y transferible. Quien la obtiene es esa cuenta de servicio, para siempre, desde cualquier lugar del mundo. No hay forma de saber si se ha copiado. Se filtra en logs, en capturas de pantalla, en commits accidentales, y su rotación es un proceso manual que nadie hace. Las claves JSON de cuenta de servicio son, con diferencia, la causa más común de incidentes de seguridad graves en GCP.

Workload Identity Federation elimina el problema de raíz. La idea, que conecta directamente con lo visto en 03-04:

GCP confía en el emisor de tokens de GitHub. Cuando una GitHub Action se ejecuta, GitHub le entrega un token OIDC de corta duración que dice "soy el workflow X del repositorio Y de la organización Z". GCP verifica ese token, comprueba que coincide con una condición que tú has definido, y lo intercambia por credenciales temporales.

flowchart LR
    A[GitHub Action<br/>en ejecución] -->|1. pide token OIDC| B[Emisor OIDC<br/>de GitHub]
    B -->|2. token firmado:<br/>repo, rama, workflow| A
    A -->|3. presenta el token| C[Workload Identity Pool<br/>en GCP]
    C -->|4. verifica firma<br/>y condición de atributos| D{¿Coincide?}
    D -->|Sí| E[Credencial temporal<br/>de la cuenta de servicio]
    D -->|No| F[Rechazado]
    E --> G[Acceso a Artifact Registry,<br/>BigQuery, etc.]

La configuración completa:

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

# 1. Crear el pool de identidades de carga de trabajo
gcloud iam workload-identity-pools create github-pool \
  --location=global --display-name="GitHub Actions" --project=$PROY

# 2. Crear el proveedor OIDC, con la condición de atributos
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. Permitir que SOLO el repositorio del catálogo suplante a la cuenta de servicio
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 paso 3 es el que hay que entender bien. El principalSet con attribute.repository/alpinashop/alpinashop-catalogo significa que solo los workflows de ese repositorio concreto pueden obtener credenciales de esa cuenta de servicio. Un workflow de otro repositorio de la misma organización presenta un token válido pero con otro repository, no coincide con el principalSet, y es rechazado.

Si además quieres restringir por rama —que solo main pueda desplegar—, se usa attribute.ref:

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

Y así se consume desde el workflow:

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

permissions:
  contents: read
  id-token: write        # imprescindible: permite pedir el token 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]'
          # Fíjate: NO hay ninguna clave, ningún secreto, ningún 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ínea id-token: write en permissions es obligatoria y se olvida constantemente; sin ella, GitHub no emite el token OIDC y la autenticación falla con un mensaje que no ayuda demasiado.

Aspecto Clave JSON Workload Identity Federation
Caducidad Ninguna Minutos
Si se filtra Acceso permanente Inútil fuera de contexto
Rotación Manual, nadie la hace Automática por diseño
Ámbito Cualquiera que la tenga Solo el repositorio/rama declarado
Auditoría "La cuenta X hizo algo" Qué repositorio y qué workflow
Trabajo de mantenimiento Alto Ninguno

La regla práctica: si estás creando una clave JSON de cuenta de servicio en 2026, hay casi siempre una alternativa mejor. Dentro de GCP, cuentas de servicio adjuntas al recurso. Fuera de GCP, federación de identidad. Las claves JSON quedan para casos residuales, y en esos casos deben tener caducidad, rotación y una organization policy que limite dónde pueden crearse —tema de 07-07—.

  1. Tabla de decisión: CSR, GitHub o GitLab

La comparación sin adornos, que es lo que AlpinaShop necesita para decidir:

Criterio Cloud Source Repositories GitHub GitLab
Estado En desuso, sin novedades Activo, líder de mercado Activo, fuerte en autoalojado
Coste Gratuito hasta un límite bajo de almacenamiento y datos Gratis en privado; de pago por funciones avanzadas Igual; edición comunitaria autoalojable gratis
Revisión de código Prácticamente inexistente Pull requests maduros, sugerencias, propietarios de código Merge requests, muy completos
CI/CD propio No, depende de Cloud Build GitHub Actions, ecosistema enorme GitLab CI, muy potente e integrado
Integración con GCP Nativa, permisos IAM Excelente (2ª gen + federación) Muy buena (2ª gen + federación)
Ecosistema y plantillas Nulo El mayor con diferencia Amplio
Incidencias y proyectos No Sí Sí, incluido DevSecOps
Autoalojado No aplica GitHub Enterprise Server Su punto fuerte
Dependencia de proveedor Alta con Google Media (Git es portable) Baja, se puede autoalojar
Contratación de talento Nadie lo conoce Todo el mundo lo conoce Muy conocido

La decisión de AlpinaShop es GitHub, por cuatro razones ordenadas por peso:

  1. La revisión de código es el objetivo principal. El problema de AlpinaShop no es dónde guardar bytes: es que nadie mira los cambios antes de que lleguen a producción. GitHub resuelve eso; CSR, no.
  2. Cualquier persona que se incorpore ya sabe usarlo. Con un equipo de tres personas técnicas, el tiempo de formación cuenta.
  3. La integración con GCP es de primera clase gracias a la 2ª generación y a la federación de identidad. La supuesta ventaja de CSR ha dejado de serlo.
  4. La dependencia es baja de verdad. Un repositorio Git es portable: git clone --mirror y estás en otro sitio. Lo que sí ata son las incidencias, los pull requests y los workflows de Actions, y por eso el pipeline principal de AlpinaShop vive en cloudbuild.yaml —un fichero neutral— en lugar de en GitHub Actions.

Cuándo elegirías GitLab en su lugar: si necesitas autoalojar por requisitos regulatorios, si quieres un CI/CD integrado sin depender de Cloud Build, o si valoras tener incidencias, código, CI y registro de contenedores en un único producto. Es una elección perfectamente defendible.

Cuándo seguirías con CSR: solo si ya lo usas y funciona. Migrar por migrar no es una prioridad; migrar cuando se necesite revisión de código en serio, sí.

  1. Estrategia de ramas para un equipo pequeño

Con el repositorio decidido, la siguiente pregunta es cómo se organiza el trabajo dentro de él. Hay dos escuelas dominantes.

GitFlow define ramas de larga vida: main (lo que está en producción), develop (integración), feature/*, release/* y hotfix/*. Un cambio nace en una rama de característica, se fusiona a develop, entra en una rama de versión, se estabiliza y por fin llega a main.

Trunk-based define una única rama de larga vida, main, siempre desplegable. Los cambios se hacen en ramas cortas —horas o un par de días como mucho— que se fusionan a main en cuanto pasan la revisión y la CI.

Aspecto GitFlow Trunk-based
Ramas de larga vida main y develop Solo main
Vida de una rama de trabajo Días o semanas Horas o un par de días
Dolor al fusionar Alto: divergen mucho Bajo: divergen poco
Encaja con CI Mal: se integra tarde Es la premisa de la CI
Complejidad Alta Baja
Idóneo para Versiones empaquetadas, varios mantenidos a la vez Servicios web con despliegue continuo
Funciones a medias Rama larga sin fusionar Feature flags

AlpinaShop elige trunk-based, y las razones son concretas:

  • Es un servicio web, no un producto empaquetado. No hay clientes con la versión 2.3 que necesiten un parche mientras se desarrolla la 3.0. Solo hay una versión: la que está en alpinashop-prod.
  • El equipo son tres personas. La coordinación que justifica GitFlow no existe cuando cabéis en una llamada.
  • Es coherente con la CI de 06-01. La "I" de integración continua significa integrar continuamente; una rama de tres semanas es exactamente lo contrario, y el pipeline que construiste solo aporta valor si los cambios llegan a main a menudo.
  • Los conflictos de fusión crecen de forma no lineal con el tiempo. Dos ramas de un día casi nunca chocan; dos ramas de tres semanas siempre lo hacen, y resolver ese conflicto es donde se cuelan los errores.

El flujo de trabajo diario resultante:

git switch main && git pull                         # 1. Partir siempre de main actualizado
git switch -c feat/filtro-por-marca                 # 2. Rama corta y con nombre claro
# ... trabajo, commits pequeños ...
git push -u origin feat/filtro-por-marca            # 3. Publicar y abrir el pull request
# 4. La CI de 06-01 se ejecuta sola sobre el PR
# 5. Marta o Dani revisan
# 6. Fusión con squash a main → se despliega en alpinashop-dev
git switch main && git pull
git branch -d feat/filtro-por-marca                 # 7. Borrar la rama

La objeción evidente: ¿y si una funcionalidad tarda tres semanas? La respuesta de trunk-based no es "mantén la rama tres semanas", sino fusionar código incompleto pero inactivo, protegido por un interruptor:

# catalogo/config.py — el interruptor se lee del entorno, no del código
NUEVO_BUSCADOR = os.environ.get("FEATURE_NUEVO_BUSCADOR", "false").lower() == "true"

# catalogo/rutas.py
@app.get("/buscar")
def buscar():
    if NUEVO_BUSCADOR:
        return buscador_semantico(request.args["q"])   # 05-06, en pruebas
    return buscador_clasico(request.args["q"])         # lo que funciona hoy

El código nuevo llega a main y a producción desde el primer día, desactivado. Se activa en alpinashop-dev para probarlo, después para un porcentaje de usuarios, y por fin para todos. Y si algo va mal, desactivarlo es cambiar una variable de entorno, no revertir un despliegue. Es una vuelta atrás de segundos.

La contrapartida honesta: los interruptores acumulan complejidad y ramas muertas en el código. La disciplina asociada es borrarlos en cuanto la funcionalidad está consolidada, con una fecha de caducidad apuntada en el propio pull request que los introduce.

  1. Qué debe y qué no debe estar en el repositorio

Una regla útil como punto de partida: si es texto que describe cómo funciona o cómo se construye el sistema, va al repositorio.

Sí va al repositorio Por qué
Código de la aplicación Evidente
Dockerfile y .dockerignore La imagen debe ser reproducible desde el código
cloudbuild.yaml y variantes El pipeline se revisa como el código (06-01)
Manifiestos de Kubernetes Hoy están en Drive; ahí no se revisan ni se versionan
Terraform (.tf) La infraestructura como código llega en 06-07
Pruebas Sin ellas la CI no comprueba nada
requirements.txt con versiones fijadas Reproducibilidad de las dependencias
Migraciones de base de datos Cambios de esquema revisables y ordenados
Definiciones de pipelines de datos y ML Los componentes KFP de 05-07
README y decisiones de arquitectura DA-001, DA-002 y DA-003 tienen que estar escritas
Configuración no sensible por entorno dev.tfvars, prod.tfvars
No va al repositorio Dónde va Por qué
Contraseñas, tokens, claves de API Secret Manager (03-06) Git no olvida nunca
Claves JSON de cuenta de servicio No deberían existir (apartado 5) —
Certificados y claves privadas Secret Manager o KMS Igual que arriba
Estado de Terraform (.tfstate) Bucket de Cloud Storage (06-07) Contiene valores sensibles
Datos de clientes, volcados de BD Cloud Storage con control de acceso RGPD
Artefactos compilados, imágenes Artifact Registry El repositorio no es un almacén binario
Ficheros grandes (modelos, CSV) Cloud Storage o Git LFS Hinchan el repositorio para siempre
.env con valores reales Variables de entorno o Secret Manager Se filtra al hacer git add .

La frase clave sobre los secretos merece subrayarse: Git no olvida. Si subes una contraseña y la borras en el commit siguiente, sigue estando en el historial, en cada clon que alguien haya hecho y en cada réplica. Reescribir la historia es doloroso y nunca es completo. La única respuesta correcta ante un secreto filtrado es rotarlo inmediatamente, y solo después limpiar el historial si merece la pena.

Un .gitignore sensato es la primera línea de defensa:

# Secretos y credenciales
*.env
.env.*
*-key.json
*credentials*.json
*.pem
*.p12

# Estado de Terraform: NUNCA a Git
*.tfstate
*.tfstate.*
.terraform/
*.tfvars.secret

# Entornos y artefactos locales
venv/
__pycache__/
*.pyc
.pytest_cache/
dist/
build/

# Datos
*.csv
*.parquet
datos/

Pero la defensa real es automática, y es el tema del apartado 12.

  1. La estructura de repositorios de AlpinaShop

El debate monorepo frente a polirepo tiene mucha literatura y poca respuesta universal:

Aspecto Monorepo Polirepo
Cambio que cruza componentes Un solo PR atómico Varios PR coordinados
Permisos por componente Difícil de acotar Natural
CI Necesita filtros por ruta Simple
Descubrimiento de código Todo a la vista Hay que saber dónde mirar
Dependencias entre partes Fáciles de compartir, fáciles de enredar Fronteras explícitas
Herramientas necesarias Más Menos

Para un equipo de tres personas, la complejidad de un monorepo con herramientas de construcción avanzadas no se justifica. Pero cuarenta repositorios diminutos tampoco. AlpinaShop se queda en el punto intermedio: cuatro repositorios agrupados por ciclo de vida y por quién los toca.

Repositorio Contenido Quién lo mantiene Pipeline
alpinashop-catalogo Flask, Dockerfile, manifiestos de tienda, cloudbuild.yaml Dani catalogo-main, catalogo-pr
alpinashop-infra Terraform de la VPC, balanceador, IAM, Cloud SQL, buckets Marta plan en PR, apply con aprobación
alpinashop-datos Pipelines de Dataflow, consultas SQL, DAG/Workflows de 04-06 Lucía Pruebas + despliegue de plantillas
alpinashop-ml Componentes KFP, código de entrenamiento, notebooks limpios Lucía y Dani cloudbuild-ml.yaml de 06-01

El criterio de división no es técnico, es de ritmo y de responsabilidad. El catálogo cambia varias veces al día y lo toca Dani. La infraestructura cambia una vez por semana, requiere aprobación de Marta y su pipeline es completamente distinto —un plan que se revisa, no unas pruebas que pasan—. Meterlos juntos obligaría a ejecutar el pipeline de infraestructura por cada cambio de una plantilla HTML.

Y una advertencia sobre el repositorio de infraestructura: es el más peligroso de los cuatro. Quien puede fusionar un PR ahí puede modificar reglas de firewall, políticas de IAM y bases de datos. Su protección de ramas debe ser la más estricta, y sus revisores obligatorios deben incluir siempre a alguien de gcp-seguridad@.

  1. Revisión de código y protección de ramas

Que exista un repositorio no impide que alguien empuje directamente a main sin que nadie lo mire. Eso lo impide la protección de ramas.

Las reglas que AlpinaShop activa sobre main en los cuatro repositorios:

Regla Efecto Por qué
Prohibido el push directo Todo cambio pasa por pull request Sin esto, el resto es decorativo
Al menos una aprobación Otra persona ha mirado el cambio El objetivo principal de la lección
La CI debe estar en verde No se fusiona con pruebas rojas Da sentido al pipeline de 06-01
Aprobaciones caducan al haber cambios nuevos No se aprueba una versión y se fusiona otra Evita un truco muy usado
Historial lineal Fusión con squash o rebase Historia legible, revertir es trivial
Nadie se salta las reglas, ni los administradores Sin excepciones Las excepciones se vuelven norma
Escaneo de secretos Bloquea el push si detecta credenciales Última red antes del desastre

En el repositorio alpinashop-infra se añade una regla más: revisor obligatorio del equipo de seguridad, mediante el fichero CODEOWNERS:

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

Cualquier PR que toque IAM, reglas de firewall o el directorio de producción exige la aprobación de alguien de gcp-seguridad@. Es el equivalente en el código de la separación de funciones de 03-04.

Qué mirar en una revisión, en orden de importancia real: primero, ¿es correcto? —¿hace lo que dice, contempla los casos límite—; segundo, ¿es seguro? —secretos, validación de entradas, permisos que se amplían—; tercero, ¿está probado? —¿hay una prueba que fallaría sin este cambio—; cuarto, ¿se entiende? —lo va a leer alguien dentro de un año—; y por último, el estilo, que debería estar automatizado y no discutirse en la revisión. Ese último punto ahorra una cantidad enorme de fricción: si el formateo lo decide una herramienta, nadie discute sobre comas.

Y dos normas de convivencia que valen más que cualquier lista: pull requests pequeños —un PR de 40 líneas recibe comentarios útiles, uno de 2.000 recibe un "lgtm"— y revisiones rápidas, porque un PR que espera dos días bloquea a quien lo escribió y acumula conflictos.

  1. Mensajes de commit y versionado semántico

Un historial de commits con "cambios", "fix", "otra vez" y "ahora sí" no sirve para nada. Un historial bueno responde a por qué se hizo cada cosa, que es lo que no se puede deducir del código.

AlpinaShop adopta Conventional Commits, un formato sencillo con un beneficio grande:

<tipo>(<ámbito>): <resumen en imperativo, menos de 72 caracteres>

<cuerpo: POR QUÉ se hizo, no qué se hizo — eso ya está en el diff>

<pie: referencias, cambios incompatibles>
feat(buscador): añadir filtro por marca en el catálogo

El 18 % de las búsquedas incluían un nombre de marca en el texto
libre, lo que producía resultados pobres. El filtro reduce la
consulta a la marca antes de aplicar el ranking.

Refs: #142
fix(pedidos): reintentar la conexión a Cloud SQL tras un fallo de red

El pool de conexiones no recuperaba tras un corte breve y la
aplicación devolvía 500 hasta que se reiniciaba el pod. Se añade
reintento con retroceso exponencial, coherente con 04-04.

Refs: #158

Los tipos habituales: feat (funcionalidad nueva), fix (corrección), docs, refactor, test, chore (mantenimiento), perf y ci.

El beneficio concreto: el formato es analizable por máquinas. De ahí salen automáticamente el registro de cambios y la versión siguiente, según versionado semántico (MAYOR.MENOR.PARCHE):

Tipo de commit Incremento Ejemplo
fix: PARCHE 2.4.1 → 2.4.2
feat: MENOR 2.4.1 → 2.5.0
BREAKING CHANGE: en el pie o feat!: MAYOR 2.4.1 → 3.0.0

Y aquí hay un matiz honesto que conviene decir: el versionado semántico está pensado para artefactos que otros consumen —bibliotecas, APIs públicas—. Para el catálogo web de AlpinaShop, que es un servicio interno desplegado continuamente, la versión importa mucho menos que el SHA del commit, que ya es un identificador exacto. AlpinaShop usa etiquetas semánticas para marcar candidatas a producción —es el disparador por etiqueta de 06-01— y para tener nombres pronunciables en una reunión, no como mecanismo de compatibilidad.

  1. Ganchos de calidad antes del commit

Un fallo detectado en la CI cuesta minutos de espera. El mismo fallo detectado antes de hacer el commit cuesta segundos. Los ganchos de pre-commit ejecutan comprobaciones rápidas en local antes de que el commit se cree.

# .pre-commit-config.yaml, en la raíz de cada repositorio
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 subir modelos y 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                          # errores y estilo
        args: [--fix]
      - id: ruff-format                   # formateo automático

  - repo: https://github.com/gitleaks/gitleaks
    rev: v8.21.0
    hooks:
      - id: gitleaks                      # EL MÁS IMPORTANTE: busca secretos
pip install pre-commit
pre-commit install          # instala el gancho en .git/hooks
pre-commit run --all-files  # primera pasada sobre todo el repositorio

El gancho de gitleaks es el que justifica todo el mecanismo por sí solo. Busca patrones de credenciales —claves de API, tokens, claves privadas, claves JSON de GCP— y bloquea el commit si encuentra alguno. Recuerda lo del apartado 8: Git no olvida. Es infinitamente más barato bloquear el commit que rotar una clave de la pasarela de pago filtrada.

Dos advertencias sobre los ganchos locales. La primera: son opcionales por naturaleza, porque cualquiera puede saltárselos con git commit --no-verify. Por eso las mismas comprobaciones deben repetirse en la CI, donde no se pueden esquivar. Los ganchos son comodidad para quien desarrolla, no un control de seguridad. La segunda: deben ser rápidos. Un gancho que tarda treinta segundos hace que todo el mundo aprenda a usar --no-verify. Lo lento —la suite completa de pruebas— va en la CI, no aquí.

  1. Conectar los repositorios con los disparadores de Cloud Build

El cierre de la lección es unir lo de 06-01 con lo de aquí. Este es el mapa completo de AlpinaShop:

flowchart TD
    A[Dani abre PR<br/>feat/filtro-por-marca] --> B[Disparador catalogo-pr]
    B --> C[cloudbuild-pr.yaml:<br/>instalar, pytest, ruff, bandit]
    C --> D{¿Verde?}
    D -->|No| E[PR bloqueado<br/>por protección de rama]
    D -->|Sí| F[Marta revisa]
    F --> G[Squash a main]
    G --> H[Disparador catalogo-main]
    H --> I[cloudbuild.yaml:<br/>construir + publicar :SHA]
    I --> J[Despliegue en<br/>alpinashop-dev]
    J --> K{Etiqueta v2.5.0}
    K --> L[Disparador de promoción<br/>con aprobación]
    L --> M[alpinashop-prod]

Los cuatro disparadores que AlpinaShop deja configurados:

Disparador Repositorio Evento Configuración Qué hace
catalogo-pr alpinashop-catalogo PR contra main cloudbuild-pr.yaml Prueba, no despliega
catalogo-main alpinashop-catalogo Push a main cloudbuild.yaml Construye, publica, despliega en dev
catalogo-prod alpinashop-catalogo Etiqueta ^v\d+\.\d+\.\d+$ cloudbuild-prod.yaml, --require-approval Promociona a producción
ml-main alpinashop-ml Push a main cloudbuild-ml.yaml Compila y lanza el pipeline de Vertex AI

Y una comprobación final que merece la pena hacer explícita: el disparador de producción se activa al crear una etiqueta, así que quien pueda crear etiquetas puede iniciar un despliegue a producción. La protección de etiquetas es tan necesaria como la de ramas:

# Regla de protección de etiquetas en GitHub
Patrón: v*
Quién puede crear: solo el equipo @alpinashop/infraestructura

Sin esa regla, la aprobación manual de 06-01 sigue protegiendo el despliegue —nadie llega a producción sin que Marta apruebe—, pero cualquiera podría llenar la cola de compilaciones pendientes. Con ella, el camino a producción está acotado de principio a fin.

Errores Comunes y Consejos

Empezar un proyecto nuevo con Cloud Source Repositories en 2026. Está en desuso, no recibe funcionalidad nueva y carece de revisión de código, que es justo lo que se necesita. Conócelo para operar lo heredado; elige GitHub o GitLab para lo nuevo.

Crear claves JSON de cuenta de servicio para la CI. Credenciales permanentes y transferibles, la causa más común de incidentes graves en GCP. Usa Workload Identity Federation. Y si encuentras claves JSON antiguas por la infraestructura, planifica su eliminación como si fuera deuda de seguridad, porque lo es.

Olvidar id-token: write en los permisos del workflow de GitHub Actions. Sin esa línea no se emite el token OIDC y la autenticación falla con un error confuso. Es el primer tropiezo de todo el mundo.

Ramas de característica que viven semanas. Cada día que una rama vive aumenta la probabilidad de un conflicto doloroso y retrasa la señal de la CI. Ramas cortas e interruptores de funcionalidad para lo que no está listo.

Subir un secreto y "arreglarlo" borrándolo en el commit siguiente. Sigue en el historial y en todos los clones. Rota el secreto inmediatamente; la limpieza del historial es secundaria.

Confiar solo en los ganchos de pre-commit. Se saltan con --no-verify. Repite siempre las comprobaciones en la CI, donde son obligatorias.

Pull requests enormes. Un PR de 2.000 líneas no se revisa, se aprueba. Divide en cambios pequeños con una intención cada uno; la calidad de los comentarios es inversamente proporcional al tamaño del diff.

Discutir estilo en las revisiones. Automatiza el formateo con ruff-format o equivalente y dedica la revisión a lo que una máquina no puede evaluar: si el enfoque es correcto y si es seguro.

No proteger las etiquetas. Si el despliegue a producción se dispara con una etiqueta, quien crea etiquetas inicia despliegues. Protégelas igual que las ramas.

Consejo final: la migración se hace por partes. No intentes llevar los cuatro repositorios a GitHub el mismo día. Empieza por el catálogo, que es el que más se toca, monta su pipeline, comprueba que el equipo se ha adaptado, y después ve con la infraestructura. Y empieza por sacar de Drive los manifiestos de Kubernetes: es la media hora de trabajo con mejor relación beneficio/coste de todo el módulo.

Ejercicios

Ejercicio 1: rescatar el código disperso

Haz un plan concreto de tres fases para llevar al control de versiones todos los activos dispersos de AlpinaShop: el repositorio local de Dani, los startup scripts en los metadatos de las plantillas de instancia, los manifiestos de Kubernetes en Drive, los notebooks de Lucía y los comandos de gcloud de Marta. Para cada activo, indica a qué repositorio va, qué hay que revisar antes de subirlo y qué riesgo concreto tiene esa subida.

Ejercicio 2: configurar el acceso de GitHub Actions con mínimo privilegio

AlpinaShop quiere un workflow de GitHub Actions en alpinashop-datos que, al hacer push a main, valide las consultas SQL ejecutando un dry run contra BigQuery en alpinashop-datos y publique un informe en el bucket alpinashop-datalake. Escribe la configuración de Workload Identity Federation y los roles IAM exactos, y explica qué impide que un workflow del repositorio alpinashop-catalogo use esa misma identidad.

Ejercicio 3: elegir estrategia de ramas con un caso incómodo

AlpinaShop firma un contrato con una cadena de tiendas que necesita una versión personalizada del catálogo, con su propio diseño y algunas reglas de precios distintas, mantenida durante al menos dos años en paralelo a la versión pública. Dani propone crear una rama cliente-x de larga vida. Analiza la propuesta, di si trunk-based sigue siendo válido y propón la solución que recomendarías, con sus contrapartidas.

Soluciones

Solución 1

Fase 1 — Rescatar lo que corre riesgo de perderse (esta semana).

Activo Destino Revisar antes Riesgo
Repositorio local de Dani alpinashop-catalogo Historial completo en busca de secretos Alto: si en algún commit antiguo hay una contraseña, subirlo la publica al equipo
Manifiestos de Drive alpinashop-catalogo/k8s/ Qué versión es la que está realmente aplicada Medio: hay tres versiones y no se sabe cuál corre
Startup scripts alpinashop-infra/scripts/ Credenciales embebidas en el script Alto: los startup scripts son un sitio clásico para pegar contraseñas

Para el repositorio de Dani, el procedimiento correcto es escanear el historial completo, no solo el estado actual:

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

Si aparecen secretos, hay dos caminos. El pragmático: rotar todas las credenciales encontradas y subir el historial tal cual, aceptando que contiene valores ya inválidos. El purista: reescribir el historial con git filter-repo. Para AlpinaShop recomiendo el pragmático —rotar es obligatorio en ambos casos, y la reescritura aporta poco si los valores ya no sirven—, salvo que haya datos personales, donde la reescritura sí es necesaria.

Para los manifiestos, la comprobación imprescindible es qué está aplicado de verdad, no qué fichero se llama FINAL:

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

Se limpian los campos generados por el clúster (status, metadata.uid, resourceVersion, anotaciones de última configuración) y eso es la versión que va al repositorio. La verdad está en el clúster, no en Drive.

Fase 2 — Estructurar y proteger (semana siguiente). Crear los cuatro repositorios, activar la protección de ramas y CODEOWNERS, instalar los ganchos de pre-commit con gitleaks y conectar los disparadores de Cloud Build.

Fase 3 — Lo difícil (siguientes semanas).

Activo Destino Cómo Riesgo
Notebooks de Lucía alpinashop-ml/notebooks/ Limpiar salidas con nbstripout; extraer lo reutilizable a módulos Alto: los notebooks suelen contener datos de clientes en las salidas
Comandos de gcloud de Marta alpinashop-infra/ como Terraform No transcribir comandos: exportar el estado real, 06-05 y 06-07 Medio: laborioso, pero sin pérdida

El detalle de los notebooks merece énfasis: una celda ejecutada con df.head() sobre alpinashop_analitica deja nombres, correos y direcciones guardados dentro del .ipynb. Subir un notebook sin limpiar salidas es una fuga de datos personales al repositorio, con Git no olvidando nada. nbstripout como gancho de pre-commit lo resuelve de forma automática y es no negociable en alpinashop-ml.

Y la observación sobre los comandos de Marta: transcribirlos a un script sería el error. Ese historial está incompleto y contiene comandos que se ejecutaron y después se deshicieron. Lo correcto es exportar la configuración real de la infraestructura viva, que es exactamente el procedimiento de 06-05.

Solución 2

Configuración de la federación, reutilizando el pool ya creado:

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

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

# Solo el repositorio alpinashop-datos, y solo la rama 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

Roles exactos, aplicando mínimo privilegio de verdad:

Rol Ámbito Por qué ese y no otro
roles/bigquery.jobUser Proyecto alpinashop-datos Permite lanzar trabajos; un dry run no lee datos
roles/bigquery.metadataViewer Dataset alpinashop_analitica El dry run necesita los esquemas para validar
roles/storage.objectCreator Bucket alpinashop-datalake, prefijo de informes Crear, no leer ni borrar

Fíjate en lo que no lleva: ni bigquery.dataViewer —un dry run valida sintaxis y esquema, no ejecuta la consulta ni lee una sola fila— ni storage.objectAdmin —crear informes no requiere poder borrar los existentes—. Y el objectCreator acotado al prefijo mediante una condición IAM (03-04) impide que un workflow comprometido sobrescriba datos del data lake.

Qué impide que un workflow de alpinashop-catalogo use esta identidad: el principalSet está atado a attribute.repository/alpinashop/alpinashop-datos. El token OIDC que GitHub emite lleva firmado el nombre del repositorio de origen, y GitHub no permite falsificarlo —lo emite su propio servicio, no el workflow—. Un workflow de alpinashop-catalogo presentaría un token con repository: alpinashop/alpinashop-catalogo, que no coincide con el principalSet, y GCP rechazaría el intercambio. Esa es la propiedad que hace que la federación sea sustancialmente más segura que una clave JSON: la identidad la certifica un tercero de confianza y está atada al contexto de ejecución, no a la posesión de un fichero.

Y una mejora adicional: el attribute-condition del proveedor (assertion.repository_owner == 'alpinashop') actúa como segunda barrera a nivel de proveedor. Sin ella, cualquier repositorio de GitHub en el mundo podría al menos intentar el intercambio, y aunque el principalSet lo rechazaría igualmente, es mejor filtrarlo antes. Defensa en profundidad.

Solución 3

La propuesta de la rama de larga vida es mala, y conviene explicar por qué antes de proponer alternativa. Una rama cliente-x mantenida dos años produce, con total certeza:

  • Divergencia creciente: cada corrección de seguridad hay que aplicarla dos veces, y en algún momento una se olvidará. Ese olvido será justo el importante.
  • Fusiones cada vez más caras: a los seis meses, traer los cambios de main a cliente-x es un proyecto en sí mismo.
  • Duplicación de todo lo demás: dos pipelines, dos conjuntos de imágenes, dos despliegues, dos series de métricas.
  • Un sistema que nadie prueba entero: la CI probaría main, y cliente-x quedaría con pruebas viejas.

Pero trunk-based sigue siendo válido, porque el problema real no es de estrategia de ramas: es de arquitectura. La pregunta correcta no es "¿cómo mantengo dos ramas?", sino "¿cómo hago que una sola base de código sirva a dos clientes?".

La solución recomendada: multi-inquilino por configuración. Una única rama main, una única imagen, y el comportamiento distinto resuelto en tiempo de ejecución:

# catalogo/inquilino.py
@dataclass(frozen=True)
class Inquilino:
    id: str
    tema: str                  # plantillas y hojas de estilo
    reglas_precio: str         # estrategia de precios a aplicar
    dominio: str

INQUILINOS = {
    "publico":  Inquilino("publico",  "alpina",   "estandar",  "www.alpinashop.example"),
    "cliente-x": Inquilino("cliente-x", "clientex", "mayorista", "tienda.cliente-x.example"),
}

def inquilino_actual() -> Inquilino:
    # Resolución por dominio: el mismo despliegue sirve a los dos
    return INQUILINOS.get(DOMINIOS.get(request.host), INQUILINOS["publico"])

Lo que cambia por inquilino son datos y configuración: el tema visual, las reglas de precio, el dominio, quizá el subconjunto de catálogo. Nada de eso justifica una rama.

Enfoque Coste de mantenimiento Riesgo de divergencia Complejidad del código ¿Recomendado?
Rama de larga vida Muy alto y creciente Certero Baja No
Multi-inquilino por configuración Bajo Ninguno Media Sí
Repositorio y producto separados Muy alto N/A: son productos distintos Baja Solo si divergen de verdad
Núcleo compartido como biblioteca Medio Media Alta Si hay muchos clientes

Las contrapartidas, dichas con honestidad. El multi-inquilino no es gratis: introduce condicionales en el código, hay que probar los dos caminos, y existe un riesgo nuevo y serio —una fuga entre inquilinos, que el cliente X vea datos del público o al revés—. Eso exige que el identificador de inquilino se aplique en la capa de acceso a datos y se pruebe explícitamente, no que se confíe en que cada consulta se acuerde de filtrar.

Y hay un punto de ruptura que hay que reconocer: si dentro de un año el cliente X pide un flujo de compra completamente distinto, un catálogo con otra estructura y un proceso de facturación propio, ya no son dos configuraciones del mismo producto, sino dos productos. En ese momento la separación es correcta, y se hace como productos separados con una biblioteca común, no como una rama.

La regla que se lleva uno de aquí: las ramas sirven para separar el trabajo en el tiempo, no para separar variantes en el espacio. Cuando alguien propone una rama de larga vida para una variante de producto, la señal es que hay una decisión de arquitectura pendiente de tomar. Y la conversación con comercial también importa: si el contrato con el cliente X permite algo de flexibilidad en el diseño y en las reglas de precio, el multi-inquilino sale casi gratis; si exige un producto completamente distinto, el coste real de ese contrato es mucho mayor de lo que parecía al firmarlo, y eso conviene saberlo antes.

Conclusión

AlpinaShop tiene por fin un cimiento sobre el que construir.

Sabes qué aporta el control de versiones —historia, atribución, reversión, ramificación, revisión, identidad inmutable y automatización— y que las dos últimas son las que hacen posible todo lo de 06-01: sin repositorio remoto no hay $COMMIT_SHA, no hay disparador y no hay pipeline.

Conoces Cloud Source Repositories: repositorios Git privados en GCP, gcloud source repos clone con autenticación por identidad de gcloud, permisos gobernados por IAM con los mismos grupos de 03-04, búsqueda de código e integración nativa con Cloud Build y Error Reporting. Y sabes la verdad incómoda: está en desuso, no admite clientes nuevos, no recibe funcionalidad y nunca tuvo revisión de código, que es lo que de verdad se necesita. Lo conoces para operar infraestructuras heredadas y para migrarlas con git clone --mirror, no para empezar nada nuevo.

Sabes conectar GitHub o GitLab con GCP usando la 2ª generación de repositorios de Cloud Build —conexión y repositorio como recursos declarables, token en Secret Manager— y por qué es preferible a la aplicación de GitHub de la primera generación. Y sobre todo sabes usar Workload Identity Federation para que las GitHub Actions accedan a GCP sin ninguna clave JSON: el pool, el proveedor OIDC, el attribute-condition, el principalSet atado a un repositorio y a una rama, y el id-token: write que todo el mundo olvida. Con la regla que resume el apartado: si en 2026 estás creando una clave JSON de cuenta de servicio, casi siempre hay una alternativa mejor.

Tienes la tabla de decisión honesta entre CSR, GitHub y GitLab, y la decisión de AlpinaShop —GitHub— con sus cuatro razones, junto a los casos en los que GitLab sería mejor elección. Tienes la estrategia de ramas: trunk-based con ramas cortas, porque es un servicio web con una sola versión viva, porque el equipo son tres personas, porque es la premisa misma de la integración continua y porque los conflictos crecen de forma no lineal con el tiempo. Con los interruptores de funcionalidad como respuesta a "esto tarda tres semanas", y con la disciplina de borrarlos después.

Sabes qué va al repositorio —código, Dockerfile, pipelines, manifiestos, Terraform, pruebas, migraciones, decisiones de arquitectura— y qué no —secretos, estado de Terraform, datos, artefactos, ficheros grandes—, con la advertencia que vale por toda la lección: Git no olvida, y ante un secreto filtrado la única respuesta correcta es rotarlo. Tienes la estructura de cuatro repositorios de AlpinaShop dividida por ritmo y responsabilidad, con alpinashop-infra señalado como el más peligroso. Tienes la protección de ramas con sus siete reglas, CODEOWNERS exigiendo revisión de seguridad para IAM y firewall, los mensajes de commit convencionales, el versionado semántico con su matiz honesto, y los ganchos de pre-commit con gitleaks como red de seguridad que no sustituye a la CI.

Y tienes los cuatro disparadores conectados de principio a fin: pull request que prueba, main que construye y despliega en desarrollo, etiqueta protegida que promociona a producción con aprobación, y el pipeline de ML compilándose solo.

Con esto, el catálogo se construye, se prueba y se despliega solo. Pero quedan piezas del sistema que no son ni una aplicación web ni un pipeline de datos: son reacciones a eventos. Cuando llega un mensaje al topic imagenes-subidas, algo tiene que despertar, llamar a la Vision API, escribir en BigQuery y volver a dormirse. Montar una máquina virtual o un pod para eso sería absurdo.

En 06-03 llegan las Cloud Functions, y con ellas se cierra por fin la punta suelta que dejó el módulo 5.

Curso de Google Cloud Platform (GCP)

Módulo 1: Introducción a Google Cloud Platform

Módulo 2: Servicios principales de GCP

Módulo 3: Redes y seguridad

Módulo 4: Datos y análisis

Módulo 5: Aprendizaje automático e IA

Módulo 6: DevOps y monitoreo

Módulo 7: Temas avanzados de GCP

Módulo 8: Proyecto final

© Copyright 2026. Todos los derechos reservados