Artefactos y Outputs
Vamos a entender un poco sobre cómo trabajar con artefactos y salida de los trabajos y caché entre las dependencias.
Cuando se trata de trabajar con datos en un workflow hay una amplia variedad de definiciones de datos a las que podemos referirnos.
Vamos a imaginar que un trabajo hace el build de una aplicación. Ese build podría producir los archivos de un sitio web que serán cargados en un servidor web, los ejecutables para crear un contenedor, un instalador para una aplicación de escritorio, una aplicación móvil para tiendas de apps, etc. Estos archivos son llamados artefactos que son los outputs (activos) generados por un trabajo.
En github podemos hacer la descarga y guardar los artefactos producidos por un trabajo manualmente o usarlos en otro trabajo subsecuente para hacer el deploy, montar una imagen, o cualquier otra cosa.
Vamos al escenario más simple, un sitio web.
name: Deploy website
on:
push:
branches:
- main
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Get code
uses: actions/checkout@v4
- name: Install dependencies
run: npm ci
- name: Lint code
run: npm run lint
- name: Test code
run: npm run test
build:
needs: test
runs-on: ubuntu-latest
steps:
- name: Get code
uses: actions/checkout@v3
- name: Install dependencies
run: npm ci
- name: Build website
run: npm run build # Este comando genera una carpeta dist que contiene lo que necesitamos para el deploy
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- name: Deploy
run: echo "Deploying..." # En este momento sería necesario tener los archivos a mano, pero si no guardamos anteriormente en el proceso de build perdemos todo tan pronto como el trabajo termina.
Ejecutando localmente el proyecto tenemos.

¿Para tener acceso a los archivos qué podríamos hacer?
...
build:
needs: test
runs-on: ubuntu-latest
steps:
- name: Get code
uses: actions/checkout@v3
- name: Install dependencies
run: npm ci
- name: Build website
run: npm run build
- name: Upload artifacts
uses: actions/upload-artifact@v4 # Vamos a usar una action para esto
# La action posee algunas configuraciones
with:
name: dist-files
# Todas las rutas que queremos cargar o no cargar en el caso del ! delante.
path: |
dist
!dist/**/*.md
!dist/**/*.tmp
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- name: Deploy
run: echo "Deploying..."
Usando este workflow tenemos nuestro escenario inicial.

Podemos ver el archivo disponible para descarga manualmente.

Ahora vamos a usarlo en el trabajo siguiente para hacer el deploy.
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- name: Get Build artifacts
uses: actions/download-artifact@v4 # Usamos esta action
with:
name: dist-files # Mismo nombre que usamos para hacer el upload
# Solo por curiosidad vamos a listar el directorio en el que estamos y el anterior
- name: list current dir
run: ls
- name: list parent dir
run: ls ..
- name: Deploy
run: echo "Deploying..."
Podemos observar que la action de download hace la descarga del zip y descomprime en la misma carpeta en la que estamos y elimina el dist-files.zip.
Cuando listé lo que teníamos en la carpeta fue para comprobar esto.

