Un workflow che esegue npm ci a ogni push scarica dal registry le stesse centinaia di pacchetti ogni volta, anche quando il lockfile non cambia da una settimana. Su un progetto con un albero di dipendenze reale sono uno o due minuti persi prima ancora che parta il primo test, e li paghi di nuovo a ogni push e a ogni pull request. actions/cache conserva quei download tra un’esecuzione e l’altra e te li restituisce quando non è cambiato nulla di rilevante.
Come actions/cache decide se è un hit o un miss
actions/cache archivia ogni voce sotto una stringa che scegli tu, la key, con ambito limitato al branch che l’ha creata. Un’esecuzione può leggere le cache del proprio branch e quelle del branch predefinito. Al ripristino, l’action confronta la tua key con quello che è salvato:
- Una corrispondenza esatta con
keyè un hit. I file vengono scaricati e il job prosegue. - Se non c’è corrispondenza esatta si passa a
restore-keys, controllate in ordine, ciascuna verificata come prefix match contro le key esistenti. Vince la corrispondenza piĂą recente. - Se non c’è nessuna corrispondenza lo step non ripristina nulla. Il job parte a freddo e, se va a buon fine, salva una nuova voce sotto
key.
Per questo il pattern standard include l’hash del lockfile nella key:
| |
Cambia una riga in package-lock.json e l’hash cambia: la key esatta manca il colpo di proposito. Vuoi una cache che corrisponda al nuovo lockfile, non a quello della settimana scorsa. Il prefix di restore-keys invece continua a corrispondere, quindi il job parte dalla cache precedente invece che da una cartella vuota, scarica solo ciò che è nuovo e salva una nuova voce a corrispondenza esatta quando va a buon fine.
Valgono due limiti. Un repository ha 10 GB di spazio per la cache, condivisi tra tutti i suoi workflow, e ogni voce inutilizzata per 7 giorni viene eliminata. Non devi ripulire nulla a mano: quando il repository supera il limite vengono eliminate le voci piĂą vecchie, e la prossima esecuzione che ne aveva bisogno paga una volta sola il costo dell’installazione a freddo.
La cache di npm con l’opzione cache di setup-node
Per i package manager piĂą comuni non serve nemmeno scrivere uno step actions/cache. actions/setup-node, actions/setup-python e actions/setup-java accettano tutti un input cache che si occupa di key, ripristino e salvataggio al posto tuo:
| |
cache: 'npm' trova package-lock.json (o npm-shrinkwrap.json) nella root del repository, ne calcola l’hash e mette in cache i pacchetti scaricati da npm. cache: 'yarn' fa lo stesso con yarn.lock. In un monorepo dove il lockfile si trova in packages/api/package-lock.json, indicalo esplicitamente:
| |
setup-python funziona allo stesso modo: cache: 'pip' (oppure 'pipenv', 'poetry'), con l’hash calcolato di default su requirements.txt. setup-java accetta cache: 'maven' o cache: 'gradle'. Parti da qui. Passa a uno step actions/cache manuale solo quando ti serve una key o un path che la setup action non ti dĂ , oppure quando devi mettere in cache qualcosa che non ha niente a che fare con un runtime di linguaggio.
Cache di Maven e pip con actions/cache diretto
Uno step manuale ha sempre le stesse tre parti: la directory in cui lo strumento scrive la propria cache, una key costruita sul file che fissa le versioni delle dipendenze, e un fallback a prefix.
| |
| |
path è la cache dello strumento, non del tuo progetto: i pacchetti scaricati da pip, non il tuo virtualenv; il repository locale di Maven, non target/. Puoi mettere in cache anche l’output della build come target/ o dist/, ma conviene solo se la build è incrementale. Altrimenti comprimi e carichi file che l’esecuzione successiva butta via.
Se metti giĂ in cache i layer Docker con cache-to: type=gha, come in Costruire e Pubblicare un’Immagine Docker con GitHub Actions, quei layer finiscono nello stesso servizio di cache di Actions. Contano sugli stessi 10 GB e sulla stessa eliminazione a 7 giorni delle tue cache di dipendenze. Un repository che fa entrambe le cose raggiunge il limite prima di quanto pensi, e cache dei layer molto grandi possono portare all’eliminazione della tua cache di npm.
Le key della cache nelle build a matrice
Una matrice su versioni di Node o sistemi operativi ha bisogno del valore della matrice dentro la key, altrimenti ogni leg compete per un’unica voce:
| |
Le voci della cache non si possono sovrascrivere. Togli matrix.node-version dalla key e tutti e tre i leg proveranno a salvare Linux-npm-<hash>: vince il primo che finisce, gli altri registrano un warning che il salvataggio è fallito. Per ~/.npm è quasi innocuo, perché i tarball scaricati non dipendono dalla versione di Node. Smette di essere innocuo quando il path in cache contiene output compilato, come node_modules con un addon nativo. A quel punto il leg Node 24 ripristina binari compilati per Node 20, e ottieni errori che sembrano tutto tranne che un problema di cache. runner.os nella key fa lo stesso lavoro tra i leg di sistemi operativi diversi.
Cosa non mettere in cache in GitHub Actions
Tratta una voce di cache come un layer di un’immagine: chiunque possa eseguire un workflow che la ripristina può leggerla. Tieni token e file .env fuori da qualsiasi directory in cache. Vale la stessa regola di Variabili d’Ambiente e Secrets in Docker: i secrets arrivano da secrets: a runtime, mai da qualcosa scritto su disco e riusato.
Due casi in cui la cache costa piĂą di quanto fa risparmiare:
- Qualsiasi cosa si installi in pochi secondi. Una manciata di pacchetti Python puri, senza estensioni compilate, si installa piĂą o meno alla stessa velocitĂ con cui si ripristina. Lo step di cache ha un suo overhead per comprimere e trasferire l’archivio, e può mangiarsi un risparmio così piccolo.
- Una key troppo generica per fare mai hit. Se fai l’hash dell’intera directory
src/invece del lockfile, ogni modifica al codice invalida la cache. Basa la key sul file che fissa le versioni delle dipendenze e su nient’altro.
Come capire se la cache sta facendo hit
Lo step actions/cache registra Cache restored from key: ... in caso di hit e Cache not found for input keys: ... in caso di miss. Leggi quella riga prima di fidarti del setup.
Per misurarlo, aggiungi cache: 'npm' o lo step manuale a un workflow, fai push due volte e confronta quanto impiega lo step npm ci nell’esecuzione a freddo e in quella a caldo. Quella differenza è quanto vale la modifica. Se un job deve costruire la cache e altri job devono solo leggerla, sostituisci actions/cache con actions/cache/restore@v6 e actions/cache/save@v6, che separano le due metĂ così decidi tu dove far girare ciascuna.