wandres.dev
CÓMO FUNCIONA LA CACHÉ · hashing e invalidación

Outputs y su restauración: la respuesta que la caché guarda

Si el hash es la pregunta, los outputs son la respuesta que la caché almacena para no volver a calcularla. Una entrada de caché no guarda solo archivos: guarda el artefacto, los logs de la terminal y el código de salida, de modo que restaurar sea reproducir la corrida entera sin ejecutarla. Aquí se estudia qué se captura, cómo se reconstruye en un hit y por qué declarar mal los outputs convierte la caché en una fuente de artefactos rotos que aún así parecen exitosos.

⏱ 16 min

Todo el nivel anterior giraba en torno a la clave; ahora toca el valor. Si el hash es la pregunta —qué tarea, con qué entradas—, los outputs son la respuesta que la caché archiva para no tener que volver a deducirla. Y la respuesta es más rica de lo que parece: no basta con guardar los archivos que la tarea produjo, porque una corrida no es solo su artefacto, es también lo que imprimió y cómo terminó. Restaurar bien es reproducir la corrida completa —artefacto, logs y veredicto— con tal fidelidad que quien mira la terminal no distinga un hit de una ejecución real. Y ahí, en esa exigencia de fidelidad total, es donde una declaración descuidada de outputs se vuelve peligrosa.

🎯 Al terminar esta lección sabrás
  • Distinguir los tres componentes de una entrada de caché: artefactos, logs y código de salida.
  • Seguir el ciclo de captura en un miss y el de restauración en un hit.
  • Entender la reproducción de logs como parte esencial de restaurar, no como adorno.
  • Reconocer por qué unos outputs incompletos producen aciertos que restauran artefactos rotos.

Qué se guarda: artefacto, logs y veredicto

Una entrada de caché es un par: una clave —el hash— y un valor. El error común es imaginar que el valor son solo los archivos generados. Es más que eso. Una tarea de build no produce únicamente un directorio dist; produce también un flujo de salida por la terminal y termina con un código de salida que dicta si tuvo éxito o falló. Los tres forman parte de lo que hay que guardar, porque los tres forman parte de “lo que pasó” cuando la tarea corrió.

📦

Artefactos

Los archivos y directorios que la tarea genera, declarados en outputs: el dist, los tipos compilados, los assets emitidos. El producto tangible.

📜

Logs

Todo lo que la tarea escribió por la salida estándar y de error, capturado para poder reproducirlo tal cual en un futuro hit.

🏁

Código de salida

El veredicto de la corrida. Cachear un cero permite saltar; muchas herramientas se niegan a cachear un fallo, porque un error no es un resultado reutilizable.

Junto a esos tres componentes, la entrada suele cargar algo de metadato: la propia clave del hash bajo la que se guarda, la duración de la corrida original y a veces un pequeño manifiesto de qué archivos contiene. Ese metadato no es decorativo: es lo que permite a la herramienta decirte “restaurado de caché, ahorrados 42 segundos” y lo que sostiene las métricas de tiempo ahorrado que justifican tener la caché. El artefacto es el cuerpo de la entrada; el metadato es su etiqueta.

Guardar el código de salida tiene una consecuencia sutil: la caché es, por norma, un archivo de éxitos. La mayoría de las herramientas solo almacenan corridas que terminaron en cero, porque un fallo depende con frecuencia de factores efímeros —una red caída, un recurso ocupado— que no forman parte del hash y que, por tanto, no deben quedar congelados bajo una clave. Reutilizar un fallo sería peor que recomputarlo.

Tareas sin artefacto: cuando el output son los logs

No toda tarea produce archivos, y esas son el mejor recordatorio de que la entrada de caché es más que su artefacto. Un lint o un test que solo verifica —que no emite un dist— declara una lista de outputs vacía, y aun así se cachea con todo el sentido: lo que se guarda es su flujo de logs y su código de salida. Un hit sobre ese lint no restaura ningún archivo, pero reproduce sus advertencias y devuelve su cero, que era justo su producto: la afirmación, reutilizable, de que ese estado de fuentes pasa la verificación. Aquí se ve con nitidez que la caché no cachea archivos, sino el resultado observable de una tarea, tenga forma de artefacto o de veredicto.

Cómo se restaura una corrida

En un miss, el ciclo es de captura: la herramienta ejecuta la tarea, recoge los archivos que caen bajo los patrones de outputs, captura el flujo de logs y anota el código de salida, y empaqueta los tres bajo la clave del hash —en disco local y, si hay caché remota, subiéndolos al almacén compartido—. En un hit, el ciclo se invierte: recupera la entrada por su clave, copia los artefactos de vuelta a sus rutas declaradas, reproduce los logs por la terminal y devuelve el código de salida guardado.

