CI süresinin çoğu değişmeyen bağımlılığı yeniden kurmakla geçiyor. Cache anahtarını lockfile’ın hash’ine bağlarsanız değişmeyen kısım tekrar kurulmuyor, build dakikalar yerine saniyelere iniyor.
pnpm cache’i ve kurulum sırası
actions/setup-nodein cache: pnpm seçeneği pnpm store’unu cache’liyor, ama bunun çalışması için pnpm’in setup-nodedan önce kurulu olması gerekiyor:
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
Sırayı tersine çevirirseniz setup-node pnpm komutunu bulamaz, store yolunu tespit edemez. Hata vermez, cache adımı sessizce atlanır ve her build sıfırdan kurulum yapar.
actions/cache ve doğru anahtar
pnpm dışındaki her şey için actions/cache kullanılır. Anahtarın kalbi lockfile’ın hash’i:
- uses: actions/cache@v4
with:
path: ~/.cache/some-tool
key: ${{ runner.os }}-some-tool-${{ hashFiles('**/pnpm-lock.yaml') }}
Sabit bir string yazarsanız (“some-tool-cache” gibi) cache hiç güncellenmiyor, bağımlılıklar değişse bile eski cache geri yükleniyor. Anahtar lockfile değiştiğinde değişmeli, o yüzden hashFiles şart.
restore-keys ile kısmi eşleşme
Tam eşleşme yoksa restore-keys bir önceki cache’i kısmi anahtarla buluyor:
- uses: actions/cache@v4
with:
path: ~/.cache/some-tool
key: ${{ runner.os }}-some-tool-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-some-tool-
Lockfile’da tek bağımlılık değişse bile tam anahtar tutmuyor. restore-keys en son uyan cache’i indiriyor, kurulum sıfırdan değil farktan başlıyor. restore-keys altındaki her satır tam anahtarın bir öneki olmalı, rastgele bir string eşleşmiyor.
Kısmi eşleşmede paket yöneticisi eksik kalan farkı kendisi tamamlıyor. pnpm install eski store’u bulur, lockfile’daki yeni paketleri üstüne ekler. Bu, sıfırdan indirmekten çok daha hızlı, tam eşleşme kadar hızlı değil.
cache-hit ile install adımını atlayın
Tam eşleşme varsa kurulum komutunu hiç çalıştırmamak mümkün:
- id: cache
uses: actions/cache@v4
with:
path: ~/.cache/some-tool
key: ${{ runner.os }}-some-tool-${{ hashFiles('**/pnpm-lock.yaml') }}
- if: steps.cache.outputs.cache-hit != 'true'
run: pnpm install --frozen-lockfile
cache-hit çıktısı sadece tam anahtar eşleştiğinde true dönüyor, restore-keys üzerinden gelen kısmi eşleşmede false kalıyor. pnpm’in kendi cache mantığı zaten hızlı olduğu için bu adım genelde node_modules dışı, daha pahalı kurulumlarda (Composer vendor, derlenen native bağımlılık) fark yaratıyor.
Matrix build’te birden fazla Node sürümü ya da işletim sistemi test ediyorsanız her kombinasyon kendi cache’ini almalı. runner.os anahtarda zaten var, matrix’te Node sürümü de değişiyorsa onu da anahtara ekleyin:
key: ${{ runner.os }}-node${{ matrix.node-version }}-${{ hashFiles('**/pnpm-lock.yaml') }}
Aksi hâlde Node 20 ile kurulan cache Node 22’nin işine yaramaz, restore edilir ama sonra baştan kurulur, sadece zaman kaybedilir.
Docker katmanlarını da cache’leyin
Image build’i de katman katman cache’lenebilir. buildx ile GitHub Actions cache backend’i:
- uses: docker/setup-buildx-action@v3
- uses: docker/build-push-action@v6
with:
context: .
push: true
cache-from: type=gha
cache-to: type=gha,mode=max
mode=max ara katmanları da cache’e yazıyor, sadece son katmanı değil. Bunsuz COPY gibi bir adım cache’i bozduğunda öncesindeki katmanlar da tekrar kurulur. İmajın kendisi neden büyüdüğü ayrı bir konu; Docker imajının 1,2 GB’a çıkma sebebi genelde tek aşamalı Dockerfile’dır.
Değişmeyen anahtar cache değil, bayat dizindir
Anahtarı hiç değişmeyen bir şeye bağlarsanız (sabit string, dal adı, tarih yok) o artık cache değil, bayatlamış bir dizindir. Her build aynı eski bağımlılıkları geri yükler, yeni bir paket eklediğinizde bile fark etmez. Anahtar bağımlılık listesinin kendisinden türetilmeli, başka hiçbir şeyden değil.
Özet
pnpm’i setup-nodedan önce kurun. Cache anahtarını lockfile hash’inden türetin, sabit string kullanmayın. restore-keys ekleyin. Docker build’i cache-from/cache-to: type=gha ile katman katman cache’leyin.