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-daysdeğerini artifact türüne göre bilinçli seçin, varsayılan 90 günü kabul etmeyinif: always()koşulunu test raporları ve debug logları için mutlaka kullanın- Matrix build senaryolarında
patternile 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.