flowchart TD
M[Cache miss] --> R[Ejecuta la tarea]
R --> Cap[Captura artefactos, logs y codigo de salida]
Cap --> S[Almacena bajo la clave del hash]
H[Cache hit] --> F[Recupera la entrada por su clave]
F --> Res[Restaura artefactos en sus rutas]
Res --> Rep[Reproduce los logs y devuelve el codigo]
style H fill:#a6e3a1,color:#11111b
style M fill:#f9e2af,color:#11111b

La reproducción de los logs merece defenderse, porque parece un lujo y es una necesidad. El principio rector de una buena caché es la transparencia: un hit debe ser indistinguible de una ejecución real salvo por su velocidad. Si al reutilizar una tarea desaparecieran los avisos, las advertencias del compilador o los mensajes que tu pipeline inspecciona más adelante, la caché habría cambiado el comportamiento observable del sistema, y eso rompe el contrato de que reutilizar es equivalente a recomputar. Por eso las herramientas capturan la salida palabra por palabra y la re-emiten en el hit —el “replay” instantáneo—: para que ni tú ni el resto del pipeline noten la diferencia.

En una caché remota el mismo ciclo cruza la red. El artefacto se comprime en un tarball, se sube al almacén de objetos con la clave del hash como nombre, y en un hit remoto se descarga y se descomprime en su sitio. Que el envío atraviese red es la razón de que el tamaño de lo que declaras como output importe: cada archivo de más es ancho de banda y almacenamiento de más, multiplicado por cada corrida de cada máquina del equipo.

Modos de restauración y el invariante que los une

No todos los hits restauran desde el mismo sitio ni con las mismas reglas. Un hit local copia los artefactos desde el directorio de caché de la máquina; un hit remoto los descarga y descomprime. En CI es común configurar la caché remota en modo de solo lectura —lee para acelerar pero no escribe—, o al revés, de solo escritura para alimentar a los demás sin consumir. Y hay una sutileza de eficiencia: si los artefactos ya están en disco y coinciden con los guardados, algunas herramientas se ahorran la copia. Debajo de toda esa variedad late un único invariante que jamás puede romperse: sea cual sea el modo, el estado final en disco tras un hit tiene que ser idéntico al que dejaría una ejecución real. El modo es una decisión de rendimiento; la fidelidad del resultado, no.

Outputs incompletos igual a caché corrupta

Aquí está el filo del cuchillo. La declaración de outputs sufre la misma asimetría que la de inputs, pero con las consecuencias volcadas al otro lado. Declarar de más es un pecado de rendimiento: guardas y transfieres archivos inútiles, la caché engorda y los hits se vuelven más lentos de lo necesario. Molesto, pero honesto. Declarar de menos es un pecado de corrección, y es silencioso y devastador.

{
  "tasks": {
    "build": {
      "outputs": ["dist/**", "types/**"]
    }
  }
}

Imagina que esa tarea genera de verdad dist, types y un manifest.json en la raíz, pero solo declaraste los dos primeros. La primera corrida es un miss: la tarea se ejecuta entera, escribe los tres, y todo funciona porque el manifest.json está ahí de la ejecución real. La caché, en cambio, solo guardó dist y types. El desastre llega en el primer hit en un entorno limpio —otra máquina, CI, tras borrar artefactos—: la caché restaura dist y types, reproduce los logs de éxito, devuelve un código cero triunfal… y el manifest.json no existe, porque nunca se guardó. La tarea “tuvo éxito” desde la caché y dejó un artefacto roto.

⚠️
El hit que restaura menos de lo que produjo

Este es el modo de fallo más traicionero de toda la caché, porque invierte la intuición. Con inputs incompletos, el peligro es reutilizar cuando no debías; con outputs incompletos, el peligro es que la reutilización, en sí correcta, te entregue un artefacto mutilado. Y no se manifiesta en la máquina donde ejecutaste la tarea de verdad —allí los archivos no declarados están sobre el disco de la corrida real—, sino en la siguiente máquina que dependa solo de la caché. Por eso el síntoma clásico es “funciona en mi equipo pero el build cacheado de CI produce un artefacto incompleto”: no es una diferencia de entorno, es un output que declaraste de menos.

ℹ️
Los outputs también son parte del contrato de la tarea

Una tarea bien definida declara con simetría qué consume y qué produce. Los inputs gobiernan cuándo se reutiliza; los outputs, qué se restaura al reutilizar. Ambos son declaraciones que la herramienta toma al pie de la letra: no escanea tu artefacto para adivinar qué archivos generaste, igual que no lee tu código para deducir qué archivos consumes. Si la tarea empieza a emitir un archivo nuevo —un mapa de fuentes, un reporte, un manifiesto—, su declaración de outputs tiene que crecer con ella, o la caché aprenderá a servir una versión mutilada de su trabajo.

