GitHub Actions ile Artifact Yönetimi: Build Çıktılarını Saklama ve Paylaşma

Bir CI/CD pipeline’ı kurduğunuzda, işin en can sıkıcı kısmı genellikle build çıktılarını yönetmek oluyor. “Build geçti, artifact nerede?” sorusu her ekipte en az bir kez sorulmuştur. GitHub Actions’ın artifact sistemi bu sorunu çözüyor, ama doğru kullanmazsanız depolama maliyetleri tavan yapar ya da pipeline’lar arasında dosya paylaşımı kabus haline gelir. Bu yazıda artifact yönetimini başından sonuna kadar ele alacağız.

Artifact Nedir ve Neden Önemlidir

Artifact, build süreci sonucunda ortaya çıkan her türlü çıktı dosyasıdır. Java için JAR/WAR dosyaları, Node.js için webpack bundle’ları, Docker image’ları, test raporları, log dosyaları ya da bir binary executable olabilir. GitHub Actions context’inde artifact, bir workflow’un job’ları arasında ya da farklı workflow’lar arasında paylaşmak istediğiniz dosyaları ifade eder.

Neden bu kadar önemli? Şöyle düşünün: Bir monorepo’nuz var, frontend build job’u çalışıyor, backend build job’u çalışıyor ve deployment job’u her ikisinin çıktısını birleştirip production’a deploy etmesi gerekiyor. Bu senaryoda artifact sistemi olmadan job’lar arasında dosya geçirmek mümkün değil çünkü her job farklı bir runner üzerinde çalışıyor.

GitHub Actions’da artifact yönetimi için iki temel action var:

  • actions/upload-artifact: Dosyaları GitHub’ın depolama alanına yükler
  • actions/download-artifact: Yüklenmiş dosyaları indirir

Bu ikili kombinasyon basit görünüyor, ama altında ciddi nüanslar var.

Temel Upload ve Download Kullanımı

En basit senaryoyla başlayalım. Bir Node.js uygulaması build edip artifact olarak kaydediyoruz:

name: Build ve Artifact Kaydet

on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Kodu Checkout Et
        uses: actions/checkout@v4

      - name: Node.js Kur
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: Bağımlılıkları Yükle
        run: npm ci

      - name: Build Al
        run: npm run build

      - name: Build Artifact'ı Yükle
        uses: actions/upload-artifact@v4
        with:
          name: frontend-build
          path: dist/
          retention-days: 7

Burada retention-days parametresi önemli. Varsayılan değer 90 gün, ama production deploy’ları için genellikle 7-30 gün yeterli. Depolama maliyetlerini azaltmak için bu değeri bilinçli seçin.

Şimdi bu artifact’ı başka bir job’da kullanalım:

  deploy:
    runs-on: ubuntu-latest
    needs: build
    steps:
      - name: Build Artifact'ı İndir
        uses: actions/download-artifact@v4
        with:
          name: frontend-build
          path: ./dist

      - name: Dosyaları Kontrol Et
        run: ls -la ./dist

      - name: Deploy Et
        run: |
          echo "Deploy ediliyor..."
          # Deployment komutlarınız buraya
          rsync -avz ./dist/ user@server:/var/www/html/

needs: build satırı kritik. Bu olmadan deploy job’u build tamamlanmadan başlayabilir ve artifact henüz yüklenmemiş olur.

Çoklu Artifact ve Matrix Build Senaryoları

Gerçek dünya projelerinde genellikle tek bir artifact yetmiyor. Bir örnek verelim: Hem Linux hem macOS hem de Windows binary’si üretmemiz gerekiyor.

name: Cross-Platform Build

on:
  push:
    tags:
      - 'v*'

jobs:
  build:
    strategy:
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
        include:
          - os: ubuntu-latest
            artifact_name: myapp-linux
            binary_name: myapp
          - os: macos-latest
            artifact_name: myapp-macos
            binary_name: myapp
          - os: windows-latest
            artifact_name: myapp-windows
            binary_name: myapp.exe

    runs-on: ${{ matrix.os }}

    steps:
      - uses: actions/checkout@v4

      - name: Go Kur
        uses: actions/setup-go@v5
        with:
          go-version: '1.22'

      - name: Build
        run: go build -o ${{ matrix.binary_name }} .

      - name: Artifact Yükle
        uses: actions/upload-artifact@v4
        with:
          name: ${{ matrix.artifact_name }}
          path: ${{ matrix.binary_name }}
          retention-days: 30

  release:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - name: Tüm Artifact'ları İndir
        uses: actions/download-artifact@v4
        with:
          pattern: myapp-*
          merge-multiple: false
          path: ./release-files

      - name: İndirilen Dosyaları Listele
        run: find ./release-files -type f

      - name: GitHub Release Oluştur
        uses: softprops/action-gh-release@v2
        with:
          files: |
            ./release-files/myapp-linux/myapp
            ./release-files/myapp-macos/myapp
            ./release-files/myapp-windows/myapp.exe

