Jenkins compite por control y GitLab por amplitud. CircleCI compite por una sola cosa: que el pipeline sea rápido. Es un SaaS especializado —no aloja tu código, no gestiona tus issues, no es tu registro de contenedores— que asume que el repositorio vive en otro sitio y se concentra en ejecutar trabajos deprisa y con buenas herramientas para saber por qué no lo hacen. Esa especialización se nota en tres piezas que ninguna otra herramienta del módulo tiene tan afinadas: una caché explícita donde tú decides la clave, cuándo se guarda y cómo se degrada; los orbs, que son paquetes de configuración versionados y publicables; y el reparto de pruebas por tiempos históricos, que es la respuesta más madura al problema de sharding que arrastramos desde la 02-04 y que la 04-04 solo pudo resolver a mano. En esta lección traducimos el pipeline de Reservalia por tercera vez, distinguimos con precisión workspace, caché y artefactos —tres conceptos que aquí son tres mecanismos distintos y explícitos—, y evaluamos el precio de esa velocidad: dependencia de un SaaS, un modelo de crédito que hay que entender antes de firmar, y un ecosistema más pequeño.

Contenido

  1. La propuesta: un SaaS especializado en velocidad
  2. Modelo de ejecución: jobs, workflows y executors
  3. El pipeline de Reservalia en .circleci/config.yml
  4. Caché explícita: claves, restore-keys y degradación
  5. Workspace, caché y artefactos: tres cosas distintas
  6. Paralelización y test splitting por tiempos históricos
  7. Reutilización: commands, parameters y orbs
  8. Contexts, secretos y aprobaciones
  9. Recursos, clases de máquina y el modelo de créditos
  10. Depurar: SSH into build y ejecución local
  11. Límites y contrapartidas
  12. Cuándo elegir CircleCI
  13. Errores Comunes y Consejos
  14. Ejercicios
  15. Conclusión

  1. La propuesta: un SaaS especializado en velocidad

CircleCI nació en 2011, en la misma ola que Travis, con una diferencia de enfoque que resultó decisiva: mientras Travis apostaba por la simplicidad extrema para proyectos de código abierto, CircleCI apostó por equipos que pagan y por el rendimiento del pipeline como producto. De ahí su forma actual.

Lo que la especialización implica, para bien y para mal:

Consecuencia
No aloja el código Se conecta a GitHub, GitLab o Bitbucket. Tu identidad y permisos siguen viviendo allí, y CircleCI pide acceso al repositorio
No es un registro ni un gestor de issues Publica en ECR, Docker Hub o donde quieras: nada regalado, pero nada que te ate
Toda la inversión está en la ejecución Clases de recurso finas, arranque rápido, caché controlable, reparto por tiempos, buenas vistas de dónde se va el tiempo
Si desaparece la conexión con el SaaS, no hay CI El plano de control es suyo, incluso con runners propios

Y una virtud cultural que conviene señalar: CircleCI ha sido durante años de las herramientas que mejor exponen dónde se va el tiempo —tiempo por job, por step, colas, tasas de acierto de caché, distribución de duraciones de test—. Eso importa porque la 04-04 estableció que optimizar sin medir es adivinar; aquí la medición viene de serie.

  1. Modelo de ejecución: jobs, workflows y executors

El fichero es único: .circleci/config.yml, versionado con el código. Su estructura tiene tres niveles.

flowchart TD
    W["workflow<br/>orquesta y ordena"] --> J1["job: preparar"]
    J1 --> J2["job: calidad"]
    J1 --> J3["job: test<br/>parallelism: 4"]
    J1 --> J4["job: build"]
    J2 --> J5["job: publicar"]
    J3 --> J5
    J4 --> J5
    J5 --> A{"hold<br/>type: approval"}
    A --> J6["job: desplegar-produccion"]
    subgraph EX["executor: donde corre cada job"]
      E1["docker"]
      E2["machine"]
      E3["macos"]
      E4["arm"]
    end
CircleCI Equivalente en el curso Matiz
Workflow El grafo de jobs Puede haber varios en un mismo fichero, con disparadores distintos
Job Job Unidad que corre en un executor; devuelve éxito o fallo
Step Step run, checkout, save_cache, store_artifacts
Executor Tipo de runner docker, machine, macos, windows, y variantes ARM/GPU
Command Composite action Secuencia de steps reutilizable, con parámetros
Orb Paquete de plantillas Publicado y versionado en un registro público o privado
Context Grupo de secretos compartidos Ámbito de organización, no de proyecto
Workspace Paso de ficheros entre jobs Distinto de caché y de artefactos

Dos rasgos característicos del modelo. Primero, el grafo es explícito desde el principio: no hay concepto de "etapa" que actúe como barrera, solo requires. CircleCI nunca tuvo que evolucionar de fases a grafo porque nació con grafo. Segundo, los executors son de primera clase: se declaran arriba, con nombre, y los jobs los referencian; cambiar toda la flota de docker a machine es editar un bloque.

  1. El pipeline de Reservalia en .circleci/config.yml

Tercera traducción del mismo pipeline.

version: 2.1

orbs:                                                   # 1
  aws-cli: circleci/[email protected]
  node: circleci/[email protected]

