5. Cachés y artefactos
Secciones
Ajustes de lectura
Voz natural grabada para este curso, la misma en todos los navegadores.
Ninguna voz para este idioma en este navegador.
Más voces
Código y términos técnicos en inglés
Desactivado: la voz de la página lo lee todo, sin pausa entre dos voces.
Ninguna voz para este idioma en este navegador.
Más voces
Más voces: Microsoft Edge ofrece voces naturales en línea, y Windows (Configuración › Hora e idioma › Voz), macOS y Android instalan otras.
Dos formas de conservar archivos
Sección titulada «Dos formas de conservar archivos»Cada job empieza en una máquina nueva (lección 1). GitHub ofrece dos almacenamientos que sobreviven a un job, y la documentación insiste en que «no se pueden usar indistintamente»:
| Caché | Artefacto | |
|---|---|---|
| Contiene | archivos que podrías regenerar: paquetes descargados, builds intermedios | archivos que un job produjo: informes de pruebas, paquetes, binarios |
| Compartido entre | ejecuciones del repositorio (por clave y rama) | los jobs de una ejecución, y las personas que lo descargan |
| ¿Falta? | el job debe funcionar igualmente, solo que más lento | el job que lo necesita falla |
| Duración | se elimina tras 7 días sin acceso, 10 GB por repositorio por defecto | 90 días por defecto, retention-days para acortarla |
| Actions | actions/cache, o cache: en las actions setup-* |
actions/upload-artifact, actions/download-artifact |
| Azure Pipelines | Cache@2 |
PublishPipelineArtifact@1, DownloadPipelineArtifact@2 |
Una caché de NuGet necesita archivos de bloqueo
Sección titulada «Una caché de NuGet necesita archivos de bloqueo»La lección 2 lo midió: la caché de Maven de setup-java ahorró de 6 a 15 segundos por job, mientras que los jobs de .NET no guardaban nada en caché. setup-dotnet tiene un input cache: true, pero su README dice que la clave de caché es el hash de los archivos packages.lock.json, y que «si el archivo de bloqueo no existe, esta action lanza un error». El proyecto de ejemplo no tenía ninguno.
Un archivo de bloqueo de NuGet registra la versión exacta y el hash del contenido de cada paquete, directo o transitivo — el equivalente de package-lock.json para npm. Una sola propiedad en un Directory.Build.props junto a la solución lo activa para todos los proyectos (dotnet/Directory.Build.props):
<Project>
<PropertyGroup> <!-- Write packages.lock.json next to each project: setup-dotnet's cache key is its hash (lesson 5) --> <RestorePackagesWithLockFile>true</RestorePackagesWithLockFile> </PropertyGroup>
</Project>Un dotnet restore en local escribió entonces Slugs/packages.lock.json (5 líneas, sin paquetes) y Slugs.Tests/packages.lock.json (170 líneas: los tres paquetes de prueba y sus dependencias). Los dos están en el commit. En la CI, dotnet restore --locked-mode falla en lugar de actualizar el archivo en silencio cuando un paquete ya no coincide con él.
El workflow
Sección titulada «El workflow».github/workflows/gha-05-artifacts.yml tiene cuatro jobs: test en tres sistemas operativos con la caché de NuGet y un informe de pruebas, pack genera un paquete NuGet, cache-demo usa actions/cache directamente, y report descarga los artefactos de los dos primeros.
Los cuatro jobs, y lo que conservan: las cachés vuelven de ejecuciones anteriores, los artefactos pasan de un job a otro dentro de esta ejecución.
flowchart LR nuget[("caché de NuGet: una por sistema operativo, compartida entre ejecuciones")] expensive[("caché gha-05-expensive-Linux-v1")] subgraph matrix["test: un job por sistema operativo"] tu["test (ubuntu-latest)"] tw["test (windows-latest)"] tm["test (macos-latest)"] end pack["pack"] demo["cache-demo"] report["report: needs test y pack"] nuget -.->|"restaurada"| matrix expensive -.->|"restaurada"| demo tu -->|"artefacto test-results-ubuntu-latest"| report tw -->|"artefacto test-results-windows-latest"| report tm -->|"artefacto test-results-macos-latest"| report pack -->|"artefacto package"| report# GitHub Actions course, lesson 5: dependency caches, actions/cache, build artifacts between jobsname: "GHA 05: caches and artifacts"
on: workflow_dispatch: push: branches: [main] paths: ['.github/workflows/gha-05-artifacts.yml', 'code/github-actions/dotnet/**']
jobs: test: name: test (${{ matrix.os }}) strategy: fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macos-latest] runs-on: ${{ matrix.os }} env: NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages defaults: run: working-directory: code/github-actions/dotnet steps: - uses: actions/checkout@v7 - uses: actions/setup-dotnet@v6 with: global-json-file: code/github-actions/dotnet/global.json cache: true cache-dependency-path: code/github-actions/dotnet/*/packages.lock.json - run: dotnet restore --locked-mode - run: dotnet test --no-restore --report-xunit-junit --report-xunit-junit-filename slugs.junit.xml --results-directory TestResults - name: Upload the test report if: always() uses: actions/upload-artifact@v7 with: name: test-results-${{ matrix.os }} path: code/github-actions/dotnet/TestResults/ retention-days: 7
pack: runs-on: ubuntu-latest defaults: run: working-directory: code/github-actions/dotnet steps: - uses: actions/checkout@v7 - uses: actions/setup-dotnet@v6 with: global-json-file: code/github-actions/dotnet/global.json - run: dotnet pack Slugs/Slugs.csproj --configuration Release --output dist -p:Version=1.0.${{ github.run_number }} - uses: actions/upload-artifact@v7 with: name: package path: code/github-actions/dotnet/dist/*.nupkg if-no-files-found: error
cache-demo: runs-on: ubuntu-latest steps: - name: Restore the cache id: cache uses: actions/cache@v6 with: path: expensive key: gha-05-expensive-${{ runner.os }}-v1 - name: Show the cache-hit output run: echo "cache-hit='${{ steps.cache.outputs.cache-hit }}'" - name: Produce the files only on a miss if: steps.cache.outputs.cache-hit != 'true' run: | mkdir -p expensive date -u +%FT%TZ > expensive/created-at.txt echo "produced" - run: cat expensive/created-at.txt
report: needs: [test, pack] if: ${{ !cancelled() }} runs-on: ubuntu-latest steps: - uses: actions/download-artifact@v8 with: pattern: test-results-* path: results - uses: actions/download-artifact@v8 with: name: package path: package - run: find results package -type f | sort - name: Tests per OS run: | for f in results/*/slugs.junit.xml; do echo "$(dirname "$f"): $(grep -o 'tests="[0-9]*" failures="[0-9]*"' "$f" | head -1)" done| Elemento | Por qué |
|---|---|
NUGET_PACKAGES |
mueve la carpeta global de paquetes de NuGet al workspace, como recomienda el README de setup-dotnet: así la caché solo contiene los paquetes de este proyecto, no lo que ya trae la imagen del runner |
cache-dependency-path |
los archivos de bloqueo no están en la raíz del repositorio, donde setup-dotnet busca por defecto |
--report-xunit-junit |
una opción de xUnit v3 (ver dotnet test --help) que escribe un informe JUnit XML; --results-directory elige la carpeta |
if: always() en la subida |
el informe es más útil precisamente cuando las pruebas fallan |
name: test-results-${{ matrix.os }} |
un artefacto por job de la matrix, con un nombre distinto |
if-no-files-found: error |
el valor por defecto es warn: una ruta equivocada no subiría nada y el job pasaría igualmente |
Ejecución 1 → ejecución 2: lo que hizo la caché
Sección titulada «Ejecución 1 → ejecución 2: lo que hizo la caché»La primera ejecución, en el push, no encontró caché y guardó una por sistema operativo al final del job (el step Post):
test (ubuntu-latest) | Dotnet cache is not foundtest (ubuntu-latest) | Cache saved with the key: dotnet-cache-Linux-557e0cce818be1e496015809f30b8f55625063a595165d73158bbd2bdf0b419cUna segunda ejecución, iniciada con gh workflow run gha-05-artifacts.yml, la restauró:
test (ubuntu-latest) | Cache hit for: dotnet-cache-Linux-557e0cce818be1e496015809f30b8f55625063a595165d73158bbd2bdf0b419ctest (ubuntu-latest) | Cache Size: ~54 MB (56306437 B)test (ubuntu-latest) | Cache restored from key: dotnet-cache-Linux-557e0cce818be1e496015809f30b8f55625063a595165d73158bbd2bdf0b419ctest (ubuntu-latest) | Restored /home/runner/work/learn/learn/code/github-actions/dotnet/Slugs.Tests/Slugs.Tests.csproj (in 294 ms).test (ubuntu-latest) | Cache hit occurred on the primary key dotnet-cache-Linux-557e0cce818be1e496015809f30b8f55625063a595165d73158bbd2bdf0b419c, not saving cache.El mismo hash en los tres sistemas operativos (los archivos de bloqueo son idénticos), con el prefijo Linux, Windows o macOS: tres cachés de unos 54 MB cada una, visibles con gh cache list.
Ahora lo que importa — la duración de los steps, según gh api repos/spareilleux/learn/actions/runs/<run-id>/jobs:
| Step | Ubuntu sin caché → acierto | Windows sin caché → acierto | macOS sin caché → acierto |
|---|---|---|---|
setup-dotnet (instalación del SDK + restauración de la caché) |
9 s → 11 s | 35 s → 29 s | 20 s → 12 s |
dotnet restore --locked-mode |
3 s → 1 s | 5 s → 4 s | 3 s → 1 s |
Post setup-dotnet (guardado de la caché) |
1 s → 0 s | 5 s → 1 s | 4 s → 0 s |
El step de restauración pasó de 3 s a 1 s. Descargar 54 MB de caché cuesta más o menos lo mismo: en este proyecto diminuto, la caché de NuGet no ahorra casi nada, y las grandes variaciones del step setup-dotnet vienen de la descarga del SDK, no de la caché. Compensa cuando una restauración descarga cientos de paquetes, y es entonces cuando conviene añadirla — después de medir, como aquí. Los archivos de bloqueo merecen conservarse de todos modos: hacen que la CI restaure exactamente lo que se probó en local.
actions/cache directamente
Sección titulada «actions/cache directamente»setup-java y setup-dotnet construyen la clave por ti. Con actions/cache eliges tú mismo path y key. El job cache-demo, en las dos ejecuciones:
Run 1Cache not found for input keys: gha-05-expensive-Linux-v1cache-hit=''produced2026-09-14T13:12:25ZCache saved with key: gha-05-expensive-Linux-v1
Run 2Cache restored from key: gha-05-expensive-Linux-v1cache-hit='true'2026-09-14T13:12:25ZCache hit occurred on the primary key gha-05-expensive-Linux-v1, not saving cache.- Sin acierto,
cache-hites una cadena vacía, no'false': comprueba!= 'true', nunca== 'false'. - La segunda ejecución imprimió la fecha escrita por la primera: los archivos volvieron.
- Una caché nunca se actualiza: con un acierto no se guarda nada, aunque el job haya cambiado los archivos. Para guardar contenido nuevo, cambia la clave — aquí el sufijo
-v1, o unhashFiles()de los archivos que definen el contenido. - La caché la guarda un step Post, al final del job, solo si el job tuvo éxito (
post-if: success()en elaction.ymlde la action).
Qué cachés puede ver una ejecución
Sección titulada «Qué cachés puede ver una ejecución»Según la documentación: «las ejecuciones de workflow pueden restaurar cachés creadas en la rama actual o en la rama por defecto», además de la rama base en un pull request; no las cachés de ramas hijas o hermanas. Una caché creada por una ejecución pull_request pertenece a la ref de merge y «solo pueden restaurarla las nuevas ejecuciones del pull request». En la práctica: deja que un push a main llene las cachés, y cada rama y cada pull request parten de ellas.
Artefactos
Sección titulada «Artefactos»Cada job test subió su informe, pack subió el paquete:
test (ubuntu-latest) | Artifact test-results-ubuntu-latest has been successfully uploaded! Final size is 510 bytes. Artifact ID is 10348099499pack | Successfully created package '/home/runner/work/learn/learn/code/github-actions/dotnet/dist/Slugs.1.0.1.nupkg'.pack | Artifact package has been successfully uploaded! Final size is 3463 bytes. Artifact ID is 10349121193report los descargó. Con pattern, cada artefacto que coincide va a su propia subcarpeta (merge-multiple: true pondría todos los archivos en una sola carpeta):
Found 4 artifact(s)Filtering artifacts by pattern 'test-results-*'Total of 3 artifact(s) downloadedpackage/Slugs.1.0.1.nupkgresults/test-results-macos-latest/slugs.junit.xmlresults/test-results-ubuntu-latest/slugs.junit.xmlresults/test-results-windows-latest/slugs.junit.xmlresults/test-results-macos-latest: tests="4" failures="0"results/test-results-ubuntu-latest: tests="4" failures="0"results/test-results-windows-latest: tests="4" failures="0"Fechas de caducidad, según gh api repos/spareilleux/learn/actions/runs/<run-id>/artifacts: 7 días para los informes (retention-days: 7), 90 días para el paquete — el valor por defecto del repositorio, el máximo permitido para un repositorio público.
Los mismos artefactos en tu máquina, con la CLI de GitHub:
gh run download 34848185308 --repo spareilleux/learn --name package --dir packagegh run download 34848185308 --repo spareilleux/learn --pattern "test-results-*" --dir resultspackage/Slugs.1.0.2.nupkgresults/test-results-macos-latest/slugs.junit.xmlresults/test-results-ubuntu-latest/slugs.junit.xmlresults/test-results-windows-latest/slugs.junit.xmlPuntos clave
Sección titulada «Puntos clave»- Caché = archivos regenerables compartidos entre ejecuciones; artefacto = archivos producidos por una ejecución, compartidos entre sus jobs y con personas.
setup-dotnetcache: truerequierepackages.lock.json(RestorePackagesWithLockFile); restaura con--locked-mode.- Mide: aquí la caché de NuGet convirtió una restauración de 3 s en 1 s, y descargarla costó lo mismo.
cache-hites'true'o vacío; una caché es inmutable, así que cambia la clave para renovarla.- Da a cada job de la matrix su propio nombre de artefacto, define
if-no-files-found: errory acortaretention-dayspara los informes.
Ejercicios
Sección titulada «Ejercicios»- Dos jobs de matrix (
ubuntu-latest,macos-latest) suben un artefacto con el mismo nombre,os, que contiene un archivo con el nombre del sistema operativo. Un job posterior descarganame: os. ¿Qué pasa?
Solución
El README de upload-artifact dice que «los artefactos creados por upload-artifact@v4 son inmutables», y advierte que en una matrix, subir al mismo artefacto significa que «encontrarás errores de conflicto». No es lo que ocurrió en gha-05-exercises.yml, tres veces seguidas: las dos subidas tuvieron éxito y la ejecución tenía dos artefactos llamados os.
same-name (macos-latest) | Artifact os has been successfully uploaded! Final size is 141 bytes. Artifact ID is 10348738133same-name (ubuntu-latest) | Artifact os has been successfully uploaded! Final size is 142 bytes. Artifact ID is 10348957530same-name-download | Downloading single artifactsame-name-download | ubuntu-latestLa descarga eligió uno de ellos sin ningún aviso — el de Ubuntu, tanto en el job como con gh run download --name os. Cuál se elige no está documentado: por verificar si es estable. En cualquier caso, un informe puede desaparecer en silencio. Pon el valor de la matrix en el nombre.
- Quita el input
cache-dependency-pathy usadotnet-version: 10.0.xen lugar deglobal-json-file, manteniendocache: true. ¿Qué hace el job?
Solución
Falla en el step setup-dotnet, después de instalar el SDK (setup-dotnet/src/cache-restore.ts):
Sin cache-dependency-path, la action busca packages.lock.json solo en la raíz del repositorio. La caché no es opcional una vez que la pides: sin archivo de bloqueo, no hay job.
- Un step usa
key: counter-${{ github.run_id }}yrestore-keys: counter-, y luego suma uno a un número guardado en la carpeta en caché. ¿Qué muestrancache-hity el contador en la primera ejecución, y en la segunda?
Solución
Run 1Cache not found for input keys: gha-05-counter-34847802892, gha-05-counter-cache-hit=''counter is now 1Cache saved with key: gha-05-counter-34847802892
Run 2Cache restored from key: gha-05-counter-34847802892cache-hit='false'counter is now 2Cache saved with key: gha-05-counter-34848189352En la ejecución 2 la clave exacta no existía (nuevo run_id), pero el prefijo gha-05-counter- coincidió con la caché más reciente: cache-hit es 'false' — una coincidencia parcial —, los archivos se restauran igualmente y, como no se encontró la clave exacta, se guarda una caché nueva al final. Así se construye una caché incremental que mejora de ejecución en ejecución, a costa de una nueva entrada de caché por ejecución.