pattern parametresi v4’te geldi ve wildcard ile birden fazla artifact’ı tek seferde indirmenizi sağlıyor. merge-multiple: false ayarıyla her artifact kendi alt dizinine iniyor, bu da dosya çakışmalarını önlüyor.

Test Raporlarını Artifact Olarak Saklama

Test sonuçlarını artifact olarak saklamak, debug süreçlerinde hayat kurtarır. Özellikle CI’da geçen ama lokal’de çalışmayan testlerde log’lara bakmak yerine tam test raporuna erişebilmek çok değerli.

  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Python Kur
        uses: actions/setup-python@v5
        with:
          python-version: '3.12'

      - name: Bağımlılıkları Yükle
        run: pip install -r requirements.txt pytest pytest-html coverage

      - name: Testleri Çalıştır
        run: |
          coverage run -m pytest tests/ 
            --html=reports/test-report.html 
            --self-contained-html 
            -v 
            --junit-xml=reports/junit.xml
        continue-on-error: true

      - name: Coverage Raporu Oluştur
        run: |
          coverage html -d reports/coverage
          coverage xml -o reports/coverage.xml

      - name: Test Raporlarını Yükle
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: test-reports-${{ github.run_number }}
          path: reports/
          retention-days: 14

      - name: JUnit Sonuçlarını Publish Et
        uses: dorny/test-reporter@v1
        if: always()
        with:
          name: Pytest Sonuçları
          path: reports/junit.xml
          reporter: java-junit

Buradaki iki önemli detay var. Birincisi continue-on-error: true testlerin başarısız olsa bile raporların artifact olarak yüklenmesini sağlıyor. İkincisi if: always() koşulu job başarısız olsa dahi artifact yükleme adımının çalışmasını garantiliyor. Bu olmadan testler fail olduğunda raporlara hiç ulaşamazsınız.

${{ github.run_number }} kullanımı da önemli bir pratik. Her run için unique bir artifact adı oluşturuyor, böylece eski raporların üzerine yazılmıyor.

Artifact Boyutlarını Optimize Etme

En sık karşılaştığım problemlerden biri artifact boyutlarının kontrolden çıkması. 500 MB’lık node_modules içeren bir artifact yüklemek hem zaman hem para israfı. İşte boyut optimizasyonu için pratik bir yaklaşım:

      - name: Build
        run: npm run build

      - name: Gereksiz Dosyaları Temizle
        run: |
          # Source map'leri production build'da genellikle istemezsiniz
          find dist/ -name "*.map" -delete
          
          # Test dosyalarını temizle
          find dist/ -name "*.test.*" -delete
          
          # Boyutu kontrol et
          du -sh dist/

      - name: Build Artifact'ı Sıkıştırarak Yükle
        uses: actions/upload-artifact@v4
        with:
          name: frontend-build-${{ github.sha }}
          path: dist/
          compression-level: 9
          retention-days: 5

compression-level parametresi 0 ile 9 arasında değer alıyor. 9 en yüksek sıkıştırma oranını veriyor ama daha fazla CPU kullanıyor. Genellikle 6 iyi bir denge noktası.

Büyük binary dosyalar için özel bir yaklaşım gerekebilir:

      - name: Docker Image'ı Artifact Olarak Kaydet
        run: |
          docker build -t myapp:${{ github.sha }} .
          docker save myapp:${{ github.sha }} | gzip > myapp-image.tar.gz
          ls -lh myapp-image.tar.gz

      - name: Docker Image Artifact Yükle
        uses: actions/upload-artifact@v4
        with:
          name: docker-image-${{ github.sha }}
          path: myapp-image.tar.gz
          retention-days: 3
          compression-level: 1

Docker image’ları zaten sıkıştırılmış olduğu için compression-level: 1 koyun, 9 koyarsanız hem zaman harcar hem de dosya boyutunda pek fark olmaz.

Workflow’lar Arası Artifact Paylaşımı

Aynı workflow içindeki job’lar arasında artifact paylaşımı basit. Ama farklı workflow’lar arasında paylaşım biraz daha karmaşık. Bunu workflow_run event’iyle yapabilirsiniz:

name: Deploy Workflow