executors:                                              # 2
  node-base:
    docker:
      - image: cimg/node:22.11                          # imágenes "convenience" preparadas por CircleCI
        auth: { username: $DOCKERHUB_USER, password: $DOCKERHUB_TOKEN }
      - image: cimg/postgres:16.2                       # 3 · contenedores secundarios del mismo job
        environment:
          POSTGRES_USER: postgres
          POSTGRES_PASSWORD: test
          POSTGRES_DB: reservalia_test
    resource_class: medium                              # 4
    working_directory: ~/reservalia
  docker-builder:
    machine:
      image: ubuntu-2404:current                        # VM completa: hay daemon Docker de verdad
      docker_layer_caching: true                        # 5

commands:                                               # 6
  preparar-node:
    description: Checkout, restauración de caché e instalación reproducible
    parameters:
      profundidad:
        type: integer
        default: 1
    steps:
      - checkout
      - restore_cache:                                  # 7
          keys:
            - npm-v2-{{ checksum "package-lock.json" }}
            - npm-v2-                                   # fallback parcial
      - run:
          name: Instalar dependencias
          command: npm ci --prefer-offline --no-audit

jobs:
  preparar:
    executor: node-base
    steps:
      - preparar-node
      - save_cache:                                     # 8 · solo este job escribe la caché
          key: npm-v2-{{ checksum "package-lock.json" }}
          paths: [ ~/.npm ]
      - persist_to_workspace:                           # 9
          root: ~/reservalia
          paths: [ ".", "!node_modules" ]

  calidad:
    executor: node-base
    steps:
      - attach_workspace: { at: ~/reservalia }
      - run: npx prettier --check .
      - run: npm run lint
      - run: npm run typecheck

  test:
    executor: node-base
    parallelism: 4                                      # 10
    steps:
      - attach_workspace: { at: ~/reservalia }
      - run:
          name: Esperar a PostgreSQL
          command: dockerize -wait tcp://localhost:5432 -timeout 1m
      - run: npm run migrate
      - run:
          name: Ejecutar la partición de tests correspondiente
          command: |
            FICHEROS=$(circleci tests glob "apps/**/*.test.ts" \
              | circleci tests split --split-by=timings)   # 11 · reparto por tiempos reales
            npm test -- --runTestsByPath $FICHEROS \
              --reporters=default --reporters=jest-junit
      - store_test_results: { path: informes }           # 12 · alimenta el histórico de tiempos
      - store_artifacts: { path: cobertura }             # 13

  build:
    executor: docker-builder
    steps:
      - attach_workspace: { at: ~/reservalia }
      - run:
          name: Construir imagen
          command: |
            docker buildx build \
              --file apps/api/Dockerfile \
              --cache-from type=registry,ref=$ECR/$IMAGEN:cache \
              --cache-to   type=registry,ref=$ECR/$IMAGEN:cache,mode=max \
              --tag $IMAGEN:$CIRCLE_SHA1 --load .
      - run:
          name: Escanear imagen
          command: trivy image --severity HIGH,CRITICAL --exit-code 1 $IMAGEN:$CIRCLE_SHA1

  publicar:
    executor: docker-builder
    steps:
      - attach_workspace: { at: ~/reservalia }
      - aws-cli/setup:                                   # 14 · OIDC, sin claves de larga vida
          role_arn: $AWS_ROLE_ARN
          region: eu-west-1
      - run:
          name: Publicar en ECR por digest
          command: |
            aws ecr get-login-password --region eu-west-1 \
              | docker login --username AWS --password-stdin "$ECR"
            docker tag  $IMAGEN:$CIRCLE_SHA1 $ECR/$IMAGEN:$CIRCLE_SHA1
            docker push $ECR/$IMAGEN:$CIRCLE_SHA1
            DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' $ECR/$IMAGEN:$CIRCLE_SHA1)
            echo "$DIGEST" | tee digest.txt
      - store_artifacts: { path: digest.txt }

  desplegar:
    executor: node-base
    parameters:
      entorno: { type: enum, enum: [staging, produccion] }   # 15
    steps:
      - attach_workspace: { at: ~/reservalia }
      - aws-cli/setup: { role_arn: $AWS_ROLE_ARN, region: eu-west-1 }
      - run: ./scripts/desplegar.sh << parameters.entorno >>

