Stages
Vamos a evolucionar un poco más y crear dos jobs. Vamos a modificar un poco el pipeline para que empieces a entender los stages.
# ¡Buena práctica que aprendimos!
default:
image: debian:bullseye-slim
tags:
- general
build_project:
# Solo para recordar podríamos definir imagen aquí sobrescribiendo el valor default.
# image: debian:bullseye-slim
script:
- echo "Build project"
- mkdir build
- cd build
- echo "Our build" > build.txt
- cat build.txt
# Vamos a probar si conseguimos obtener el valor de una variable masked.
# Primero colocándola en un archivo para ver qué pasa.
- echo $VAR2 > var2.txt
- cat var2.txt
# Dos pruebas extra copiando la variable a otra y colocándola en el archivo.
- NOVA_VAR2=$VAR2
- echo $NOVA_VAR2
- echo $NOVA_VAR2 > novavar2.txt
- cat novavar2.txt
# En este job vamos a probar el archivo.
test_project:
script:
- echo "Probando si el archivo existe..."
- test -f build/build.txt
- echo "Probando si el contenido existe..."
- grep "Our build" build.txt
❯ git add .gitlab-ci.yml
❯ git commit -m "test more jobs"
❯ git push origin main
Veamos qué pasó...
test es lo que llamamos stage. Estos dos jobs están ejecutándose en el mismo stage y en paralelo sin dependencia entre ellos.
Si no se especifica un stage por defecto será
test. Es buena práctica y debemos siempre especificar los stages.

test_project se ejecutó antes que build_project, es decir, se ejecutaron juntos (en paralelo) sin ningún tipo de dependencia.

Por curiosidad veamos qué pasó con las variables masked del job build_project...
...
Using docker image sha256:6ad184acc3babba4df7e3126b4a8a13bed727901c11fc121f141445f98e1ddba for debian:bullseye-slim with digest debian@sha256:7aafeb23eaef5d5b1de26e967b9a78f018baaac81dd75246b99781eaaa2d59ef ...
$ echo "Build project"
Build project
$ mkdir build
$ cd build
$ echo "Our build" > build.txt
$ cat build.txt
Our build
$ echo $VAR2 > var2.txt
$ cat var2.txt
[MASKED]
$ NOVA_VAR2=$VAR2
$ echo $NOVA_VAR2
[MASKED]
$ echo $NOVA_VAR2 > novavar2.txt
$ cat novavar2.txt
[MASKED]
Cleaning up project directory and file based variables
00:01
Job succeeded
El valor en realidad se lee normalmente, lo que pasa es que el frontend de GitLab "enmascara" el valor durante la visualización de los logs. Este valor está dentro del archivo, fue mostrado en la consola, pero no fue expuesto en GitLab.
GitLab solo lo oculta en la UI. ¿Quieres ver?
Ya vamos a resolver la situación de no dejar que estos jobs se ejecuten en paralelo. Vamos a crear dos stages.
# Buena práctica que aprendimos!
default:
image: debian:bullseye-slim
tags:
- general
stages: # La secuencia que se define de los stages garantiza un orden.
- build
- test
build_project:
stage: build
# Solo para recordar podríamos definir imagen aquí sobrescribiendo el valor default.
# image: debian:bullseye-slim
script:
- echo "Build project"
- mkdir build
- cd build
- echo "Our build" > build.txt
- cat build.txt
- echo "$VAR2" | base64 > segredo.b64
- echo "VAR2 codificado en base 64"
- cat segredo.b64
# En este job vamos a probar el archivo.
test_project:
stage: test
script:
- echo "Probando que el archivo existe..."
- test -f build/build.txt
- echo "Probando si el contenido existe..."
- grep "Our build" build.txt
❯ git push origin main
❯ git commit -m "build and test stage"
❯ git push origin main
Verificando podemos ver que se hizo el orden, pero aun así el segundo job tuvo problemas.

