GitHub Actions ile Cache Kullanımı: Build Süreçlerini Hızlandırın
Bir pipeline’ın 15 dakika sürmesi ile 3 dakika sürmesi arasındaki fark, sadece kahve molası değil; developer deneyimi, maliyet ve ekip verimliliği açısından büyük bir uçurum demektir. GitHub Actions’ta cache mekanizmasını doğru kullanmak, bu farkı yaratmanın en kolay ve en etkili yollarından biridir. Bu yazıda sıfırdan başlayıp production ortamlarında kullandığım gerçek cache stratejilerini paylaşacağım.
Cache Neden Bu Kadar Önemli?
Her CI çalışmasında bağımlılıkları sıfırdan indirmek, paket registry’lerine gereksiz yük bindirir, build sürelerini uzatır ve GitHub Actions dakika kotanızı hızla tüketir. Bir Node.js projesinde npm install komutu, node_modules dizinini her seferinde sıfırdan oluşturuyorsa ve bu işlem 4-5 dakika sürüyorsa, günde 20 push yapan bir ekip için sadece bu adım aylık ciddi bir maliyet kalemi haline gelir.
GitHub Actions’ın actions/cache aksiyonu, belirli dosya ve dizinleri workflow çalışmaları arasında önbellekte tutmanıza olanak tanır. Cache anahtarı değişmediği sürece, bir sonraki çalışmada bu veriler doğrudan runner’a yüklenir ve indirilmez.
Cache Mekanizması Nasıl Çalışır?
Cache sistemi iki temel kavram üzerine kuruludur: key ve restore-keys.
- key: Cache’i benzersiz olarak tanımlayan string. Bu key değiştiğinde yeni bir cache oluşturulur.
- restore-keys: Tam eşleşme bulunamazsa kullanılan fallback anahtar listesi. Kısmi eşleşme sağlar.
- path: Cache’lenecek dosya veya dizin yolları.
Cache hit (isabet) olduğunda, belirtilen path içerikleri runner’a geri yüklenir. Cache miss (ıskalama) olduğunda ise job tamamlandıktan sonra bu dizinler cache’e kaydedilir.
Bir cache entry’nin ömrü 7 gündür ve kullanılmazsa otomatik silinir. Depo başına toplam cache limiti ise 10 GB’tır.
Temel Cache Yapısı
En sade haliyle bir cache adımı şöyle görünür:
- name: Cache bağımlılıkları
uses: actions/cache@v3
with:
path: ~/.npm
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-npm-
Burada hashFiles('**/package-lock.json') ifadesi kritik bir rol oynar. package-lock.json değiştiğinde hash değeri de değişir, dolayısıyla yeni bir cache oluşturulur. Dosya değişmediğinde ise aynı key kullanılarak mevcut cache geri yüklenir.
Node.js Projeleri için Cache Stratejisi
Node.js dünyasında iki farklı yaklaşım var: npm cache dizinini cache’lemek ya da doğrudan node_modules dizinini cache’lemek. İkisinin de artıları ve eksileri var.
npm cache dizini cache’leme (önerilen):
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Node.js kurulumu
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Bağımlılıkları yükle
run: npm ci
- name: Build al
run: npm run build
- name: Testleri çalıştır
run: npm test
actions/setup-node@v3 aksiyonunun yerleşik cache desteği var ve cache: 'npm' parametresiyle otomatik olarak ~/.npm dizinini cache’ler. Bu yaklaşım daha temizdir çünkü her zaman npm ci ile temiz bir node_modules kurulumu yapılır, sadece paket indirme adımı atlanır.
node_modules doğrudan cache’leme (hız odaklı):
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: node_modules cache kontrol
id: cache-nodemodules
uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-node-modules-${{ hashFiles('package-lock.json') }}
- name: Bağımlılıkları yükle (cache yoksa)
if: steps.cache-nodemodules.outputs.cache-hit != 'true'
run: npm ci
- name: Build al
run: npm run build
Bu yöntemde cache-hit output değerini kontrol ederek cache varsa npm ci adımını tamamen atlıyoruz. Bu özellikle büyük node_modules dizinlerinde ciddi zaman kazandırır, ancak Node.js sürümü veya işletim sistemi değiştiğinde sorun çıkarabilir. Key’e runner.os eklememizin sebebi tam olarak bu.
Python Projeleri için pip Cache
Python projelerinde pip cache’i yönetmek, özellikle data science veya ML pipeline’larında hayat kurtarır. NumPy, Pandas gibi büyük paketleri her seferinde derlemekten kurtulmak büyük zaman tasarrufu sağlar.
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Python kurulumu
uses: actions/setup-python@v4
with:
python-version: '3.11'
cache: 'pip'
- name: Bağımlılıkları yükle
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Testleri çalıştır
run: pytest tests/ -v
actions/setup-python@v4 da tıpkı setup-node gibi yerleşik cache desteğine sahiptir. Ancak sanal ortam (virtualenv) kullanıyorsanız ayrı bir strateji gerekir:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Python kurulumu
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: pip cache dizinini bul
id: pip-cache
run: echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT
- name: Virtualenv ve pip cache
uses: actions/cache@v3
with:
path: |
${{ steps.pip-cache.outputs.dir }}
.venv
key: ${{ runner.os }}-py311-${{ hashFiles('requirements.txt') }}
restore-keys: |
${{ runner.os }}-py311-
- name: Virtualenv oluştur ve bağımlılıkları yükle
run: |
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
- name: Testleri çalıştır
run: |
source .venv/bin/activate
pytest tests/ -v
Maven ve Gradle için Java Projeleri
Java ekosisteminde bağımlılık boyutları çok büyük olabilir. Spring Boot projeleri için tipik bir Maven repository yüzlerce MB’a ulaşabilir.
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: JDK 17 kurulumu
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
cache: 'maven'
- name: Maven ile build
run: mvn -B package --file pom.xml -DskipTests
- name: Testleri çalıştır
run: mvn test
Gradle için ise hem Gradle wrapper cache’ini hem de bağımlılık cache’ini ayrı ayrı yönetmek daha iyi sonuç verir:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: JDK kurulumu
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Gradle cache
uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Build ve test
run: ./gradlew build test
Docker Build Cache ile Entegrasyon
Docker image build süreleri genellikle pipeline’ın en uzun adımıdır. Layer cache’ini doğru kullanmak dramatik farklar yaratır.
jobs:
docker-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Docker Buildx kurulumu
uses: docker/setup-buildx-action@v2
- name: Docker Hub login
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
- name: Docker image build ve push
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: myapp:latest
cache-from: type=gha
cache-to: type=gha,mode=max
Burada type=gha parametresi, GitHub Actions cache storage’ını Docker build cache deposu olarak kullanır. mode=max ise tüm layer’ları cache’ler. Bu yöntem, özellikle multi-stage build yapılarında ve RUN apt-get install gibi uzun süren adımlarda büyük tasarruf sağlar.
Dockerfile’ınızı da cache dostu yazmak gerekir. Sık değişen katmanları en sona koyun:
# Sık değişmez - cache'den gelir
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
# Sık değişir - her zaman yeniden çalışır
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["node", "dist/index.js"]
Çoklu Ortam ve Matrix Build’lerde Cache
Birden fazla Node.js versiyonu veya işletim sistemi ile test yapıyorsanız, her kombinasyon için ayrı cache key kullanmak önemlidir:
jobs:
test-matrix:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node-version: ['16', '18', '20']
steps:
- uses: actions/checkout@v3
- name: Node.js ${{ matrix.node-version }} kurulumu
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- name: Bağımlılık cache
uses: actions/cache@v3
with:
path: ~/.npm
key: ${{ matrix.os }}-node${{ matrix.node-version }}-${{ hashFiles('package-lock.json') }}
restore-keys: |
${{ matrix.os }}-node${{ matrix.node-version }}-
${{ matrix.os }}-
- name: Bağımlılıkları yükle
run: npm ci
- name: Testleri çalıştır
run: npm test
matrix.os ve matrix.node-version değerlerini key’e dahil etmezsek, Ubuntu’da oluşturulan cache Windows’ta hata verir.
Branch Bazlı Cache Stratejisi
Gerçek dünya senaryolarında feature branch’lerinin main branch cache’inden yararlanması çok işe yarar. Bunu restore-keys ile şöyle kurgulanabilir:
- name: Akıllı cache stratejisi
uses: actions/cache@v3
with:
path: ~/.npm
key: ${{ runner.os }}-npm-${{ github.ref }}-${{ hashFiles('package-lock.json') }}
restore-keys: |
${{ runner.os }}-npm-${{ github.ref }}-
${{ runner.os }}-npm-refs/heads/main-
${{ runner.os }}-npm-
Bu stratejide önce aynı branch + aynı lock file eşleşmesi aranır. Bulunamazsa aynı branch’in eski cache’i denenir. O da yoksa main branch’in cache’i kullanılır. En son çare olarak herhangi bir npm cache’i restore edilir. Bu hiyerarşi sayesinde feature branch’ler nadiren sıfırdan başlar.
Cache İstatistiklerini Takip Etmek
Cache etkinliğini ölçmek için workflow’unuza basit bir izleme adımı ekleyebilirsiniz:
- name: Cache durumu raporla
run: |
echo "Cache hit: ${{ steps.my-cache.outputs.cache-hit }}"
if [ "${{ steps.my-cache.outputs.cache-hit }}" == "true" ]; then
echo "✅ Cache başarıyla yüklendi"
else
echo "⚠️ Cache bulunamadı, bağımlılıklar indirilecek"
fi
GitHub Actions UI’da da her workflow çalışmasının “Post” adımlarında cache kaydetme süresini görebilirsiniz. Ayrıca repository Settings > Actions > Caches menüsünden mevcut cache entry’lerini, boyutlarını ve ne zaman oluşturulduklarını inceleyebilirsiniz.
Sık Yapılan Hatalar ve Çözümleri
Çok geniş restore-keys kullanmak: restore-keys listesi ne kadar genişse, eski ve uyumsuz cache’leri geri yükleme ihtimali o kadar artar. Bu bazen “phantom dependency” sorunlarına yol açar. Key’lerinizi yeterince spesifik tutun.
Lock file olmadan hashFiles kullanmak: hashFiles('package.json') yerine hashFiles('package-lock.json') kullanın. package.json değişmeden bağımlılık versiyonları değişebilir; asıl güvenilir kaynak lock file’dır.
Cache’i çok agresif kullanmak: Her şeyi cache’lemek her zaman iyi değildir. Özellikle güvenlik güncellemeleri içeren bağımlılıklar için periyodik cache temizleme politikası belirleyin. Bunu key’e tarih ekleyerek yapabilirsiniz:
key: ${{ runner.os }}-npm-week${{ steps.date.outputs.week }}-${{ hashFiles('package-lock.json') }}
Windows path sorunları: Windows runner’larında path separator farklıdır. ~ home directory gösterimi çalışmayabilir. %USERPROFILE% yerine $env:USERPROFILE veya doğrudan GitHub Actions context değişkenlerini kullanın.
Performans Kazanımlarını Ölçme
Cache’in gerçekten işe yarayıp yaramadığını anlamak için before/after karşılaştırması yapın. GitHub Actions’ta her job’un başlangıç ve bitiş zamanları loglanır. Birkaç haftalık veri topladıktan sonra şu metriklere bakın:
- Ortalama job süresi (cache hit vs. cache miss)
- Haftalık toplam dakika tüketimi
- Cache hit oranı (başarılı restore / toplam çalışma)
Büyük bir e-ticaret projesinde uyguladığım bir optimizasyonda, sadece npm ve Docker layer cache’ini doğru kurgulamak, ortalama deploy pipeline süresini 18 dakikadan 6 dakikaya düşürmüştü. Bu, günde 30-40 deployment yapan bir ekip için aylık yüzlerce dakika tasarruf anlamına geliyordu.
Gelişmiş: Turborepo ve Monorepo Cache
Monorepo yapılarında Turborepo kullananlar için cache stratejisi daha da önemlidir. Turborepo kendi remote cache mekanizmasına sahiptir ancak GitHub Actions cache ile de entegre çalışabilir:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Node.js kurulumu
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Turborepo cache
uses: actions/cache@v3
with:
path: .turbo
key: ${{ runner.os }}-turbo-${{ github.sha }}
restore-keys: |
${{ runner.os }}-turbo-
- name: npm cache
uses: actions/cache@v3
with:
path: ~/.npm
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-npm-
- name: Bağımlılıkları yükle
run: npm ci
- name: Build (sadece değişen paketler)
run: npx turbo run build --cache-dir=".turbo"
Bu yapıda Turbo, --cache-dir=".turbo" parametresiyle cache dosyalarını bu dizine yazar. Actions cache bu dizini saklar ve sonraki çalışmada geri yükler. Böylece değişmeyen paketler yeniden build edilmez.
Sonuç
GitHub Actions cache mekanizması, doğru kullanıldığında CI/CD pipeline maliyetlerinizi ve sürelerinizi ciddi ölçüde düşüren güçlü bir araçtır. Özetlemek gerekirse:
- Temel kurulum için
actions/setup-node,actions/setup-python,actions/setup-javagibi aksiyonların yerleşik cache özelliklerini kullanın; bunlar çoğu senaryoda yeterlidir. - Özel senaryolar için
actions/cache@v3ile path, key ve restore-keys üçlüsünü iyi kurgulayın. - Docker build için
type=ghacache backend’ini ve Dockerfile layer sırasını optimize edin. - Key stratejisi için hashFiles fonksiyonunu lock file’larla kullanın,
runner.osve ilgili versiyonları her zaman key’e ekleyin. - Branch stratejisi için restore-keys hiyerarşisi ile feature branch’lerin main’den yararlanmasını sağlayın.
- Ölçün ve izleyin; cache hit oranı düşükse key stratejinizi gözden geçirin, 10 GB limitine yaklaşıyorsanız eski veya gereksiz cache’leri temizleyin.
Cache optimizasyonu tek seferlik bir iş değil; proje büyüdükçe, yeni bağımlılıklar eklendikçe ve altyapı değiştikçe güncellenmesi gereken yaşayan bir konfigürasyondur. Ama başlangıç maliyeti düşük, geri dönüşü yüksek bu yatırımı ne kadar erken yaparsanız, ekibinizin o kadar çabuk faydasını göreceğinden emin olabilirsiniz.