workflows:
  ci:
    jobs:
      - preparar
      - calidad: { requires: [preparar] }                 # 16
      - test:     { requires: [preparar] }
      - build:    { requires: [preparar] }
      - publicar:
          requires: [calidad, test, build]
          context: [aws-ci]                               # 17
          filters: { branches: { only: main } }
      - desplegar:
          name: desplegar-staging
          entorno: staging
          requires: [publicar]
          context: [aws-staging]
          filters: { branches: { only: main } }
      - aprobar-produccion:                               # 18
          type: approval
          requires: [desplegar-staging]
          filters: { branches: { only: main } }
      - desplegar:
          name: desplegar-produccion
          entorno: produccion
          requires: [aprobar-produccion]
          context: [aws-produccion]
          filters: { branches: { only: main } }
  1. Los orbs se importan arriba y se anclan a versión. circleci/[email protected] es un paquete publicado; su contenido son commands, jobs y executors ya escritos. Anclar a versión exacta es aquí la misma regla que fijar acciones por SHA de la 04-03: un orb es código de terceros que corre con tus secretos.
  2. Los executors con nombre evitan repetir la configuración de entorno en cada job. Es el rasgo que más limpio deja el fichero.
  3. Los contenedores secundarios son el equivalente de services en GitLab o GitHub: comparten red con el primario, así que PostgreSQL está en localhost:5432. Detalle importante: CircleCI no espera a que estén listos, de ahí el paso con dockerize -wait. Es una causa habitual de flaky al empezar.
  4. resource_class elige el tamaño de la máquina (small, medium, large, xlarge…). Es la palanca directa de la 04-04: subir de clase acorta el job y multiplica el consumo de créditos por minuto. Se decide midiendo, no por intuición.
  5. docker_layer_caching conserva las capas Docker entre ejecuciones en el executor machine. Acelera mucho los builds de imagen y consume créditos adicionales; con una caché de capas en el registro (--cache-from, 06-05) a veces no compensa. Hay que medir las dos opciones.
  6. commands es la composite action de la 04-05: pasos reutilizables con parámetros, dentro del mismo fichero o publicados en un orb.
  7. restore_cache con lista de claves: prueba la primera; si no existe, la segunda como prefijo. Es el restore-keys de la 04-02, y aquí es completamente explícito.
  8. save_cache solo en un job. Las claves de caché en CircleCI son inmutables: una vez escrita una clave, no se sobrescribe. Eso obliga a versionar la clave (npm-v2-) cuando quieres invalidar, y evita el problema de varios jobs sobreescribiéndose.
  9. persist_to_workspace guarda ficheros para los jobs siguientes de este mismo workflow. Es lo que en Jenkins era stash y en GitHub Actions se hace con artefactos.
  10. parallelism: 4 lanza cuatro contenedores idénticos del job, con CIRCLE_NODE_INDEX y CIRCLE_NODE_TOTAL.
  11. La pieza estrella: circleci tests split --split-by=timings reparte los ficheros entre los cuatro contenedores usando los tiempos reales de ejecuciones anteriores, no el número de ficheros. Apartado 6.
  12. store_test_results no es cosmético: es lo que alimenta el histórico de tiempos que usa el reparto anterior, además de la vista de tests fallidos y flaky. Sin él, --split-by=timings degrada a reparto por nombre.
  13. store_artifacts sube ficheros descargables desde la interfaz, que es un concepto distinto del workspace (apartado 5).
  14. Identidad federada por OIDC: CircleCI emite un token por job y AWS lo cambia por credenciales temporales, exactamente el diseño de la 03-02. La condición de confianza en AWS se ata al proyecto y opcionalmente al context.
  15. Jobs parametrizados invocados varias veces con name distinto: un solo job desplegar sirve para staging y producción, con contexts distintos. Es reutilización sin duplicar definición.
  16. requires es todo el mecanismo de orden. No hay etapas.
  17. context aporta los secretos, y se asigna por invocación en el workflow, no dentro del job. Esa separación es una de las mejores ideas de CircleCI: el mismo job corre con credenciales de staging o de producción según quién lo invoque.
  18. type: approval es un job especial sin steps: un botón. Su semántica es la puerta de la 03-01; quién puede pulsarlo se controla con las restricciones del context y del proyecto.

  1. Caché explícita: claves, restore-keys y degradación

Frente a la caché "automática" de setup-node en GitHub Actions o al cache: declarativo de GitLab, aquí todo es manual. Se paga en verbosidad y se gana en control.

- restore_cache:
    keys:
      # 1 · coincidencia exacta: mismo lockfile, misma versión de Node, mismo SO
      - deps-v3-{{ arch }}-{{ checksum "package-lock.json" }}
      # 2 · fallback: cualquier caché de esta arquitectura, aunque el lockfile difiera
      - deps-v3-{{ arch }}-
      # 3 · último recurso
      - deps-v3-

- run: npm ci --prefer-offline

- save_cache:
    key: deps-v3-{{ arch }}-{{ checksum "package-lock.json" }}
    paths: [ ~/.npm ]
    when: on_success                    # no guardes una caché generada por un build roto

Tres reglas que evitan los fallos habituales:

  1. La clave debe incluir todo lo que invalida la caché: el lockfile, la arquitectura y, si tienes varias versiones de runtime, la versión. Una caché de Node 20 restaurada en un job de Node 22 produce errores de módulos nativos que cuestan horas de diagnóstico.
  2. El fallback parcial es lo que hace útil la caché. Sin él, cualquier cambio en el lockfile obliga a descargar todo desde cero. Con él, se restaura la caché anterior y npm ci solo baja lo que cambió. Es el mecanismo del restore-keys de la 04-02.
  3. El prefijo versionado (v3) es el botón de invalidar. Como las claves son inmutables, la única forma de forzar una caché limpia cuando se corrompe es subir el número. Tenerlo desde el principio ahorra un mal rato.

Y una advertencia de seguridad que aplica a todas las herramientas: la caché es un canal de escritura entre ramas. Un PR de un colaborador que ejecute save_cache puede envenenar una caché que luego restaura un build de main. Cachea solo artefactos derivados de dependencias verificadas por el lockfile, nunca binarios construidos que después se ejecutan con privilegios (04-03).

  1. Workspace, caché y artefactos: tres cosas distintas

