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-java gibi aksiyonların yerleşik cache özelliklerini kullanın; bunlar çoğu senaryoda yeterlidir.
  • Özel senaryolar için actions/cache@v3 ile path, key ve restore-keys üçlüsünü iyi kurgulayın.
  • Docker build için type=gha cache backend’ini ve Dockerfile layer sırasını optimize edin.
  • Key stratejisi için hashFiles fonksiyonunu lock file’larla kullanın, runner.os ve 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.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir