Workflow, który przy każdym push uruchamia npm ci, za każdym razem pobiera z registry te same kilkaset pakietów, nawet gdy lockfile nie zmienił się od tygodnia. W projekcie z prawdziwym drzewem zależności to minuta czy dwie stracone, zanim ruszy pierwszy test, i płacisz to ponownie przy każdym push i każdym pull request. actions/cache przechowuje te pobrania między uruchomieniami i przywraca je, gdy nic istotnego się nie zmieniło.
Jak action decyduje o hit czy miss
actions/cache zapisuje każdy wpis pod wartością key, którą sam wybierasz, ograniczoną do brancha, który go utworzył. Uruchomienie może odczytać cache z własnego brancha i z brancha domyślnego. Przy przywracaniu action porównuje twój key z tym, co jest zapisane:
- Dokładne dopasowanie do
keyto hit. Pliki są pobierane i job idzie dalej. - Brak dokładnego dopasowania przechodzi do
restore-keys, sprawdzanych po kolei, z których każdy to prefix match względem istniejących kluczy. Wygrywa najnowsze dopasowanie. - Brak jakiegokolwiek dopasowania: krok nic nie przywraca. Job startuje na zimno, a jeśli się powiedzie, zapisuje nowy wpis pod
key.
Dlatego standardowy wzorzec haszuje lockfile do klucza:
| |
Zmień jedną linię w package-lock.json, a hash się zmieni, więc dokładny klucz celowo nie trafia. Chcesz cache pasujący do nowego lockfile, nie do tego sprzed tygodnia. Prefix z restore-keys nadal jednak pasuje, więc job startuje od poprzedniego cache zamiast od pustego katalogu, pobiera tylko to, co nowe, i po sukcesie zapisuje świeży wpis z dokładnym dopasowaniem.
Obowiązują dwa limity. Repozytorium ma 10 GB miejsca na cache, współdzielone przez wszystkie jego workflow, a każdy wpis nieużywany przez 7 dni jest usuwany. Nie musisz niczego czyścić ręcznie: gdy repozytorium przekroczy limit, usuwane są najstarsze wpisy, a kolejne uruchomienie, które ich potrzebowało, płaci jednorazowo koszt instalacji na zimno.
Cache npm przez wbudowaną opcję w setup-node
Dla popularnych menedżerów pakietów w ogóle nie piszesz kroku actions/cache. actions/setup-node, actions/setup-python i actions/setup-java przyjmują input cache, który sam zajmuje się kluczem, przywracaniem i zapisem:
| |
cache: 'npm' znajduje package-lock.json (lub npm-shrinkwrap.json) w katalogu głównym repozytorium, liczy jego hash i cache’uje pobrania npm. cache: 'yarn' robi to samo z yarn.lock. W monorepo, gdzie lockfile leży w packages/api/package-lock.json, wskaż go jawnie:
| |
setup-python działa tak samo: cache: 'pip' (albo 'pipenv', 'poetry'), z domyślnym hashem liczonym z requirements.txt. setup-java przyjmuje cache: 'maven' lub cache: 'gradle'. Zacznij od tego. Do ręcznego kroku actions/cache przechodź tylko wtedy, gdy potrzebujesz klucza albo ścieżki, których setup action ci nie da, albo cache’ujesz coś, co nie ma nic wspólnego z runtime języka.
Cache Maven i pip bezpośrednio przez actions/cache
Ręczny krok zawsze ma te same trzy części: katalog, do którego narzędzie zapisuje swój cache, klucz zbudowany z pliku, który ustala wersje zależności, i fallback po prefiksie.
| |
| |
path to cache samego narzędzia, nie twojego projektu: cache pobierania pip, nie twój virtualenv; lokalne repozytorium Maven, nie target/. Możesz też cache’ować wynik builda, jak target/ czy dist/, ale opłaca się to tylko przy buildzie inkrementalnym. W przeciwnym razie kompresujesz i wysyłasz pliki, które kolejne uruchomienie i tak wyrzuci.
Jeśli już cache’ujesz warstwy Dockera przez cache-to: type=gha, jak w Budować i Publikować Obraz Docker z GitHub Actions, te warstwy trafiają do tej samej usługi cache Actions. Liczą się do tych samych 10 GB i tego samego 7-dniowego usuwania co cache zależności. Repozytorium, które robi obie rzeczy, osiąga limit szybciej, niż się spodziewasz, a duże cache warstw mogą wypchnąć cache npm.
Klucze cache w buildach matrix
Matrix po wersjach Node albo systemach operacyjnych potrzebuje wartości matrix w kluczu, inaczej wszystkie trzy legi rywalizują o jeden wpis:
| |
Wpisów cache nie da się nadpisać. Usuń matrix.node-version z klucza, a wszystkie trzy legi spróbują zapisać Linux-npm-<hash>: wygrywa ten, który skończy pierwszy, pozostałe logują warning, że zapis się nie udał. Dla ~/.npm to w większości nieszkodliwe, bo pobrane tarballe nie zależą od wersji Node. Przestaje być nieszkodliwe, gdy ścieżka w cache zawiera skompilowany output, jak node_modules z natywnym addonem. Wtedy leg Node 24 przywraca binaria zbudowane dla Node 20, a ty dostajesz błędy, które wyglądają na wszystko oprócz problemu z cache. runner.os w kluczu robi to samo między legami różnych systemów.
Czego nie cache’ować w GitHub Actions
Traktuj wpis cache jak warstwę obrazu: każdy, kto może uruchomić workflow, który go przywraca, może go odczytać. Trzymaj tokeny i pliki .env z dala od jakiegokolwiek katalogu w cache. Obowiązuje ta sama zasada co w Zmienne Środowiskowe i Secrets w Kontenerach Docker: secrets pochodzą z secrets: w czasie działania, nigdy z czegoś zapisanego na dysku i użytego ponownie.
Dwa przypadki, w których cache’owanie kosztuje więcej, niż oszczędza:
- Wszystko, co instaluje się w kilka sekund. Garść czystych pakietów Python bez skompilowanych rozszerzeń instaluje się mniej więcej tak szybko, jak się przywraca. Krok cache ma własny narzut na kompresję i przesłanie archiwum, który może pochłonąć tak małą oszczędność.
- Klucz zbyt szeroki, by kiedykolwiek trafić. Gdy haszujesz cały katalog
src/zamiast lockfile, każda zmiana kodu unieważnia cache. Oprzyj klucz na pliku, który ustala wersje zależności, i na niczym więcej.
Jak sprawdzić, czy cache trafia
Krok actions/cache loguje Cache restored from key: ... przy hicie i Cache not found for input keys: ... przy missie. Przeczytaj tę linię, zanim zaufasz konfiguracji.
Żeby to zmierzyć, dodaj cache: 'npm' albo ręczny krok do jednego workflow, zrób push dwa razy i porównaj, ile trwa krok npm ci przy zimnym i ciepłym uruchomieniu. Ta różnica to realny zysk, jaki daje cache. Jeśli jeden job ma budować cache, a inne joby mają go tylko czytać, zamień actions/cache na actions/cache/restore@v6 i actions/cache/save@v6, które dzielą te dwie połowy, żebyś sam decydował, gdzie która działa.