CircleCI es la herramienta que más explícita hace esta distinción, y por eso es el mejor sitio para fijarla.

Caché Workspace Artefactos
Para qué Acelerar Pasar ficheros entre jobs del mismo workflow Guardar salidas para personas o sistemas externos
Alcance Entre workflows, entre ramas Un workflow Persistente, descargable desde la interfaz
Cómo save_cache / restore_cache persist_to_workspace / attach_workspace store_artifacts
Clave Explícita, inmutable Rutas; se acumula por job Ruta y nombre
Si desaparece Más lento El pipeline falla Se pierde la evidencia
Contenido típico ~/.npm, ~/.gradle dist/, fuentes compiladas, digest.txt Informes de cobertura, capturas de Playwright, SBOM

El error clásico: usar caché para pasar el dist/ entre build y deploy. Funciona casi siempre y falla el día que la caché no está o cuando trae la de otra rama, y entonces se despliega un build antiguo sin que nada avise. Un despliegue con un artefacto equivocado es peor que un fallo, porque no se detecta. Regla, la misma de la 06-02: si perderlo rompe la corrección del resultado, no es caché.

Detalle práctico del workspace: se acumula. Si build persiste dist/ y publicar persiste digest.txt, un job posterior que haga attach_workspace ve los dos. Y como cada persist_to_workspace añade, conviene persistir lo mínimo: persistir node_modules entero es lento y casi nunca necesario si la caché está bien puesta.

  1. Paralelización y test splitting por tiempos históricos

Este es el argumento técnico más fuerte de CircleCI y la solución más madura al problema que la 02-04 planteó y la 04-04 dejó a medias.

El sharding ingenuo reparte por número de ficheros. Con los tests de Reservalia:

Contenedor Ficheros Tiempo real
1 34 2 min 10 s
2 34 1 min 40 s
3 34 7 min 50 s ← lleva los tests de integración con base de datos
4 34 2 min 05 s

El job tarda 7:50, no 3:30. Se paga por cuatro contenedores y se aprovecha uno. Con --split-by=timings, CircleCI usa los tiempos por fichero de ejecuciones anteriores —que conoce porque store_test_results los sube— y reparte para igualar duración:

Contenedor Ficheros Tiempo real
1 51 3 min 25 s
2 44 3 min 30 s
3 12 3 min 20 s
4 29 3 min 35 s
- run:
    command: |
      # glob → lista de ficheros; split → solo los que tocan a este contenedor
      FICHEROS=$(circleci tests glob "apps/**/*.test.ts" | circleci tests split --split-by=timings)
      npm test -- --runTestsByPath $FICHEROS --reporters=jest-junit
- store_test_results: { path: informes }     # imprescindible: sin esto no hay histórico

Matices que hay que conocer:

  • La primera ejecución no tiene datos y reparte por nombre de fichero; a partir de la segunda, mejora. Si añades muchos tests nuevos de golpe, el reparto tarda una o dos ejecuciones en reequilibrarse.
  • Existen otras estrategias: --split-by=filesize (útil sin histórico) y --timings-type=classname para lenguajes donde la unidad no es el fichero.
  • No arregla un test lento: reparte mejor, pero un único test de cinco minutos sigue siendo el suelo del job. La 04-04 ya avisaba de que paralelizar no sustituye a arreglar.
  • Interacción con los flaky: un test inestable falla en un contenedor y tumba el job entero; el reparto no cambia eso. La cuarentena de la 02-04 sigue siendo necesaria.

Comparado con las demás herramientas: en GitHub Actions y GitLab, el reparto equilibrado por tiempos hay que construirlo (guardando tiempos como artefacto y escribiendo el reparto, o con herramientas de terceros). Aquí es un comando. Es una diferencia de días de trabajo, y es real.

  1. Reutilización: commands, parameters y orbs

Tres niveles, del más local al más compartido:

# 1. Command: pasos reutilizables dentro del proyecto (≈ composite action)
commands:
  notificar:
    parameters:
      canal:   { type: string, default: "#reservalia-ci" }
      estado:  { type: enum, enum: [ok, fallo] }
    steps:
      - run:
          when: always
          command: ./scripts/notificar.sh "<< parameters.canal >>" "<< parameters.estado >>"

# 2. Job parametrizado: el mismo job invocado con datos distintos
jobs:
  desplegar:
    parameters:
      entorno: { type: enum, enum: [staging, produccion] }
      espera:  { type: integer, default: 60 }
    steps:
      - run: ./scripts/desplegar.sh << parameters.entorno >> << parameters.espera >>

# 3. Orb: paquete versionado, publicado y reutilizable entre proyectos
orbs:
  reservalia: reservalia/[email protected]
workflows:
  ci:
    jobs:
      - reservalia/build-publicar:
          dockerfile: apps/api/Dockerfile
          repositorio: reservalia/api

Un orb es un paquete con tres tipos de contenido —commands, jobs y executors— publicado con versionado semántico. Publicar el tuyo:

# src/@orb.yml del orb reservalia/plataforma
version: 2.1
description: Piezas comunes de los pipelines de Reservalia

