Skip to main content

Includes

Dependiendo de lo que estés haciendo, la cantidad de pasos, variaciones de reglas, etc., llegará un momento en que el archivo .gitlab-ci.yml se volverá bastante grande.

Podemos separar los códigos en diferentes archivos yaml y mantener .gitlab-ci.yml limpio, solo haciendo inclusiones y declarando las cosas principales.

Recomiendo mantener el bloque default siempre en la raíz para evitar sobrescrituras. Si el yaml se lee de arriba hacia abajo, entonces haz los includes después para evitar problemas.

Documentación Include

default:
tags:
- general
image: node:22-alpine

stages:
- check # Nuevo stage
- build
- deploy

Voy a crear un árbol de carpetas para dividir los stages.

❯ tree cicd
cicd
├── globals.yaml
├── build
│ └── build.yaml
├── check
│ └── check.yaml
└── deploy
└── deploy.yaml

Así quedan los archivos.

cat cicd/check/check.yaml
.check:
stage: check
before_script:
- env
- npm ci
artifacts:
when: always
expire_in: "3 months"

# ESTO VA DENTRO DE globals.yaml pues lo usamos para varios stages
# .rules-only-main-mr:
# rules:
# - if: '$CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "main"'
# when: always

unit-test:
extends:
- .check
- .rules-only-main-mr
script:
- npm test
artifacts:
reports:
junit: reports/junit.xml

lint-test:
extends: [.check,.rules-only-main-mr]
script:
- npm run lint
artifacts:
reports:
codequality: gl-codequality.json

vulnerability-test:
extends: [.check,.rules-only-main-mr]
script:
- npm audit --audit-level=high --json > vulnerability-report.json
artifacts:
paths:
- vulnerability-report.json

cat cicd/build/build.yaml
# ESTO VA DENTRO DE globals.yaml pues lo usamos para varios stages
# .rules-only-main-mr:
# rules:
# - if: '$CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "main"'
# when: always

build:
stage: build
extends: [.rules-only-main-mr]
script:
- node --version
- npm --version
- npm ci
- npm run build
artifacts:
when: on_success
expire_in: "1 hour"
paths:
- build/

cat cicd/deploy/deploy.yaml
deploy:
stage: deploy
image: alpine
script:
- echo "Deploy iniciando con TOKEN=$TOKEN"
environment:
name: $DEPLOY_ENV
rules:
- if: '$CI_COMMIT_BRANCH == "develop"'
variables:
DEPLOY_ENV: develop
- if: '$CI_COMMIT_BRANCH == "main"'
variables:
DEPLOY_ENV: production

Nuestro stage quedaría así entonces, de forma que pudiera llamar a todos estos archivos.

default:
tags:
- general
image: node:22-alpine

stages:
- check # Nuevo stage
- build
- deploy

include:
- local: '/cicd/globals.yaml' # Un archivo específico
- local: '/cicd/check/check.yaml' # Un archivo específico
- local: '/cicd/build/*.yaml' # Cualquier archivo .yaml en la carpeta build
- local: '/cicd/deploy/*.yaml'

Preferí separar en carpetas por stage porque en el futuro puedo resolver separar cada uno de los jobs en diferentes archivos yaml. Hasta ahora nuestros jobs son simples, pero pueden existir jobs mucho más grandes en el futuro.

Todavía podemos hacer uso de wildcards para facilitar.

default:
tags:
- general
image: node:22-alpine

stages:
- check
- build
- deploy

# include:
# - local: '/cicd/**/*.yaml' # Esto NO FUNCIONA

include: # Esto funciona
- 'cicd/globals.yaml'
- 'cicd/**/*.yaml'

Cuidado con esta trampa que la propia documentación oficial nos confunde.

Includes De Otros Repositorios

Podemos hacer includes directamente de otro repositorio si leíste bien la documentación. Te debe haber pasado por la cabeza tener un repositorio central que podamos reutilizar código en lugar de desarrollar para cada repositorio todos los jobs.

Ventajas:

  • Estandarización: Equipos diferentes siguen los mismos pipelines (build, test, deploy).
  • DRY (Don't Repeat Yourself): Centraliza plantillas y reduce repetición.
  • Mantenimiento centralizado: Actualizar un pipeline (ej: nueva regla de seguridad) afecta a todos los proyectos automáticamente.
  • Seguridad/control: Puedes controlar exactamente lo que se ejecuta en los proyectos, importante en ambientes más regulados.

Desventajas:

  • Acoplamiento: Todos los proyectos dependen de un único repositorio. Un cambio erróneo rompe todo.
  • Complejidad en el debug
  • Menos autonomía: Los equipos pueden quejarse de no poder adaptar fácilmente el pipeline a sus necesidades.

Casos de uso comunes:

  • Empresas con muchos proyectos y equipos que necesitan seguir estándares (ej: microservicios).
  • Organizaciones con foco en compliance/seguridad.
  • Plataformas internas tipo PaaS interno, donde la esteira es parte de la plataforma.

Puedes adoptar un enfoque híbrido: cada proyecto mantiene un .gitlab-ci.yml simple, que incluye plantillas del repositorio central, pero permite sobrescribir o extender jobs cuando sea necesario. Esto ofrece flexibilidad sin renunciar a la estandarización.

Pensando como piezas de LEGO, es posible montar pipelines a medida a partir de bloques reutilizables. Además, se pueden crear plantillas de repositorios — por ejemplo, un boilerplate para proyectos Node ya configurado con el pipeline estándar de Node, y lo mismo para proyectos en Python.

¿Cómo sería una estructura de repositorio así? Es solo una sugerencia, no una regla.

ci-templates/
├── templates/
│ ├── node/
│ │ ├── install.yml
│ │ ├── test.yml
│ │ └── build.yml
│ ├── python/
│ │ ├── lint.yml
│ │ ├── test.yml
│ │ └── package.yml
│ └── common/
│ ├── security-scan.yml
│ ├── docker-build.yml
│ └── notify-slack.yml
├── full-pipelines/
│ ├── node.yml # Incluye las plantillas de node + common
│ ├── python.yml
│ └── microservice.yml

Al final del proyecto podemos pensar en esto, pues en este momento mantener el pipeline junto con el repositorio facilita nuestra evolución. Esta idea es muy interesante principalmente para empresas que tienen un equipo solo para pipelines.

Imaginemos que tenemos un repositorio centralizado con jobs en platform/ci-templates y otro repositorio que aprovechará esto.

include:
- project: 'platform/ci-templates'
file: '/templates/node/install.yml'
ref: main

- project: 'platform/ci-templates'
file: '/templates/node/test.yml'
ref: main

- project: 'platform/ci-templates'
file: '/templates/common/docker-build.yml'
ref: main

# Ejemplo de sobrescritura opcional (si quieres personalizar algo)
custom_test:
extends: .node_test_template
variables:
RUN_TESTS: "true"
script:
- echo "Ejecutando tests con cobertura"
- npm run test:coverage

Pero para que esto funcione, es necesario un estándar de desarrollo.