Outputs de Trabajos
Además de los artefactos que son carpetas y archivos tenemos el Job Outputs. Son valores más simples que importan para ser usados en trabajos subsecuentes como por ejemplo el nombre de un archivo, hashes, valores aleatorios, etc.
Los outputs de trabajos serán más estudiados más adelante cuando hagamos una action personalizada.
Si miras en las imágenes arriba verás que el comando npm run build produce una carpeta dist/assets donde tenemos un archivo con el nombre index.xxxxxxxx.js siendo xxxxxxxx es un número aleatorio. Vamos a imaginar que necesitamos ese nombre en el próximo trabajo de deploy.
name: Deploy website
on:
push:
branches:
- main
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Get code
uses: actions/checkout@v3
- name: Install dependencies
run: npm ci
- name: Lint code
run: npm run lint
- name: Test code
run: npm run test
build:
needs: test
runs-on: ubuntu-latest
steps:
- name: Get code
uses: actions/checkout@v3
- name: Install dependencies
run: npm ci
- name: Build website
run: npm run build
- name: Output JS Filename
# Este es el identificador del step que estamos creando para usar después en el output
id: output-js-filename
# buscando un archivo del tipo file finalizando en .js y guardando en una variable js_filename. Después estamos creando un clave valor diciendo que js-filename es igual al contenido de js_filename que es el nombre del archivo.
# estamos colocando también la clave-valor dentro de GITHUB_OUTPUT
run: |
js_filename=$(find dist/assets -type f -name "*.js" | awk -F/ '{print $NF}')
echo "js-filename=$js_filename" >> $GITHUB_OUTPUT
- name: Upload artifacts
uses: actions/upload-artifact@v4 # Vamos a usar una action para esto
# La action posee algunas configuraciones
with:
name: dist-files
# Las rutas que queremos cargar o no cargar en el caso del ! delante.
path: |
dist
!dist/**/*.md
!dist/**/*.tmp
# Clave para definir cuáles son los outputs
outputs:
# Aquí, build-output es el nombre del output del trabajo, y está cogiendo el valor js-filename del step con el id output-js-filename.
# {{ steps }} es una palabra reservada que hace referencia a un contexto y usamos el id para encontrar lo que queremos.
build-output: ${{ steps.output-js-filename.outputs.js-filename }}
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- name: Get Build artifacts
uses: actions/download-artifact@v4
with:
name: dist-files
- name: Print JS Filename
# usar la palabra especial needs consigue referenciar los trabajos.
run: echo "${{ needs.build.outputs.build-output}}"
- name: Deploy
run: echo "Deploying..."
Ejecutando este workflow.



Parece complicado, pero no lo es mucho, vamos a tener mucho estudio de esto más adelante.
Caché
Si observas bien verás que cada workflow tardó cerca de 1 minuto en ejecutar pues es un workflow simple, pero podrían ser muy complejos tardando mucho más tiempo.

El hecho de separar lo que necesitamos hacer por trabajos hace aún el proceso más demorado una vez que todo el ambiente necesita ser creado para ejecutar cada uno de los trabajos y aún la espera secuencial que montamos no ejecutando nada en paralelo por cuestiones de dependencias.
En este caso el workflow tardó la suma de todos los trabajos más el tiempo que lleva montar cada uno de los runners.