commands:
  preparar-node:
    parameters:
      version: { type: string, default: "22.11" }
    steps:
      - checkout
      - restore_cache: { keys: ["npm-v2-{{ checksum \"package-lock.json\" }}", "npm-v2-"] }
      - run: npm ci --prefer-offline
# Publicación: primero una versión de desarrollo, luego la promoción semántica
circleci orb pack src/ > orb.yml
circleci orb validate orb.yml
circleci orb publish orb.yml reservalia/plataforma@dev:pruebas     # mutable, para probar
circleci orb publish promote reservalia/plataforma@dev:pruebas patch  # inmutable: 2.3.1

Esto es exactamente la idea de la 04-05 —repositorio central de plantillas— pero con dos ventajas de diseño: el versionado semántico es obligatorio y las versiones publicadas son inmutables, así que el problema de "alguien cambió main de la librería y rompió a todo el mundo" que vimos en Jenkins y en los include de GitLab no puede ocurrir por accidente. La contrapartida sigue siendo la misma: un orb público de terceros es código que corre con tus secretos, así que las reglas de la 04-03 se aplican igual —revisar qué hace, anclar a versión exacta, preferir orbs certificados o propios para lo que toca credenciales—.

Los orbs pueden ser públicos (catálogo abierto, código visible) o privados dentro de tu organización, que es lo que usarías para reservalia/plataforma.

  1. Contexts, secretos y aprobaciones

Hay dos sitios donde viven los secretos, y la diferencia importa:

Variables de proyecto Context
Ámbito Un proyecto Toda la organización
Se asigna Siempre, a todos los jobs del proyecto Por invocación de job en el workflow
Restricción de acceso Por proyecto Por grupo de seguridad: solo ciertos equipos
Uso típico Configuración del propio proyecto Credenciales compartidas y, sobre todo, las de producción
workflows:
  cd:
    jobs:
      - desplegar:
          name: desplegar-produccion
          context: [aws-produccion, notificaciones]   # solo este job las ve
          requires: [aprobar-produccion]

La práctica correcta: las credenciales de producción viven en un context restringido a un grupo de seguridad, y solo se adjuntan al job de despliegue a producción. Ningún job de test las tiene, así que un PR no puede exfiltrarlas aunque modifique el config.yml —siempre que el context esté restringido, que es lo que hay que verificar—. Es el mismo razonamiento que las variables protegidas de la 06-02 y el mínimo privilegio de la 04-03, con un mecanismo distinto.

Sobre las aprobaciones: type: approval es un job vacío que bloquea el grafo hasta que alguien pulsa. Es más simple que los Environments de GitHub o los entornos protegidos de GitLab: no hay historial de despliegues por entorno ni rollback desde la interfaz, porque CircleCI no modela el entorno como objeto. Si quieres saber qué versión está en producción, lo construyes tú. Es la contrapartida directa de no ser una plataforma integrada.

  1. Recursos, clases de máquina y el modelo de créditos

jobs:
  test:
    docker: [ { image: cimg/node:22.11 } ]
    resource_class: large       # más CPU y memoria por minuto, y más créditos por minuto

CircleCI factura por créditos, y esa es la parte del modelo que hay que entender antes de firmar. La idea: cada minuto de ejecución consume créditos, y cuántos depende de la clase de recurso. Una clase grande no cuesta lo mismo por minuto que una pequeña; los executors macos y las máquinas con GPU están en otro orden de magnitud (es exactamente el problema de coste que la 05-02 encontró con los runners macOS). Otras funcionalidades, como docker_layer_caching, también consumen. Además existe un componente por usuario activo.

No doy cifras: cambian, dependen del plan y de la región. Lo que no cambia es la forma de razonar sobre ellas:

  • El coste es minutos × clase × concurrencia. Subir de medium a large solo compensa si el tiempo baja más que el multiplicador. Se mide con una ejecución de cada, no se estima.
  • La paralelización multiplica el coste por el número de contenedores aunque el tiempo de pared baje. Cuatro contenedores durante 3 minutos consumen más que uno durante 8. Se paga velocidad con dinero, que es la decisión explícita que la 04-04 pedía tomar a conciencia.
  • Un pipeline que se ejecuta dos veces por push —rama y PR— duplica la factura sin dar información nueva. Los filters están para eso.
  • La caché mal configurada se paga en cada ejecución de cada rama.

Y una advertencia que vale para todos los SaaS por minuto: el coste crece con el equipo y con la actividad, no con el valor. Un equipo que triplica sus commits triplica la factura. Conviene tener alarmas de consumo y revisar mensualmente los jobs más caros, con la misma disciplina con que se revisa el tiempo de pipeline.

  1. Depurar: SSH into build y ejecución local

Dos herramientas que resuelven el problema de "falla en CI y no en mi máquina", que es donde más tiempo se pierde.

SSH into build: relanzar un job con SSH habilitado y conectarse a la máquina mientras corre.

# Tras pulsar "Rerun job with SSH" en la interfaz:
ssh -p 54782 [email protected]
# Ya dentro: el workspace está tal cual lo dejó el job
cd ~/reservalia && npm test -- apps/api/reservas.test.ts
env | grep -i database

