Módulo 3: GitLab CI/CD y SDK Headless
GitLab CI/CD: Stages, Jobs, Artifacts
GitLab CI/CD: Stages, Jobs, Artifacts
Descripción
Esta cápsula te enseña el modelo arquitectural de GitLab CI/CD comparado con GitHub Actions. No es un tutorial básico de GitLab — es una vista comparativa con foco en lo que importa para correr Claude Code: estructura del pipeline, variables, artifacts, y el Docker executor que es la base de los jobs.
Si copias un workflow de GitHub Actions a GitLab cambiando solo la sintaxis, vas a tener problemas. Los modelos son conceptualmente distintos, no solo sintácticamente. Entender estas diferencias es la diferencia entre un pipeline frágil y uno que aprovecha bien la plataforma.
Al terminar, vas a poder leer un .gitlab-ci.yml con confianza, entender la jerarquía de stages/jobs/artifacts, y diseñar pipelines de GitLab que aprovechan sus ventajas (no que solo "imiten" GitHub Actions).
La Diferencia Fundamental: Modelos Mentales
GITHUB ACTIONS:
Workflow (1 archivo .yml)
└── Jobs (corren en máquinas separadas)
└── Steps (acciones secuenciales en el job)
GITLAB CI/CD:
Pipeline (1 archivo .gitlab-ci.yml)
└── Stages (fases ordenadas: build, test, deploy)
└── Jobs (corren en paralelo dentro de un stage)
└── Script (comandos shell)
La diferencia clave: GitLab tiene un nivel intermedio explícito — stages. En GitHub, "fases" se simulan con needs: entre jobs.
Comparación visual
GITHUB ACTIONS GITLAB CI/CD
Workflow Pipeline
├── Job: lint Stage: build
│ └── needs: [] ├── Job: lint
├── Job: test └── Job: typecheck
│ └── needs: [lint]
├── Job: build Stage: test
│ └── needs: [test] ├── Job: unit
└── Job: deploy ├── Job: integration
└── needs: [build] └── Job: e2e
Stage: deploy
├── Job: deploy-staging
└── Job: deploy-prod
(manual gate)
En GitLab, jobs en el mismo stage corren en paralelo. Stages se ejecutan en orden. Si un job de un stage falla, los stages siguientes no corren.
Estructura Básica de .gitlab-ci.yml
# Define las fases en orden
stages:
- build
- test
- deploy
# Variables disponibles en todos los jobs
variables:
PYTHON_VERSION: "3.11"
CLAUDE_MODEL: "claude-haiku-4-5"
# Image base para todos los jobs (override-able)
default:
image: python:3.11-slim
before_script:
- pip install --upgrade pip
# Jobs
lint:
stage: build
script:
- pip install ruff
- ruff check .
test:
stage: test
script:
- pip install pytest
- pytest
deploy:
stage: deploy
script:
- ./scripts/deploy.sh
only:
- main
Anatomía de un Job
job-name: # nombre único del job
stage: test # a qué stage pertenece
image: python:3.11-slim # override de la image
variables: # variables específicas del job
PYTEST_ARGS: "-v"
before_script: # comandos antes del script principal
- pip install pytest
script: # los comandos principales (lo que hace el job)
- pytest $PYTEST_ARGS
after_script: # cleanup, corre incluso si script falla
- echo "Job done"
artifacts: # archivos a preservar
paths:
- test-results/
expire_in: 7 days
rules: # cuándo corre este job
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
Variables: GitLab vs GitHub
GitLab provee variables con prefijo CI_ (no GITHUB_):
| GitHub Actions | GitLab CI/CD | Significado |
|---|---|---|
${{ github.repository }} | $CI_PROJECT_PATH | "owner/repo" |
${{ github.event.pull_request.number }} | $CI_MERGE_REQUEST_IID | Número del MR/PR |
${{ github.event.pull_request.head.sha }} | $CI_COMMIT_SHA | SHA del commit |
${{ github.event.pull_request.base.ref }} | $CI_MERGE_REQUEST_TARGET_BRANCH_NAME | Rama destino |
${{ github.actor }} | $GITLAB_USER_LOGIN | Username |
${{ secrets.X }} | $X (CI/CD variable) | Secrets/variables |
Nota: GitLab usa "Merge Request" (MR) en lugar de "Pull Request" (PR). Mismo concepto, distinto nombre.
CI/CD Variables (equivalente a GitHub Secrets)
En GitLab: Settings → CI/CD → Variables
Cada variable tiene flags importantes:
| Flag | Significado |
|---|---|
| Protected | Solo accesible en branches protected |
| Masked | Enmascarada en logs (como secret en GitHub) |
| Expanded | Si quieres que se expanda como ${OTRA_VAR} |
Recomendación: API keys siempre con Protected: true y Masked: true. Solo accesibles desde branches main/release.
Rules: El if de GitLab
GitLab usa rules (no if como GitHub) para condicionar la ejecución:
review-bot:
stage: review
script:
- python scripts/review.py
rules:
# Solo en MR events, no en push directo
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
when: on_success
# Si no es MR, skipear
- when: never
Rules comunes
rules:
# MR específicos
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
# Push a main
- if: $CI_COMMIT_BRANCH == "main"
# Cualquier push excepto a main
- if: $CI_COMMIT_BRANCH != "main"
# Solo si cambió cierto path
- changes:
- "src/**/*.py"
- "package.json"
# Manual (requiere click para ejecutar)
- if: $CI_COMMIT_BRANCH == "main"
when: manual
# Combinaciones
- if: $CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "main"
Diferencia importante con GitHub: las rules se evalúan en orden. La primera que matchea define el comportamiento. Por eso es común terminar con un - when: never como fallback.
Artifacts: Pasar Datos Entre Jobs
Los artifacts en GitLab son archivos que un job preserva y los stages siguientes pueden consumir.
extract-diff:
stage: prepare
script:
- git diff origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME...HEAD > pr_diff.txt
artifacts:
paths:
- pr_diff.txt
expire_in: 1 day
review:
stage: analyze
script:
- python scripts/review.py # tiene acceso a pr_diff.txt automáticamente
needs:
- extract-diff # asegura que el job previo termine antes
Diferencia con GitHub Actions
GITHUB ACTIONS:
→ Cada job es una máquina nueva
→ Para pasar archivos: actions/upload-artifact + actions/download-artifact
→ Explícito y verboso
GITLAB CI/CD:
→ artifacts: paths: [...] preserva archivos
→ Jobs siguientes los reciben automáticamente
→ Implícito, menos código
Casos de uso típicos
| Caso | Configuración |
|---|---|
| Coverage reports | paths: [coverage.xml], expire_in: 7 days |
| Build artifacts | paths: [dist/], expire_in: 30 days |
| Test results | reports: junit: results.xml (GitLab los renderiza) |
| Análisis de Claude | paths: [review_result.json], expire_in: 7 days |
Docker Executor: La Base de los Jobs
En GitLab CI/CD, los jobs siempre corren en containers. El Docker executor es lo que ejecuta cada job.
La image determina el environment
job:
image: python:3.11-slim # ← container donde corre el script
script:
- python --version # imprime "Python 3.11.x"
Custom images para Claude Code
Para CI con Claude Code, puedes crear una image custom que tenga todo precargado:
# .gitlab/Dockerfile.review
FROM python:3.11-slim
RUN pip install --no-cache-dir \
anthropic>=0.39.0,<1.0.0 \
requests \
python-gitlab
WORKDIR /workspace
COPY scripts/ /scripts/
# .gitlab-ci.yml
review:
image: registry.gitlab.com/$CI_PROJECT_PATH/review-image:latest
script:
- python /scripts/code_review.py
Ventaja: las dependencias se instalan una vez (al construir la image), no en cada job run. Pipeline más rápido.
Services (DBs, Redis, etc.)
test:
image: python:3.11-slim
services:
- postgres:15
- redis:7
variables:
POSTGRES_DB: testdb
POSTGRES_PASSWORD: testpass
script:
- pytest
services son containers adicionales que corren al lado del job. Útil para tests que necesitan DB real.
Caching para Velocidad
Como en GitHub Actions, el cache de dependencias acelera mucho:
cache:
key: $CI_COMMIT_REF_SLUG
paths:
- .pip-cache/
- node_modules/
review:
before_script:
- pip install --cache-dir=.pip-cache anthropic requests
script:
- python scripts/review.py
key define cómo se versiona el cache. $CI_COMMIT_REF_SLUG = nombre de la rama, así que cada rama tiene su cache. key: "global" sería un cache compartido.
Ejemplo Completo: Pipeline de Code Review
Pipeline que:
- Extrae el diff del MR
- Corre el SDK de Claude para analizar
- Postea comments al MR via API
stages:
- prepare
- analyze
- publish
variables:
PYTHON_VERSION: "3.11"
CLAUDE_MODEL: "claude-haiku-4-5"
default:
image: python:3.11-slim
cache:
key: pip-cache
paths:
- .pip-cache/
extract-diff:
stage: prepare
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
script:
- apt-get update && apt-get install -y git
- git fetch origin $CI_MERGE_REQUEST_TARGET_BRANCH_NAME
- git diff origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME...HEAD > pr_diff.txt
- echo "Diff size: $(wc -l < pr_diff.txt) lines"
artifacts:
paths:
- pr_diff.txt
expire_in: 1 day
claude-analysis:
stage: analyze
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
needs:
- extract-diff
before_script:
- pip install --cache-dir=.pip-cache anthropic
script:
- python scripts/code_review.py
artifacts:
paths:
- review_result.json
expire_in: 7 days
publish-review:
stage: publish
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
needs:
- claude-analysis
before_script:
- pip install --cache-dir=.pip-cache python-gitlab
script:
- python scripts/publish_to_mr.py
variables:
GITLAB_TOKEN: $CI_GITLAB_TOKEN
Estructura limpia:
- Stage
prepare→ extrae el diff - Stage
analyze→ corre el SDK de Claude (depende deprepare) - Stage
publish→ publica resultados (depende deanalyze)
Si cualquier stage falla, los siguientes no corren. Si todo va bien, los artifacts pasan automáticamente entre stages.
Trampas Comunes
Error 1: Asumir que if funciona como en GitHub
Síntoma: Pruebas if: $CI_PIPELINE_SOURCE == "merge_request_event" directo en el job y falla.
Por qué pasa: GitLab usa rules: con sub-key if:. No es if: directo.
Cómo corregir: Estructura correcta:
job:
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
when: on_success
Error 2: Variables sin Protected: true para keys
Síntoma: La API key se expone en runs de feature branches no protegidas.
Por qué pasa: Default de variables es accesibles desde cualquier branch.
Cómo corregir: Settings → CI/CD → Variables → ANTHROPIC_API_KEY → Editar → marcar "Protected" y "Masked".
Error 3: No usar needs: para dependencias entre jobs
Síntoma: Job que necesita un artifact de un job anterior corre antes y falla.
Por qué pasa: Sin needs:, los stages se ejecutan en orden pero los jobs dentro de un stage corren en paralelo.
Cómo corregir: Usar needs: [job-anterior] para forzar dependencia explícita y obtener artifacts.
Error 4: Image con dependencias pero before_script redundante
Síntoma: El pipeline tarda mucho instalando dependencias en cada job.
Por qué pasa: Cada job hace pip install, aunque las dependencias podrían estar en una image custom.
Cómo corregir: Para projects con muchos jobs que comparten deps, crear image custom con deps preinstaladas.
Error 5: Artifacts sin expire_in
Síntoma: Storage de la org crece sin parar, alcanza la quota.
Por qué pasa: Default es 30 días. Para artifacts efímeros (PR analysis), es excesivo.
Cómo corregir: Configurar expire_in: 7 days (o menos) en artifacts no críticos.
Diagnóstico
Pregunta 1: ¿Sabes la diferencia conceptual entre stages y jobs?
Stages = fases secuenciales. Jobs = unidades de trabajo (paralelas dentro de un stage). Sin entender esta diferencia, los pipelines de GitLab se sienten arbitrarios.
Pregunta 2: ¿Tu API key tiene `Protected: true` y `Masked: true`?
Si no, puedes exponerla en runs de feature branches o en logs.
Pregunta 3: ¿Usas `rules:` o estás intentando usar `only/except`?
only/except está deprecated. rules: es el approach moderno y más expresivo.
Pregunta 4: ¿Tus jobs que dependen de artifacts usan `needs:` explícitamente?
Sin needs:, GitLab puede correr jobs en paralelo aunque dependan entre sí (mismo stage).
Pregunta 5: ¿Configuraste `expire_in` en tus artifacts?
Si no, default 30 días puede saturar storage. Para CI de PRs, 7 days suele ser suficiente.
Ejercicios
Ejercicio 1: Pipeline mínimo (Fácil)
Crea un .gitlab-ci.yml con 2 stages (build, test) y un job en cada uno. Verifica que corren en orden cuando haces push.
Ejercicio 2: Variables y rules (Medio)
Configura una CI/CD variable MY_TOKEN (Protected, Masked). Haz un job que solo corra en MRs y use esa variable.
Ver solución
test-job:
stage: test
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
script:
- echo "Token disponible (enmascarado): $MY_TOKEN"
En Settings → CI/CD → Variables: agregar MY_TOKEN con flags Protected y Masked.
Ejercicio 3: Pipeline completo de code review (Difícil)
Adapta el "Ejemplo Completo" de la sección anterior a tu proyecto:
- 3 stages: prepare, analyze, publish
- Pasar el diff via artifacts
- Variables protected/masked para keys
- Cache de pip
- Verificar que corre end-to-end en un MR
Resumen
- GitLab modelo: Pipeline → Stages → Jobs → Script (vs GitHub: Workflow → Jobs → Steps)
- Stages se ejecutan en orden; jobs dentro de un stage corren en paralelo
- Variables equivalen a GitHub secrets — siempre Protected + Masked para keys
- Rules reemplazan
only/except(deprecated) — más expresivas - Artifacts pasan archivos entre jobs/stages automáticamente con
needs: - Docker executor es la base — todo job corre en container
- Image custom con deps preinstaladas acelera pipelines
Próxima cápsula: 04 — Pipeline GitLab con SDK headless. Combinas todo: el SDK que aprendiste en cápsula 02 con el modelo de pipelines que aprendiste aquí. Resultado: un pipeline funcional de GitLab CI/CD que corre Claude Code en cada MR.
Recursos Adicionales
- GitLab CI/CD Documentation — Documentación oficial completa
- GitLab CI/CD: Predefined Variables — Lista completa de variables
CI_* - GitLab CI/CD: rules — Sintaxis de rules
- GitLab Docker Executor — Configuración del executor
- GitLab Container Registry — Para hospedar images custom
- GitLab vs GitHub Actions migration guide — Comparación oficial