Claro que trabajos sin dependencias que ejecutan en paralelo mejoran el rendimiento pero en este caso no sería una opción.
Podemos observar que tenemos trabajos que ejecutan los mismos steps aquí.
name: Deploy website
on:
push:
branches:
- main
jobs:
test:
runs-on: ubuntu-latest
steps:
## BLOCO DE STEPS IDÊNTICOS ##
- name: Get code
uses: actions/checkout@v3
- name: Install dependencies
run: npm ci
##############################
- name: Lint code
run: npm run lint
- name: Test code
run: npm run test
build:
needs: test
runs-on: ubuntu-latest
steps:
## BLOCO DE STEPS IDÊNTICOS ##
- name: Get code
uses: actions/checkout@v3
- name: Install dependencies
run: npm ci
##############################
- name: Build website
run: npm run build
- name: Output JS Filename
id: output-js-filename
run: |
js_filename=$(find dist/assets -type f -name "*.js" | awk -F/ '{print $NF}')
echo "js-filename=$js_filename" >> $GITHUB_OUTPUT
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: dist-files
path: |
dist
!dist/**/*.md
!dist/**/*.tmp
outputs:
build-output: ${{ steps.output-js-filename.outputs.js-filename }}
deploy:
...
Get code con el actions checkout no lleva mucho tiempo, pero instalar las dependencias sí y este es un paso muy común en la gran mayoría de los códigos que trabajamos.
Además de mejorar la velocidad del trabajo y del equipo, reducir el tiempo reduce los costes si estás pagando o si estás en el plan gratuito ahorra el tiempo gratis.
La idea es crear caché para ser aprovechado en otro trabajo en lugar de ejecutar otra vez. Este escenario es importante en casos que los archivos no cambian con frecuencia, pues esto en el checkout no tiene sentido, pues cambian constantemente y el tiempo es pequeño.
Podemos crear la caché entre workflows no solo entre los trabajos de un mismo workflow. Un step de una ejecución de un workflow puede coger la caché de otro workflow en el mismo step.
Existe una action solo para eso llamada cache y vamos a usarla ahora.
Antes vamos a entender sobre lo que vamos a hacer. Cada vez que alteramos un archivo el hash de él cambia y si el archivo no es alterado el hash es el mismo. Para saber si podemos aprovechar la caché necesitamos tener certeza de que no fue alterado.
El archivo package-lock.json en Node.js es un archivo generado automáticamente por el gestor de paquetes npm (Node Package Manager) cuando ejecutas comandos como npm install o npm ci. Se crea para garantizar que las instalaciones de dependencias sean reproducibles y consistentes. Si el hash de este archivo altera sabemos que la caché no puede ser reaprovechada.
Vamos a usar la función hashFile para generar el hash.
name: Deploy website
on:
push:
branches:
- main
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Get code
uses: actions/checkout@v3
# Necesita ser antes de lo que queremos crear la caché en la primera ejecución del código, por eso aquí en test.
# Esta action avisa a github para guardar o sincronizar el path que definimos en algún lugar en github cloud
- name: Cache dependences
uses: actions/cache@v4
with:
# En el caso de node cuando instalamos las dependencias irá a esta carpeta en el home del usuario.
path: ~/.npm
# Si la clave existe restaurará sino generará
key: deps-node-modules-{{ hashFiles('**/package-lock.json') }}
# Este paso será ejecutado siempre, pero con las dependencias ya instaladas será mucho más rápido.
- name: Install dependencies
run: npm ci
- name: Lint code
run: npm run lint
- name: Test code
run: npm run test
build:
needs: test
runs-on: ubuntu-latest
steps:
- name: Get code
uses: actions/checkout@v3
## Copiamos esta parte ##
- name: Cache dependences
uses: actions/cache@v4
with:
path: ~/.npm
key: deps-node-modules-{{ hashFiles('**/package-lock.json') }}
#########################
# ¿Podríamos retirar esta parte del código? Sí, ¿hace mal dejar? No.. Entonces deja que garantiza que si algo sale mal aún ejecutará.
- name: Install dependencies
run: npm ci
- name: Build website
run: npm run build
- name: Output JS Filename
id: output-js-filename
run: |
js_filename=$(find dist/assets -type f -name "*.js" | awk -F/ '{print $NF}')
echo "js-filename=$js_filename" >> $GITHUB_OUTPUT
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: dist-files
path: |
dist
!dist/**/*.md
!dist/**/*.tmp
outputs:
build-output: ${{ steps.output-js-filename.outputs.js-filename }}
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- name: Get Build artifacts
uses: actions/download-artifact@v4
with:
name: dist-files
- name: Print JS Filename
# usar la palabra especial needs consigue referenciar los trabajos.
run: echo "${{ needs.build.outputs.build-output}}"
- name: Deploy
run: echo "Deploying..."
Vemos que a la hora del test no encontró el key.

Sin embargo en el build lo encontró pues el test acabó creando la caché.

Si hacemos una alteración en el código y no tocamos en las bibliotecas en el próximo workflow usará la caché incluso en diferentes ejecuciones de workflow. Vamos a comparar las ejecuciones.

Si actualizamos las bibliotecas localmente usando el npm update veremos que el archivo package-lock.json cambiará incluso el git subirá la diferencia el hash cambiará y el test de la próxima ejecución no tendrá el key no recuperando caché y generando uno nuevo.
❯ npm update
npm warn deprecated @humanwhocodes/[email protected]: Use @eslint/config-array instead
npm warn deprecated @humanwhocodes/[email protected]: Use @eslint/object-schema instead
added 66 packages, removed 29 packages, changed 175 packages, and audited 413 packages in 26s
113 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
❯ git status
On branch main
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: package-lock.json
no changes added to commit (use "git add" and/or "git commit -a")
❯ git add package-lock.json
❯ git commit -m "update packages"
❯ git push origin main