Es la forma más rápida de diagnosticar un fallo dependiente del entorno. Dos avisos serios: la sesión mantiene la máquina reservada y consumiendo créditos hasta que la cierras o expira; y el entorno contiene los secretos del job, así que quién puede abrir sesión SSH es una decisión de seguridad, no de comodidad (04-03). En un job con credenciales de producción, restringe esa capacidad.

Ejecución local con la CLI, para iterar sin gastar ejecuciones remotas:

circleci config validate                          # errores de sintaxis antes de hacer push
circleci config process .circleci/config.yml      # expande orbs y parámetros: ver el YAML real
circleci local execute --job calidad              # ejecuta un job en Docker local

circleci config process merece atención: muestra el fichero después de expandir orbs, commands y parámetros, y es la mejor forma de entender qué hace realmente un orb de terceros antes de darle tus secretos. circleci local execute tiene límites —no soporta workflows completos, ni workspaces entre jobs, ni contexts—, así que sirve para jobs sueltos, no para validar el pipeline entero. Aun así es una de las mejores respuestas del módulo a la pregunta "¿cómo pruebo el pipeline?" de la 04-05.

  1. Límites y contrapartidas

Con la misma franqueza que se aplicó a Jenkins y GitLab:

  • Acoplamiento a un SaaS. El plano de control es de CircleCI: si el servicio no está disponible, no hay CI, ni siquiera con runners propios. Y hay una lección histórica concreta: en 2023 CircleCI comunicó un incidente de seguridad que obligó a rotar todos los secretos almacenados en el servicio. Es el mismo riesgo estructural que la 06-04 contará con Travis: confiar secretos a un tercero es una decisión con consecuencias, y tener un procedimiento de rotación masiva ensayado no es paranoia.
  • No es una plataforma. No hay repositorio, ni registro, ni entornos, ni issues. Se integra con lo que tengas, y todo lo que en GitLab venía regalado aquí hay que montarlo.
  • Ecosistema más pequeño. El catálogo de orbs es notablemente menor que el de acciones de GitHub. Para lo común hay orb; para lo raro, lo escribes.
  • El coste crece con el equipo, y los executors caros (macOS, GPU) desequilibran una factura rápido.
  • Runners autoalojados: existen, y resuelven el caso de "el trabajo debe correr en mi red o en mi hardware". Pero el plano de control sigue siendo SaaS, así que no resuelven la restricción de cumplimiento estricta que en la 06-01 llevaba a Jenkins. Conviene no confundir "el trabajo corre en mi máquina" con "todo el sistema está bajo mi control".
  • Mercado. El peso relativo de CircleCI ha disminuido desde que GitHub Actions se integró en el sitio donde ya estaba el código de casi todo el mundo. Eso importa para contratar, para encontrar respuestas y para la longevidad de la herramienta.

  1. Cuándo elegir CircleCI

Sí, cuando:

  • La velocidad del pipeline es un problema medido y caro, con una suite grande y lenta. El reparto por tiempos y las clases de recurso dan mejoras reales sin escribir infraestructura propia.
  • Quieres un CI potente sin casarte con la plataforma del repositorio, o tienes proyectos en más de un proveedor de Git.
  • Necesitas macOS, ARM o GPU de forma cómoda y sin operar máquinas: la oferta de executors es amplia (relevante para el caso móvil de la 05-02).
  • Valoras herramientas de diagnóstico de primera —insights, SSH into build, validación local— y tienes quien las use.
  • Ya lo tienes y funciona. Migrar cuesta, y la 06-07 pone números a ese cálculo.

No, cuando:

  • Tu código está en GitHub y no tienes un problema de velocidad: GitHub Actions viene integrado y sin una segunda integración de permisos que mantener.
  • Necesitas control total o cumplimiento estricto: el plano de control SaaS no lo permite (ahí está Jenkins, 06-01).
  • Quieres una plataforma integrada: eso es GitLab (06-02).
  • El presupuesto es una restricción dura y el equipo crece: el modelo por créditos y por usuario escala con la actividad.

Errores Comunes y Consejos

No llamar a store_test_results. Sin él, --split-by=timings no tiene histórico y el reparto degrada a alfabético, que es justo el desequilibrio que se quería evitar. Es el error que más veces convierte una buena idea en una decepción.

Claves de caché sin arquitectura ni versión de runtime. Restaurar una caché de otra arquitectura produce fallos de módulos nativos difíciles de leer. Incluye {{ arch }} y la versión.

Sin fallback en restore_cache. Con una sola clave exacta, cualquier cambio del lockfile obliga a descargar todo.

Usar caché para pasar artefactos entre jobs. Es lo que provoca desplegar un build viejo sin que nada falle. Para eso está el workspace.

No esperar a los contenedores secundarios. PostgreSQL tarda en aceptar conexiones; sin dockerize -wait tendrás flaky que dependen de la carga de la máquina.

Subir de resource_class sin medir. Duplicar la clase no siempre reduce el tiempo (un build limitado por E/S o por un test secuencial no mejora) y sí duplica el consumo.

parallelism alto por defecto. Multiplica el coste linealmente. El punto óptimo se busca midiendo tiempo total frente a créditos, y suele estar más bajo de lo que la intuición dice.

Dejar sesiones SSH abiertas. Reservan la máquina y consumen. Ciérralas.

