Artifacts en GitLab CI: compila una vez, reutiliza en todos lados
Tu job de deploy recompila la app que otro job acaba de compilar. Meter dist/ al cache parece la solución, hasta que despliega código viejo sin avisar. Artifacts, expiración, reportes de fallos y cómo pasar valores entre jobs con dotenv.
- gitlab
- ci-cd
- devops
Tu pipeline compila la app en build-app. Después arranca el job de deploy, recibe un checkout limpio como todos los jobs, no encuentra ningún dist/, y compila todo otra vez. El doble de tiempo de build en un solo pipeline, y la cuenta empeora con cada job que necesite el resultado.
La tentación es usar la herramienta que ya conoces: meter dist/ al cache. Y va a parecer que funciona. Pero un cache miss (y la parte 2 mostró cuántas formas hay de fallar) no rompe el job: le entrega al deploy un dist/ vacío o, peor, uno viejo, sin decir nada. Y «desplegamos el build de la semana pasada» es un día mucho peor que «el build corrió dos veces». Herramienta equivocada. Este post es sobre la correcta.
Cache y artifacts resuelven problemas distintos
Los dos mecanismos parecen intercambiables al inicio, así que vale la pena separarlos bien:
- El cache es para trabajo que no quieres rehacer entre pipelines: instalar dependencias, descargar toolchains. Es best-effort por diseño (un miss te hace más lento, nunca te rompe) y vive donde el backend de cache del runner decida.
- Los artifacts son lo que un job produce dentro de un pipeline: la app compilada, el reporte de tests. Llegan garantizados a los jobs siguientes, los almacena el propio GitLab, y puedes verlos y descargarlos desde la interfaz.
Regla práctica: lo que puedes regenerar cuando quieras va al cache; lo que otros jobs consumen va en artifacts. Y por eso meter dist/ al cache sale mal: «best-effort» es aceptable para los insumos, inaceptable para lo que estás a punto de desplegar.
Compila una vez, reutiliza en todos lados
Declarar artifacts es un bloque en el job que los produce:
build-app:
stage: build
script: pnpm build
artifacts:
paths:
- dist/
expire_in: 3 hoursLos jobs de stages posteriores descargan automáticamente los artifacts de todos los jobs anteriores, así que un job de deploy que corre después de build-app se encuentra con dist/ ya en su lugar y no compila nada por su cuenta. Ese es el patrón de compilar una sola vez, y es la limpieza más grande que puedes hacerle a la mayoría de pipelines: exactamente un job compila y todo lo demás consume.
Configura expire_in a conciencia. Los artifacts se acumulan rápido: cada pipeline de MR subiendo un dist/ que vive 30 días termina costando almacenamiento de verdad. Nosotros usamos 3 horas para los builds, porque todo lo que los necesita corre en el mismo pipeline minutos después. Los reportes sí los guardamos más tiempo.
Los artifacts que más necesitas son los de los jobs que fallan
Por defecto, los artifacts se suben solo si el job termina bien. Para un build está perfecto. Para un reporte de tests es exactamente al revés: la corrida que quieres inspeccionar es la que falló:
test:
stage: quality
script: pnpm test
artifacts:
when: always
reports:
junit: junit.xmlLa variante reports:junit cumple doble función: GitLab la parsea y anota en el merge request exactamente qué tests fallaron, así nadie tiene que bucear en logs crudos para encontrar el spec roto.
Pasar valores entre jobs, no solo archivos
A veces lo que el siguiente job necesita no es un directorio sino un par de valores calculados: una versión, un destino de deploy, una URL. Escribirlos en un archivo y parsearlo del otro lado funciona, pero GitLab tiene un mecanismo dedicado para esto, el dotenv report:
prepare-release:
stage: setup
script:
- echo "APP_VERSION=$(node -p "require('./package.json').version")" >> run.env
- echo "DEPLOY_TARGET=preview" >> run.env
artifacts:
reports:
dotenv: run.env
deploy-preview:
stage: deploy
script: ./scripts/deploy.sh "$DEPLOY_TARGET" "$APP_VERSION"Cada variable de run.env aparece como variable de entorno común y corriente en los jobs que siguen, sin source ni parseo manual. Nosotros usamos este patrón para calcular la metadata del deploy una sola vez y consumirla en tres jobs de deploy distintos; más adelante en la serie lo vas a ver cargando estadísticas de corridas de E2E hacia un stage de reportes.
Con qué te quedas
Un solo job compila, todo lo demás consume, los fallos suben sus reportes, y los valores calculados viajan como variables de entorno en lugar de archivos improvisados.
Ahora fíjate en cómo se mueven esos outputs: a lo largo de las fronteras de stage. El job de deploy espera al stage de build completo, aunque solo consuma el dist/ de uno de sus jobs. Con un puñado de jobs no se nota; cuando el archivo crece, la espera se convierte en la parte más lenta del pipeline. En la parte 4 reemplazamos las barreras de stage por un grafo de dependencias de verdad con needs, y partimos los jobs más lentos entre varios runners con parallel.