El coste simétrico: declarar de más

Si declarar de menos corrompe, declarar de más engorda. La asimetría no significa que puedas pecar de generoso sin consecuencias: incluir en los outputs archivos que la tarea no necesita restaurar tiene un precio real, solo que se paga en rendimiento y no en corrección. Cada archivo sobrante infla el tarball que se comprime, sube y baja en cada hit remoto, ocupa espacio en el almacén compartido y ralentiza justo la operación que la caché existía para acelerar. Un dist que arrastra por error una carpeta de artefactos intermedios o un directorio de dependencias puede convertir un hit que debía durar milisegundos en una descarga de cientos de megabytes.

Declaración Pecado Síntoma Coste
de menos corrección artefacto restaurado incompleto despliegue roto, silencioso
de más rendimiento tarballs enormes, hits lentos tiempo y almacenamiento
exacta ninguno hit fiel a la ejecución real el objetivo

El punto dulce, entonces, no es “declara todo por si acaso” ni “declara lo mínimo para ahorrar”, sino declarar exactamente lo que la tarea produce y algún consumidor río abajo necesita: ni un archivo de menos, que rompería la restauración, ni uno de más, que la encarecería. A diferencia de los inputs —donde ante la duda conviene incluir, porque el error barato es el falso miss—, en los outputs los dos errores tienen dientes, y el criterio correcto es la exhaustividad ajustada: todo lo que la tarea genera y se vaya a usar, y nada de lo que solo fue un paso intermedio.

💡
Vigila el tamaño de tus entradas de caché

Un artefacto que crece sin explicación suele delatar un output declarado de más: un glob demasiado ancho que se tragó una carpeta que no debía. Revisar de vez en cuando el peso de tus entradas de caché —lo que de verdad se sube y se baja— es tan saludable como revisar el desglose del hash. Una caché sana no solo acierta cuando debe y restaura lo que produjo, sino que transfiere lo justo: ni un byte de trabajo intermedio que nadie va a consumir.

Restaurar no es copiar archivos: es prometer que reutilizar equivale a ejecutar

Hay una promesa implícita en el corazón de toda caché de build, y los outputs son el lugar donde esa promesa se cumple o se rompe: la promesa de que un hit es indistinguible de una ejecución real salvo en el tiempo que ahorra. Suena modesta hasta que se enuncian sus implicaciones. Significa que la caché no puede guardar solo el producto visible —el dist que a ti te importa— sino todo lo observable de la corrida: cada archivo que quedó en disco, cada línea que se imprimió, el mismísimo veredicto de éxito o fracaso, porque cualquier cosa que un consumidor río abajo pudiera haber observado de la ejecución real tiene que estar disponible también en la reutilización, o la equivalencia se rompe y la caché empieza a mentir por omisión. La declaración de outputs es, vista así, la frontera que separa lo que la caché promete restaurar de lo que dejará caer en el olvido, y su calidad no supera jamás la exhaustividad de esa frontera. Un desarrollador que declara sus outputs mirando solo el directorio que le interesa, y olvida el manifiesto en la raíz o el mapa de fuentes en otra carpeta, no ha cometido un descuido cosmético: ha definido una caché que restaura una fotografía recortada de la realidad, y esa fotografía recortada se desplegará algún día como si fuera completa. La asimetría con los inputs es la clave que hay que grabar a fuego: un input olvidado hace que la caché acierte cuando no debía —un problema de cuándo—; un output olvidado hace que la caché, acertando correctamente, entregue menos de lo que la tarea produjo —un problema de qué—. El primero corrompe la decisión de reutilizar; el segundo corrompe el contenido de lo reutilizado. Y como el segundo solo aflora en la máquina que confía en la caché en vez de en la que ejecutó, puede vivir latente durante semanas hasta que un despliegue limpio lo despierta. Declarar bien los outputs no es burocracia de configuración: es honrar la única promesa que hace que valga la pena tener una caché.

⚔️ Rompe y repara la restauración
  1. Ejecuta una tarea de build en un entorno limpio y anota todos los archivos y directorios que genera, no solo el que te interesa.
  2. Compara esa lista con tu declaración de outputs y localiza cualquier archivo que la tarea produzca pero tú no declares.
  3. Fuerza un hit en un entorno limpio —borra los artefactos y vuelve a correr— y confirma que todo lo declarado se restaura y los logs se reproducen intactos.
  4. Provoca el fallo a propósito: quita un artefacto real de tu declaración de outputs, cachea, limpia y restaura; comprueba que el hit “tiene éxito” pero deja el artefacto incompleto.
  5. Repara la declaración, repite la restauración limpia y verifica que ahora el artefacto reconstruido desde caché es idéntico al de una ejecución real.