Orbs sin anclar o de origen desconocido. Ejecutan con tus secretos. Ancla a versión exacta, revisa con circleci config process y prefiere orbs certificados o propios para lo que toca credenciales.

Consejo transversal: aunque uses commands y orbs, mantén la lógica real en scripts del repositorio (./scripts/desplegar.sh) y deja el YAML como orquestador. Es lo que hace que el ejercicio 3 de esta lección sea de horas y no de semanas, y es la lección de portabilidad que la 06-07 desarrollará.

Ejercicios

Ejercicio 1. El job test de Reservalia usa parallelism: 6 y tarda 9 minutos. Al mirar los contenedores: cinco terminan entre 1:30 y 2:10, y uno tarda 8:50. En el config.yml aparece circleci tests glob "apps/**/*.test.ts" | circleci tests split y no hay ningún store_test_results. Diagnostica, corrige y calcula qué pasa con el tiempo y con el consumo. ¿Convendría bajar parallelism?

Ejercicio 2. Reservalia tiene seis repositorios en CircleCI y el bloque de preparación y publicación repetido en todos. Diseña el orb reservalia/plataforma: qué contiene, cómo se versiona, cómo se prueba antes de publicar, cómo se despliega a los seis repositorios y qué política se aplica a los orbs de terceros que ya usan.

Ejercicio 3. Diego trae una hoja de cálculo: el consumo de CircleCI ha subido un 240 % en cuatro meses sin que el equipo haya crecido. Los datos: 68 % del consumo es del job e2e con resource_class: xlarge y parallelism: 8; se ejecutan dos pipelines por push (rama y PR); docker_layer_caching está activado en cuatro jobs; y hay 14 ramas con pipelines nocturnos programados que nadie mira. Escribe el plan de reducción, ordenado por relación entre ahorro y riesgo.

Soluciones

Solución 1.

Diagnóstico. circleci tests split sin --split-by=timings reparte por nombre de fichero, y sin store_test_results no hay histórico aunque se pidiera. El contenedor lento concentra los tests que tardan —probablemente los de integración con base de datos—. El job tarda lo que el peor contenedor: 8:50. Se pagan seis contenedores durante casi nueve minutos y se aprovecha uno.

Corrección:

  test:
    executor: node-base
    parallelism: 4
    steps:
      - attach_workspace: { at: ~/reservalia }
      - run: dockerize -wait tcp://localhost:5432 -timeout 1m
      - run:
          command: |
            FICHEROS=$(circleci tests glob "apps/**/*.test.ts" \
              | circleci tests split --split-by=timings)
            npm test -- --runTestsByPath $FICHEROS --reporters=jest-junit
      - store_test_results: { path: informes }    # imprescindible
      - store_artifacts:    { path: cobertura }

Efecto. Suma de trabajo real ≈ 5×1:50 + 8:50 ≈ 18 min. Repartido entre 4 contenedores, ~4:30 cada uno más arranque: el job pasa de 8:50 a unos 5 minutos. El consumo baja además por dos vías: menos contenedores (4 en vez de 6) y menos minutos por contenedor.

¿Bajar parallelism? Sí, y el razonamiento importa más que el número. Con 18 minutos de trabajo real:

parallelism Tiempo aproximado Contenedor·minuto
1 18:00 18
2 9:00 18
4 4:30 18 + arranques
6 3:00 18 + más arranques
12 1:30 18 + arranque dominante

El trabajo total es casi constante, así que el coste por ejecución apenas cambia con el paralelismo —hasta que el arranque del contenedor deja de ser despreciable frente al trabajo asignado—. Por eso la respuesta es "4 está bien y 6 no era el problema": el problema era el reparto, no el número. La regla práctica: sube el paralelismo mientras cada contenedor siga haciendo bastante más trabajo del que cuesta arrancarlo, y arregla el reparto antes de tocar el número.

Solución 2.

Contenido del orb:

# src/@orb.yml
version: 2.1
description: Piezas comunes de CI/CD de Reservalia

executors:
  node:
    parameters: { version: { type: string, default: "22.11" } }
    docker: [ { image: cimg/node:<< parameters.version >> } ]
    resource_class: medium

commands:
  preparar:
    parameters:
      version-cache: { type: string, default: "v2" }
    steps:
      - checkout
      - restore_cache:
          keys:
            - npm-<< parameters.version-cache >>-{{ arch }}-{{ checksum "package-lock.json" }}
            - npm-<< parameters.version-cache >>-{{ arch }}-
      - run: npm ci --prefer-offline --no-audit

jobs:
  build-publicar:
    parameters:
      dockerfile:  { type: string }
      repositorio: { type: string }
      publicar:    { type: boolean, default: false }
    machine: { image: ubuntu-2404:current }
    steps:
      - preparar
      - run: ./scripts/build-imagen.sh << parameters.dockerfile >> << parameters.repositorio >> << parameters.publicar >>

Obsérvese que el job llama a un script del repositorio, no incrusta veinte líneas de shell en el YAML: eso mantiene la lógica portable y hace que el orb sea un orquestador delgado.

Versionado y publicación: versionado semántico obligatorio; cambios incompatibles suben major. El flujo es publish a @dev:<etiqueta> (mutable, para probar) y publish promote a una versión estable (inmutable). Los repositorios anclan a versión exacta @2.3.0, no a @2, para que ninguna adopción sea automática.

