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 ActionsGitLab CI/CDSignificado
${{ github.repository }}$CI_PROJECT_PATH"owner/repo"
${{ github.event.pull_request.number }}$CI_MERGE_REQUEST_IIDNúmero del MR/PR
${{ github.event.pull_request.head.sha }}$CI_COMMIT_SHASHA del commit
${{ github.event.pull_request.base.ref }}$CI_MERGE_REQUEST_TARGET_BRANCH_NAMERama destino
${{ github.actor }}$GITLAB_USER_LOGINUsername
${{ 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:

FlagSignificado
ProtectedSolo accesible en branches protected
MaskedEnmascarada en logs (como secret en GitHub)
ExpandedSi 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

CasoConfiguración
Coverage reportspaths: [coverage.xml], expire_in: 7 days
Build artifactspaths: [dist/], expire_in: 30 days
Test resultsreports: junit: results.xml (GitLab los renderiza)
Análisis de Claudepaths: [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:

  1. Extrae el diff del MR
  2. Corre el SDK de Claude para analizar
  3. 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 de prepare)
  • Stage publish → publica resultados (depende de analyze)

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:

  1. 3 stages: prepare, analyze, publish
  2. Pasar el diff via artifacts
  3. Variables protected/masked para keys
  4. Cache de pip
  5. 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

  1. GitLab CI/CD Documentation — Documentación oficial completa
  2. GitLab CI/CD: Predefined Variables — Lista completa de variables CI_*
  3. GitLab CI/CD: rules — Sintaxis de rules
  4. GitLab Docker Executor — Configuración del executor
  5. GitLab Container Registry — Para hospedar images custom
  6. GitLab vs GitHub Actions migration guide — Comparación oficial