TL;DR
En la introducción y en los pipelines agénticos programamos steps, caches, condiciones de arranque y agentes, todo en un mismo bitbucket-pipelines.yml. Esto funciona para un ejemplo, pero en un proyecto real ese fichero se convierte en un cajón desastre de cientos de líneas donde conviven CI, CD y agentes, nadie es dueño de nada y cada modificación genera fricción. En esta entrada no aprenderemos ninguna figura nueva sino que usaremos lo que ya sabemos. Lo repartiremos por responsabilidad con definitions > imports, reutilizaremos steps y reduciremos el bitbucket-pipelines.yml a un enrutador de todo el CI/CD.
Todo lo que voy a contar no es más de lo que ya hay escrito en el blog (step, caches, parallel o agents). Lo único que cambia es dónde vive cada entidad. Ten en cuenta que esto es una programación totalmente opinionated y que a mí me ha funcionado.
El monolito no escala
Un bitbucket-pipelines.yml que contenga todo se hace francamente inmantenible cuando el proyecto crece. Cualquier nueva incorporación o evolución de un pipeline requiere estar leyendo un fichero enorme para encontrar un step o luchar por indentar bien el YAML. Además, mezclar responsabilidades hace que no podamos tener una visión general del alcance de nuestras automatizaciones del SDLC.
Un fichero por responsabilidad
La figura que lo resuelve es definitions > imports. Declaramos un alias por cada fichero externo donde vamos a repartir nuestros pipelines:
image: node:20-alpine
definitions:
imports:
ci: .bitbucket/ci-pipelines.yml
cd: .bitbucket/cd-pipelines.yml
agentic: .bitbucket/agentic-pipelines.ymlCon esto, ci, cd y agentic pasan a ser referencias a tres ficheros que viven en .bitbucket/ (no es obligatorio usar este directorio), y cada uno agrupa una responsabilidad. Los checks de código van en ci-pipelines.yml, los despliegues en cd-pipelines.yml y los agentes de IA en agentic-pipelines.yml. Dentro de cada fichero declaramos pipelines con su identificador bajo definitions > pipelines:
image: node:20-alpine
definitions:
pipelines:
pr-checks:
- step:
name: Lint
script:
- echo "npm run lint"
- step:
name: Test
script:
- echo "npm run test"Fíjate en que ci-pipelines.yml no tiene sección pipelines: de primer nivel, o sea, no declara condiciones de arranque. No decide cuándo se ejecuta nada, solo define bloques con nombre (pr-checks) listos para que alguien los invoque. El cuándo se queda en el fichero principal.
El pipeline principal como router
Con las responsabilidades repartidas, el bitbucket-pipelines.yml se reduce hasta quedarse en lo único que de verdad le corresponde, decidir qué pipeline arranca con qué evento. Cada entrada es un import: nombre@alias que trae el pipeline con nombre del fichero correspondiente:
image: node:20-alpine
definitions:
imports:
ci: .bitbucket/ci-pipelines.yml
cd: .bitbucket/cd-pipelines.yml
agentic: .bitbucket/agentic-pipelines.yml
pipelines:
pull-requests:
'**':
import: pr-checks@ci
custom:
'Deploy a staging':
import: deploy-staging@cd
'Auditoría semanal':
import: security-audit@agenticLa sintaxis pr-checks@ci se lee como "el pipeline pr-checks del alias ci". El fichero principal se convierte así en un mapa que se lee casi en un lenguaje natural. A la izquierda el disparador (una PR, un custom...) y a la derecha de qué fichero sale la lógica.
Este fichero se vuelve muy estable. Añadir un step a CI ya no implica su modificación, se edita ci-pipelines.yml. El bitbucket-pipelines.yml solo cambia cuando aparece una condición de arranque nueva.
Reutilizar sin repetir
Dividir por ficheros mejora el ownership, pero dentro de cada fichero seguimos pudiendo repetirnos. El caso más habitual es la instalación de dependencias, con la que arranca casi todo pipeline. Copiar y pegar ese step en cada uno es el mismo monolito a menor escala. Aquí entran los anchors de YAML, que no son ningún término reservado de Bitbucket sino del propio formato. Definimos un valor una vez con &nombre y lo reutilizamos con *nombre.
definitions:
pipelines:
_install:
- step: &install
name: Instala dependencias
caches:
- node
script:
- echo "npm ci"
pr-checks:
- step: *install
- parallel:
fail-fast: true
steps:
- step:
name: Lint
script:
- echo "npm run lint"
- step:
name: Test
script:
- echo "npm run test"Definimos el step de instalación una sola vez, lo anclamos con &install y lo inyectamos donde haga falta con *install. Si mañana cambia el comando de instalación, vamos a ir a un solo sitio a modificarlo. Le pongo un guion bajo al bloque (_install) por una convención mía con la que marco los pipelines que solo existen para ejecutarse desde otros y que nunca dependen de una condición de arranque, a diferencia de pr-checks, que sí se dispara con las pull requests.
El fail-fast: true del parallel termina la ejecución del resto de steps en cuanto uno falla. En una batería de checks (lint, tipos, tests...) evita gastar minutos en algo que ya sabemos que va a echar para atrás la PR.
Ownership
Con todo modularizado, la responsabilidad reside en su sitio correspondiente. La curva de entrada a la programación del pipeline del proyecto se reduce, los conflictos de merge disminuyen y cada equipo razona sobre su fichero sin leerse las mil líneas de los demás.
Si te fijas, ninguna de las entidades descritas en esta entrada es nueva. Son los mismos steps, caches y agentes de las dos entradas anteriores. La diferencia entre un pipeline básico y uno de producción no suele estar en qué figuras usas, sino en cómo las organizas.