Pruebas antes de publicar: el propio repositorio del orb tiene un pipeline con circleci orb validate, circleci config process sobre un config.yml de ejemplo para comprobar que expande a lo esperado, y un proyecto de integración (reservalia-orb-sandbox) que consume @dev:pruebas y ejerce todos los commands en cada commit. Es el canario de la 04-05.

Despliegue a los seis repositorios: progresivo. Se publica 2.4.0, se adopta en un repositorio de bajo riesgo durante una semana, y solo entonces se abre un PR en los otros cinco. Nunca los seis a la vez, y nunca por adopción automática: el riesgo de radio grande es el mismo que en Jenkins y GitLab.

Política sobre orbs de terceros: lista permitida revisada por el equipo de plataforma; anclados a versión exacta; ningún orb de terceros en jobs que tienen contexts de producción salvo los certificados y revisados; y circleci config process como paso obligatorio de revisión al añadir uno nuevo, para ver qué comandos ejecuta realmente. Es la política de dependencias de la 04-02 aplicada al pipeline.

Solución 3. Ordenado por ahorro dividido por riesgo, que es el orden correcto para este tipo de trabajo:

# Acción Ahorro estimado Riesgo Esfuerzo
1 filters para no ejecutar pipeline de rama y de PR a la vez ~50 % de todo Nulo 1 h
2 Apagar los 14 pipelines nocturnos que nadie mira Directo y medible Bajo: si nadie los mira, no dan señal 1 h
3 Arreglar el reparto de e2e (timings) y bajar parallelism de 8 a 4 Alto sobre el 68 % Bajo 0,5 día
4 Bajar e2e de xlarge a large y medir Medio Bajo si se mide antes/después 0,5 día
5 Quitar docker_layer_caching donde ya hay caché de registro Medio Bajo: se compara el tiempo con y sin 0,5 día
6 Ejecutar e2e completo solo en main y un subconjunto en PR Alto Medio: menos señal antes de fusionar 2 días

Notas de criterio. El punto 1 es el más rentable del plan y el más invisible: no se manifiesta como lentitud, solo como factura, y por eso lleva meses ahí. Los puntos 3 y 4 hay que hacerlos en este orden y midiendo entre uno y otro, porque si se cambian a la vez no se sabe cuál causó qué; y xlarge puede estar justificado si el e2e usa navegadores reales, así que se comprueba, no se asume. El punto 6 es el único que cambia el contrato de calidad: reduce la señal antes de fusionar y por tanto puede subir el CFR, así que se decide con Marta, se escribe qué subconjunto se ejecuta y por qué, y se vigilan las métricas DORA durante un mes. Si el CFR sube del 3,8 % actual, se revierte.

Y la observación de método que hay que darle a Diego: el 240 % no vino de un cambio, vino de cuatro decisiones pequeñas que nadie revisó. La corrección estructural no es esta lista, es poner una alarma de consumo y una revisión mensual de los jobs más caros, con la misma disciplina con la que se revisa el tiempo de pipeline en la 04-04. Sin eso, en cuatro meses hay otro 240 %.

Conclusión

CircleCI es la herramienta del módulo que mejor responde a una pregunta concreta: cómo hacer que un pipeline grande sea rápido sin construir la infraestructura tú. Sus tres piezas distintivas son de aprendizaje transferible aunque nunca la uses: la caché explícita con claves y fallback te obliga a entender qué invalida qué; la separación nítida entre workspace, caché y artefactos aclara una confusión que en otras herramientas se puede arrastrar durante años; y el reparto de pruebas por tiempos históricos es la mejor solución disponible al problema de sharding, y saber que existe cambia lo que le pides a las demás.

Lo que se paga: es un SaaS y su plano de control no es tuyo —con el precedente de la rotación masiva de secretos de 2023 como recordatorio—, no es una plataforma y todo lo que GitLab regalaba aquí lo montas, el ecosistema es más pequeño, y el modelo por créditos crece con la actividad del equipo, lo que exige una vigilancia del gasto que no hay que dejar para cuando llegue la factura.

La siguiente lección cambia de registro. Travis CI fue el que inventó buena parte de lo que hoy damos por evidente —el fichero de CI en el repositorio, la matriz de versiones, el CI gratuito para código abierto— y hoy es, sobre todo, algo que te encontrarás heredado y una lección de historia con moraleja: sobre modelos de negocio que cambian bajo tus pies, sobre confiar secretos a un tercero, y sobre por qué conviene que tu pipeline no dependa demasiado de la herramienta que lo ejecuta. Veremos su modelo de fases fijas, el pipeline de Reservalia forzado a caber en él, y cómo migrar un .travis.yml a lo que uses hoy.

Curso de CI/CD: Integración y Despliegue Continuo

Módulo 1: Introducción a CI/CD

Módulo 2: Integración Continua (CI)

Módulo 3: Despliegue Continuo (CD)

Módulo 4: Prácticas Avanzadas de CI/CD

Módulo 5: Implementación de CI/CD en Proyectos Reales

Módulo 6: Herramientas y Tecnologías

Módulo 7: Ejercicios Prácticos

Módulo 8: Recursos Adicionales

© Copyright 2026. Todos los derechos reservados