Saltar al contenido
Volver al blog
LaravelDevOpsSeguridad

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.

Ismael Catala4 min de lectura

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.


Fuente: GitHub Changelog

Documentación oficial de cache-mode