El primer job tiene el siguiente log.
...
$ echo "Build project"
Build project
$ mkdir build
$ cd build
$ echo "Our build" > build.txt
$ cat build.txt
Our build
$ echo "$VAR2" | base64 > segredo.b64
$ echo "VAR2 codificado en base 64"
VAR2 codificado en base 64
$ cat segredo.b64
dmFyaWFibGUyCg==
Cleaning up project directory and file based variables
00:01
Job succeeded
Si decodificamos este valor base64 ¿qué tenemos?
❯ echo "dmFyaWFibGUyCg==" | base64 --decode
variable2
Es decir, solo porque no se mostró en la interfaz no significa que no puedas descubrirlo. Al final del curso vamos a hablar de una lista de métodos que podemos usar para revelar estas variables. La intención no es enseñarte cómo burlar las cosas, sino cómo defenderte. Quien sabe cómo hacer sabe cómo protegerse.
Por último, el segundo job tuvo el siguiente problema.
...
$ echo "Probando que el archivo existe..."
Probando que el archivo existe...
$ test -f build/build.txt
Cleaning up project directory and file based variables
00:02
ERROR: Job failed: exit code 1
No encontró el archivo. Estos contenedores se ejecutan en ambientes completamente aislados. Lo que pasó en el contenedor del build_project murió con él.
-
Aprendimos que los stages garantizan orden solamente entre stages, pero no entre jobs del mismo stage.
-
Los stages ayudan a dividir las etapas para que sea más fácil ver el proceso.
-
Los pipelines se activan todas las veces que ocurre un cambio en el repositorio. Hasta ahora, siempre que hicimos cualquier modificación en el repositorio el pipeline se ejecutó. ¿Fue solo porque fue en la rama main? No, y por último haremos una demostración de eso.
-
La consola enmascara la visualización de un secret, pero no el valor real de ella durante el proceso de ejecución. Con un acceso directo al contenedor podríamos ver todo. Es necesario protegerse de algunos métodos que burlan este escenario.
Artifact
El artifact es una especie de output que podemos guardar en el contexto del pipeline. Es como si fuera un file system que en el primer job inicia vacío, pero en los demás jobs obtienen las cosas que se añaden dentro de él.
Cuando guardamos algo podemos utilizarlo en otro job.
Entonces vamos a modificar para que build_project guarde la carpeta build.
default:
image: debian:bullseye-slim
tags:
- general
stages:
- build
- test
build_project:
stage: build
script:
- echo "Build project"
- mkdir build
- cd build
- echo "Our build" > build.txt
- cat build.txt
artifacts: # Guardando esta carpeta en el contexto
paths:
- build/
test_project:
stage: test
script:
- echo "Probando que el archivo existe..."
# Observa que estamos respetando la jerarquía de directorio.
- test -f build/build.txt
- echo "Probando si el contenido existe..."
- grep "Our build" build/build.txt
❯ git add .gitlab-ci.yml
❯ git commit -m "add artifact"
❯ git push origin main
¡Y tenemos éxito!

Incluso podemos tener acceso a esos artefactos.

Sabiendo esto, esta es una prueba más a realizar en relación con las variables.
Esto funcionaría perfectamente si hiciéramos la descarga del artefacto
build_project:
stage: build
script:
- echo "Build project"
- mkdir build
- cd build
- echo "Our build" > build.txt
- cat build.txt
- echo $VAR3 > var3.txt # Probando variable masked
- cat build.txt
artifacts: # Guardando esta carpeta en el contexto
paths:
- build/
Al hacer la descarga de este artefacto por la interfaz gráfica a la carpeta de descargas...
❯ cd ~/Downloads
❯ unzip artifacts.zip
Archive: artifacts.zip
replace build/build.txt? [y]es, [n]o, [A]ll, [N]one, [r]ename: n
inflating: build/var3.txt
❯ cat build/var3.txt
variable3
¡Esto ya vale para que prestes atención al hacer un review que involucra pipeline!
Vamos a cambiar de rama para demostrar que el pipeline también se activará.
❯ git checkout -b develop
Switched to a new branch 'develop'
❯ git push origin develop
Total 0 (delta 0), reused 0 (delta 0), pack-reused 0
remote:
remote: To create a merge request for develop, visit:
remote: https://gitlab.com/puziol/first-pipeline/-/merge_requests/new?merge_request%5Bsource_branch%5D=develop
remote:
To gitlab.com:puziol/first-pipeline.git
* [new branch] develop -> develop

¿Y si hacemos un merge? De develop para main también se ejecutará.
Vale la pena observar algunos detalles todavía...

En la barra lateral izquierda tenemos la duración, tiempo en cola, qué runner ejecutó y su tag, qué evento disparó (un push), los artefactos que podemos mantener, descargar o ver vía navegador, el número del commit, etc. En navegador tenemos...

Observa en el log que hizo la subida de los artifacts antes de terminar el job. Al terminar el job este contenedor es destruido, pero el artefacto se mantuvo.
En la secuencia el job test_project hizo la descarga del artefacto antes de iniciar el job.

GitLab actúa como un servidor de archivos entre jobs.
Por defecto no necesitamos definir algunos stages, pues están preconfigurados en GitLab. Al marcar jobs para estos stages sin definirlos no tendremos errores. Sin embargo, es buena práctica definir los stages para que quede claro, principalmente para quien no sabe esto.
Los stages por defecto están en el siguiente orden.
stages:
- .pre
- build
- test
- deploy
- .post
Algunos detalles que vale la pena mencionar sobre definición de jobs.
# Dos jobs con nombres idénticos
build:
stage: build
image: node:22-slim
script:
- node --version
- npm --version
- npm ci
- npm run build
build: # Esto sobrescribe todo el job build anterior. El archivo se lee de arriba abajo, por lo tanto la última definición del job es la que prevalecerá
stage: teste
image: alpine
script:
- mkdir teste
Y si un job comienza con . entonces es ignorado en la ejecución, es decir, ni necesitamos comentarlo.
# Sería más o menos lo mismo
.build:
stage: teste
image: alpine
script:
- mkdir teste
# build:
# stage: teste
# image: alpine
# script:
# - mkdir teste
Sin embargo, lo que escribí arriba no es verdad. Aunque .build sea ignorado puede funcionar como plantilla, diferente de cuando comentamos. Una plantilla nunca se ejecuta sola. Podemos utilizar .build como plantilla y sobrescribir solo la parte que queremos. Haremos esto más adelante, pero aquí es solo una pequeña demostración.
.build:
stage: teste
image: alpine
script:
- mkdir teste
build:
extends: .build
# Este build tiene todo esto
# stage: teste
# image: alpine
# script:
# - mkdir teste
Aún existen otras formas que aprenderemos en el futuro.