Controla quién escribe la caché en GitHub Actions
cache-mode permite limitar por job quién restaura o guarda cachés en tus pipelines de Laravel.
El problema está en dar permisos de más
En muchos pipelines de Laravel, todos los jobs comparten la misma capacidad para restaurar y guardar cachés. Parece cómodo hasta que el workflow también se ejecuta con código que no considero de confianza, por ejemplo en contribuciones externas. Un job de tests necesita acelerar composer install o npm ci, pero no necesariamente debería poder publicar una caché que reutilizará un job posterior.
GitHub Actions incorpora cache-mode para separar esas responsabilidades. La opción se puede definir para todo el workflow o de forma individual en cada job, y el valor del job tiene prioridad sobre el valor general. El acceso se aplica mediante tokens de caché con alcance limitado, así que no depende únicamente de que una acción se comporte correctamente.
Cuatro modos para una decisión sencilla
read permite restaurar cachés, pero no guardarlas. write permite ambas operaciones, mientras que write-only impide restaurar y permite guardar. Por último, none bloquea tanto la restauración como el guardado de caché.
Para mí, el valor útil no está en memorizar los cuatro modos, sino en asignarlos según el propósito del job. Los tests y los análisis estáticos suelen encajar en read; el job que actualiza una caché desde una rama de confianza puede usar write. write-only tiene sentido si quiero calentar una caché nueva sin consumir una previa, y none sirve para jobs donde la caché no aporta nada o no quiero que exista esa vía de acceso.
Un patrón para Laravel con npm y Composer
Mi punto de partida sería dejar el workflow completo en modo lectura y conceder escritura sólo al job que se ejecuta después de un push a main. Así, los jobs que validan una pull request pueden aprovechar cachés existentes sin poder modificarlas. El ejemplo usa la caché de npm, pero el mismo reparto de permisos aplica si en otros pasos restauro la caché de Composer.
name: Tests Laravel
on:
pull_request:
push:
branches:
- main
cache-mode: read
jobs:
tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Restaurar caché de npm
uses: actions/cache@v4
with:
path: ~/.npm
key: npm-${{ hashFiles('**/package-lock.json') }}
- name: Instalar dependencias y ejecutar tests
run: |
composer install
npm ci
php artisan test
actualizar-cache:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
cache-mode: write
steps:
- uses: actions/checkout@v6
- name: Preparar dependencias
run: |
composer install
npm ci
despliegue:
runs-on: ubuntu-latest
cache-mode: none
steps:
- name: Desplegar
run: echo "El despliegue no usa la caché de Actions"No hace falta convertir todos los jobs en jobs de escritura para que el pipeline siga funcionando. Los jobs que no tengan permiso para restaurar tratarán esa operación como un fallo de caché, y los que no tengan permiso para guardar simplemente no crearán una entrada. GitHub indica que esas restricciones no hacen fallar por sí solas el job ni el workflow, algo que conviene tener presente al revisar los logs.
La letra pequeña importa
cache-mode no arregla una clave de caché mal diseñada. Si una caché mezcla dependencias, artefactos generados y resultados de build sin una clave que represente bien el contenido, seguiré teniendo problemas de consistencia aunque limite quién puede escribir. También debo revisar qué directorios guardo: cachear dependencias descargadas no es lo mismo que cachear un vendor completo o una salida de build que después se ejecutará.
Tampoco conviene añadir write o write-only a triggers de baja confianza por comodidad. GitHub avisa expresamente de que hacerlo puede saltarse la restricción segura de sólo lectura y volver a introducir riesgo de cache poisoning. La configuración explícita es potente precisamente porque cambia el comportamiento por defecto; hay que tratarla como un permiso de seguridad, no como un ajuste de rendimiento.
En runners propios el límite sigue siendo útil
En mi homelab, un runner propio no convierte automáticamente todos los jobs en confiables. Si el runner procesa workflows de repositorios o ramas con distintos niveles de confianza, separar el permiso de caché reduce una superficie que suele quedar olvidada detrás de la optimización del pipeline. Es una medida pequeña, pero encaja bien con la idea de que cada job reciba sólo los permisos que necesita.
También es una buena excusa para ordenar el workflow de Laravel. Un job dedicado a preparar dependencias desde main puede ser el único que escriba, mientras que tests de PHP, frontend, análisis estático y validaciones de pull requests se quedan en lectura. El resultado no es un pipeline más complejo: es un pipeline donde la intención de cada job queda escrita en el YAML.