on:
  workflow_run:
    workflows: ["Build Workflow"]
    types:
      - completed
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    if: ${{ github.event.workflow_run.conclusion == 'success' }}
    steps:
      - name: Build Workflow Artifact'larını İndir
        uses: actions/github-script@v7
        with:
          script: |
            const artifacts = await github.rest.actions.listWorkflowRunArtifacts({
              owner: context.repo.owner,
              repo: context.repo.repo,
              run_id: ${{ github.event.workflow_run.id }},
            });
            
            const matchArtifact = artifacts.data.artifacts.filter((artifact) => {
              return artifact.name == "frontend-build"
            })[0];
            
            const download = await github.rest.actions.downloadArtifact({
              owner: context.repo.owner,
              repo: context.repo.repo,
              artifact_id: matchArtifact.id,
              archive_format: 'zip',
            });
            
            const fs = require('fs');
            fs.writeFileSync('frontend-build.zip', Buffer.from(download.data));

      - name: Artifact'ı Aç
        run: unzip frontend-build.zip -d ./dist

      - name: Deploy Et
        run: |
          echo "Deployment başlıyor..."
          # deployment komutları

Bu yaklaşım biraz verbose ama farklı workflow’lar arasında güvenli artifact transferi için şu an en sağlam yöntem.

Cache vs Artifact: Doğru Seçim

Çok sık karıştırılan bir konu bu. Cache ve artifact farklı amaçlara hizmet eder:

Cache kullanın:

  • node_modules, pip paketleri, Maven dependencies gibi tekrar tekrar indirilen şeyler için
  • Build sürelerini kısaltmak için
  • Workflow run’lar arasında tekrar oluşturulabilecek şeyler için

Artifact kullanın:

  • Job’lar arasında geçmesi gereken build çıktıları için
  • Test raporları, log dosyaları gibi saklamak istediğiniz şeyler için
  • Release dosyaları gibi indirmeye sunmak istediğiniz şeyler için

Pratikte ikisini birlikte kullanmak en verimli yaklaşım:

  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Node.js Kur
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'          # Bu cache, bağımlılıkları cache'liyor

      - name: Bağımlılıkları Yükle
        run: npm ci              # Cache sayesinde bu hızlı çalışır

      - name: Build
        run: npm run build

      - name: Build Çıktısını Artifact Olarak Kaydet
        uses: actions/upload-artifact@v4    # Bu artifact, build çıktısını saklar
        with:
          name: production-build
          path: dist/

Koşullu Artifact Yükleme

Her zaman her durumda artifact yüklemek istemeyebilirsiniz. Örneğin sadece main branch’e push’larda veya sadece başarısız build’lerde artifact kaydetmek:

      - name: Production Artifact Yükle
        uses: actions/upload-artifact@v4
        if: github.ref == 'refs/heads/main' && success()
        with:
          name: production-build-${{ github.run_number }}
          path: dist/
          retention-days: 30

      - name: Debug Artifact Yükle (Sadece Başarısızlıkta)
        uses: actions/upload-artifact@v4
        if: failure()
        with:
          name: debug-logs-${{ github.run_number }}
          path: |
            logs/
            *.log
            build-error.txt
          retention-days: 7

Bu pattern özellikle production ortamlarında çok işe yarıyor. Başarılı build’lerin artifact’larını uzun süre saklarken, debug log’larını kısa süre saklıyorsunuz.

Artifact Boyut Limitleri ve Depolama Yönetimi

GitHub’ın artifact limitleri şöyle:

  • Tek artifact boyutu: 10 GB
  • Toplam depolama: Plan’a göre değişiyor (Free: 500 MB, Pro: 2 GB, Teams: 2 GB, Enterprise: 50 GB)
  • Varsayılan retention süresi: 90 gün (değiştirilebilir)

Bu limitleri aşmamak için birkaç strateji:

      - name: Artifact Boyutunu Kontrol Et
        run: |
          BUILD_SIZE=$(du -sb dist/ | cut -f1)
          MAX_SIZE=$((100 * 1024 * 1024))  # 100 MB
          
          if [ $BUILD_SIZE -gt $MAX_SIZE ]; then
            echo "UYARI: Build boyutu 100 MB'yi aşıyor: $(du -sh dist/)"
            exit 1
          fi
          
          echo "Build boyutu: $(du -sh dist/)"

      - name: Seçici Artifact Yükleme
        uses: actions/upload-artifact@v4
        with:
          name: production-build
          path: |
            dist/
            !dist/**/*.map
            !dist/**/*.test.*
            !dist/assets/fonts/
          retention-days: 7

! ile başlayan path’ler exclude pattern. Yani dist/ içindeki her şeyi al ama .map dosyalarını, test dosyalarını ve font dosyalarını alma demek.

Eski Artifact’ları Temizleme

Depolama alanını yönetmek için scheduled bir cleanup workflow yazmak iyi bir pratik:

name: Artifact Temizleme

on:
  schedule:
    - cron: '0 2 * * 0'  # Her Pazar saat 02:00'de
  workflow_dispatch:

jobs:
  cleanup:
    runs-on: ubuntu-latest
    steps:
      - name: Eski Artifact'ları Temizle
        uses: actions/github-script@v7
        with:
          script: |
            const days = 14;
            const cutoffDate = new Date();
            cutoffDate.setDate(cutoffDate.getDate() - days);
            
            const artifacts = await github.paginate(
              github.rest.actions.listArtifactsForRepo,
              {
                owner: context.repo.owner,
                repo: context.repo.repo,
                per_page: 100,
              }
            );
            
            let deletedCount = 0;
            
            for (const artifact of artifacts) {
              const createdAt = new Date(artifact.created_at);
              
              // Test raporları ve debug logları için erken temizleme
              if (
                (artifact.name.includes('test-reports') || 
                 artifact.name.includes('debug-logs')) &&
                createdAt < cutoffDate
              ) {
                console.log(`Siliniyor: ${artifact.name} (${artifact.created_at})`);
                await github.rest.actions.deleteArtifact({
                  owner: context.repo.owner,
                  repo: context.repo.repo,
                  artifact_id: artifact.id,
                });
                deletedCount++;
              }
            }
            
            console.log(`Toplam ${deletedCount} artifact silindi.`);

Bu script’i repository’nizde çalıştırmak için GITHUB_TOKEN’ın yeterli yetkiye sahip olması gerekiyor. Repository settings’den Actions permissions’ı kontrol edin.

Gerçek Dünya Senaryosu: Monorepo Deployment Pipeline

Her şeyi bir araya getirelim. Bir monorepo’da frontend ve backend ayrı build edilip tek bir deployment job’unda birleştirilen bir senaryo:

name: Monorepo CI/CD Pipeline

on:
  push:
    branches: [main]

jobs:
  build-frontend:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
          cache-dependency-path: frontend/package-lock.json

      - run: cd frontend && npm ci
      - run: cd frontend && npm run build

      - uses: actions/upload-artifact@v4
        with:
          name: frontend-dist
          path: frontend/dist/
          retention-days: 1

  build-backend:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with:
          java-version: '21'
          distribution: 'temurin'
          cache: 'maven'

      - run: cd backend && mvn package -DskipTests
      - run: cd backend && mvn test

      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: backend-test-reports
          path: backend/target/surefire-reports/
          retention-days: 7

      - uses: actions/upload-artifact@v4
        with:
          name: backend-jar
          path: backend/target/*.jar
          retention-days: 1

  deploy:
    needs: [build-frontend, build-backend]
    runs-on: ubuntu-latest
    environment: production
    steps:
      - name: Frontend Build İndir
        uses: actions/download-artifact@v4
        with:
          name: frontend-dist
          path: ./deploy/frontend

      - name: Backend JAR İndir
        uses: actions/download-artifact@v4
        with:
          name: backend-jar
          path: ./deploy/backend

      - name: Deploy Dosyalarını Doğrula
        run: |
          echo "Frontend dosyaları:"
          ls -la ./deploy/frontend/
          echo "Backend dosyaları:"
          ls -la ./deploy/backend/

      - name: Production'a Deploy Et
        env:
          DEPLOY_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
          DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
        run: |
          eval $(ssh-agent -s)
          echo "$DEPLOY_KEY" | ssh-add -
          
          rsync -avz --delete 
            ./deploy/frontend/ 
            deploy@$DEPLOY_HOST:/var/www/frontend/
          
          scp ./deploy/backend/*.jar 
            deploy@$DEPLOY_HOST:/opt/backend/app.jar
          
          ssh deploy@$DEPLOY_HOST 
            "sudo systemctl restart backend-service"

Bu pipeline’da retention-days’i kasıtlı olarak 1 gün tutuyoruz çünkü deployment artifact’ları sadece o run için gerekli. Test raporlarını ise 7 gün saklıyoruz çünkü debug için geri dönebilirsiniz.

Sonuç

GitHub Actions artifact sistemi, doğru kullanıldığında CI/CD pipeline’larınızı hem güvenilir hem de verimli hale getiriyor. Özetlemek gerekirse:

  • Artifact vs cache ayrımını net tutun, karıştırmayın
  • retention-days değerini artifact türüne göre bilinçli seçin, varsayılan 90 günü kabul etmeyin
  • if: always() koşulunu test raporları ve debug logları için mutlaka kullanın
  • Matrix build senaryolarında pattern ile wildcard kullanımını öğrenin
  • Artifact boyutlarını exclude pattern’leriyle kontrol altında tutun
  • Depolama alanını yönetmek için scheduled cleanup workflow’u kurun

En çok işe yarayan pratik ise artifact adlarına ${{ github.run_number }} veya ${{ github.sha }} eklemek. Hangi artifact’ın hangi commit’ten geldiğini anında görebiliyorsunuz ve eski artifact’ların üzerine yanlışlıkla yazılması engellenmiş oluyor. Pipeline’ınızda bu yaklaşımları uyguladığınızda hem ekip içi debug süreçleri hızlanacak hem de depolama maliyetleriniz kontrol altına girecek.

Bir yanıt yazın

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