GitHub Actions Workflow Dosyası Yazımı: .github/workflows Klasörü Rehberi
Bir projede CI/CD kurulumu yaparken en çok zaman harcanan yer, pipeline’ın kendisinden çok o pipeline’ı tetikleyen workflow dosyasını doğru yazmak oluyor. GitHub Actions’a geçiş yapan çoğu sysadmin “YAML syntax’ı biliyorum, ne kadar zor olabilir ki” diye başlıyor ama birkaç saat sonra neden job’ların çalışmadığını, neden secret’ların görünmediğini ya da neden her push’ta tüm pipeline’ın baştan çalıştığını anlamaya çalışıyor. Bu rehberde .github/workflows dizinini sıfırdan ele alacağız, gerçek dünya senaryolarıyla birlikte.
Temel Kavramlar ve Dosya Yapısı
GitHub Actions workflow dosyaları YAML formatında yazılır ve .github/workflows/ dizininde saklanır. Bir repo içinde birden fazla workflow dosyası olabilir, her biri bağımsız çalışır. Dosya adının bir önemi yok ama anlamlı isimlendirme hayat kurtarır: deploy-production.yml, run-tests.yml, build-docker.yml gibi.
Temel bir workflow dosyasının iskelet yapısı şöyle görünür:
name: Uygulama Deployment
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Kodu çek
uses: actions/checkout@v4
- name: Basit test
run: echo "Pipeline çalışıyor"
Bu dosyayı .github/workflows/deploy.yml olarak kaydettiğinizde, main branch’e her push veya pull request açıldığında otomatik tetiklenecektir.
Workflow Bileşenleri
Bir workflow dosyasında dört ana bölüm var:
- name: Workflow’un GitHub arayüzünde görünen adı
- on: Tetikleyici olaylar (trigger)
- jobs: Paralel veya sıralı çalışan iş grupları
- steps: Her job içindeki adımlar
Trigger (Tetikleyici) Olaylarını Anlamak
En çok karşılaşılan sorunlardan biri trigger’ları yanlış yapılandırmak. Yanlış bir trigger, ya hiç çalışmayan ya da çok sık çalışan pipeline’lara yol açar.
Push ve Pull Request Trigger’ları
on:
push:
branches:
- main
- 'release/**'
paths:
- 'src/**'
- 'package.json'
paths-ignore:
- '**.md'
- 'docs/**'
pull_request:
branches:
- main
types:
- opened
- synchronize
- reopened
Burada birkaç kritik nokta var. paths filtresini kullanarak sadece belirli dosyalar değiştiğinde pipeline’ı tetikleyebilirsiniz. Örneğin sadece dokümantasyon değiştiyse tüm deployment pipeline’ını çalıştırmak kaynak israfı. paths-ignore ise tam tersi çalışır, belirtilen dosyalar değiştiğinde tetiklemez.
pull_request için types belirtmek de önemli. Varsayılan olarak opened, synchronize ve reopened eventleri dinleniyor ama siz sadece PR merge edildiğinde bir şey çalıştırmak istiyorsanız closed event’ini kullanmanız gerekir ve bunu if koşuluyla birleştirmeniz gerekir.
Zamanlanmış ve Manuel Trigger’lar
on:
schedule:
- cron: '0 2 * * *'
workflow_dispatch:
inputs:
environment:
description: 'Deploy edilecek ortam'
required: true
default: 'staging'
type: choice
options:
- staging
- production
debug_mode:
description: 'Debug modunu etkinleştir'
required: false
type: boolean
default: false
workflow_dispatch production deployment’larında çok işe yarıyor. Manuel tetikleme sırasında parametre almanızı sağlıyor. environment input’unu sonra ${{ github.event.inputs.environment }} şeklinde kullanabilirsiniz.
schedule için standart cron syntax kullanılıyor ama dikkat: GitHub Actions cron’ları UTC bazlı çalışıyor. Türkiye saatiyle gece 2’de çalıştırmak istiyorsanız UTC’ye göre hesaplamanız gerekiyor (yaz saatinde UTC+3, kış saatinde UTC+2).
Jobs ve Runner Yapılandırması
Runner Seçimi
jobs:
test:
runs-on: ubuntu-latest
build-windows:
runs-on: windows-latest
deploy:
runs-on: [self-hosted, linux, production]
GitHub’ın hosted runner’ları (ubuntu-latest, windows-latest, macos-latest) hızlı başlamak için idealdir ama her job için dakika bazlı ücretlendirilirsiniz. Self-hosted runner’lar kendi sunucunuzda çalışır, private network erişimi gerektiren deployment’lar için zorunludur. [self-hosted, linux, production] şeklinde etiketler kullanarak belirli runner’ları hedefleyebilirsiniz.
Job’lar Arası Bağımlılık
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Testleri çalıştır
run: npm test
build:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build al
run: npm run build
deploy-staging:
needs: build
runs-on: ubuntu-latest
steps:
- name: Staging'e deploy et
run: echo "Staging deployment"
deploy-production:
needs: [build, deploy-staging]
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- name: Production'a deploy et
run: echo "Production deployment"
needs ile job bağımlılıklarını tanımlıyorsunuz. deploy-production job’ı hem build hem de deploy-staging tamamlanmadan başlamıyor. Bu, production’a geçmeden önce staging’in başarıyla tamamlanmasını zorunlu kılıyor.
Environment Variables ve Secrets Yönetimi
Bu konu, yeni başlayanların en çok kafasının karıştığı alan. GitHub Actions’da birkaç farklı değişken türü var.
Değişken Hiyerarşisi
env:
NODE_ENV: production
APP_VERSION: '1.0.0'
jobs:
deploy:
runs-on: ubuntu-latest
env:
DATABASE_HOST: db.internal.company.com
steps:
- name: Deploy
env:
API_KEY: ${{ secrets.PRODUCTION_API_KEY }}
DATABASE_PASSWORD: ${{ secrets.DB_PASSWORD }}
run: |
echo "Uygulama versiyonu: $APP_VERSION"
echo "Ortam: $NODE_ENV"
./deploy.sh --host $DATABASE_HOST
Değişken öncelik sırası şöyle çalışır: Step seviyesi > Job seviyesi > Workflow seviyesi. Aynı isimde değişken varsa en yakın scope kazanır.
Secret’lar GitHub repository ayarlarından veya organization ayarlarından tanımlanır. Workflow dosyasına asla plain text secret yazmayın. Secret değerleri log’larda otomatik olarak maskelenir ama buna rağmen secret’ları echo ile yazdırmaktan kaçının.
Context Kullanımı
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Context bilgilerini göster
run: |
echo "Branch: ${{ github.ref_name }}"
echo "Commit SHA: ${{ github.sha }}"
echo "Actor: ${{ github.actor }}"
echo "Event: ${{ github.event_name }}"
echo "Repository: ${{ github.repository }}"
echo "Run ID: ${{ github.run_id }}"
echo "Run Number: ${{ github.run_number }}"
Bu değerler özellikle Docker image tag’lama, deployment versiyonlama ve notification mesajlarında işe yarıyor. github.sha‘nın ilk 8 karakterini image tag olarak kullanmak yaygın bir pratik: ${{ github.sha }} yerine ${GITHUB_SHA::8} şeklinde bash substring kullanabilirsiniz.
Gerçek Dünya Senaryosu: Node.js Uygulaması CI/CD
Bir e-ticaret platformu düşünün. Her PR açıldığında testler çalışacak, main’e merge edildiğinde staging’e, tag push’landığında production’a deploy edilecek.
name: E-Ticaret Platform CI/CD
on:
push:
branches:
- main
tags:
- 'v*'
pull_request:
branches:
- main
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
lint-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Node.js kurulumu
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Bağımlılıkları kur
run: npm ci
- name: Lint çalıştır
run: npm run lint
- name: Unit testleri çalıştır
run: npm test -- --coverage
- name: Coverage raporu yükle
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage/
retention-days: 7
build-and-push:
needs: lint-and-test
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
outputs:
image-tag: ${{ steps.meta.outputs.tags }}
image-digest: ${{ steps.build.outputs.digest }}
steps:
- uses: actions/checkout@v4
- name: Container Registry'e giriş yap
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Image metadata hazırla
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=sha,prefix=sha-
- name: Docker image build et ve push et
id: build
uses: docker/build-push-action@v5
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
Bu workflow’da dikkat etmeniz gereken birkaç detay var. outputs ile job’dan dışarıya değer aktarabilirsiniz, sonraki job’lar bu değerleri kullanabilir. permissions bloğu ile job’un ihtiyaç duyduğu minimum izinleri tanımlamak güvenlik açısından önemli. cache-from: type=gha Docker layer cache’ini GitHub Actions cache mekanizmasıyla entegre ediyor, build sürelerini ciddi ölçüde kısaltıyor.
Matrix Strategy ile Çoklu Ortam Testleri
jobs:
test-matrix:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os:
- ubuntu-latest
- windows-latest
node-version:
- 18
- 20
- 22
exclude:
- os: windows-latest
node-version: 18
fail-fast: false
steps:
- uses: actions/checkout@v4
- name: Node.js ${{ matrix.node-version }} kurulumu
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Testleri çalıştır
run: npm test
Matrix strategy, her kombinasyon için ayrı bir job oluşturur. Yukarıdaki örnekte ubuntu-latest + Node 18, ubuntu-latest + Node 20, ubuntu-latest + Node 22, windows-latest + Node 20, windows-latest + Node 22 kombinasyonları çalışacak. exclude ile windows-latest + Node 18 kombinasyonu dışlandı. fail-fast: false ise bir kombinasyon başarısız olsa bile diğerlerinin çalışmaya devam etmesini sağlıyor.
Reusable Workflows ve Composite Actions
Büyük organizasyonlarda aynı workflow adımlarını defalarca yazmak yerine paylaşılabilir yapılar oluşturmak şart.
Reusable Workflow
.github/workflows/deploy-shared.yml dosyası:
name: Shared Deploy Workflow
on:
workflow_call:
inputs:
environment:
required: true
type: string
image-tag:
required: true
type: string
secrets:
KUBE_CONFIG:
required: true
SLACK_WEBHOOK:
required: true
jobs:
deploy:
runs-on: ubuntu-latest
environment: ${{ inputs.environment }}
steps:
- name: Kubernetes config ayarla
run: |
mkdir -p ~/.kube
echo "${{ secrets.KUBE_CONFIG }}" | base64 -d > ~/.kube/config
- name: Deployment güncelle
run: |
kubectl set image deployment/app
app=${{ inputs.image-tag }}
-n ${{ inputs.environment }}
kubectl rollout status deployment/app
-n ${{ inputs.environment }}
--timeout=5m
- name: Slack bildirimi gönder
if: always()
run: |
STATUS="${{ job.status }}"
curl -X POST ${{ secrets.SLACK_WEBHOOK }}
-H 'Content-type: application/json'
-d "{"text":"Deploy to ${{ inputs.environment }}: ${STATUS}"}"
Bu reusable workflow’u başka bir workflow’dan şöyle çağırırsınız:
jobs:
deploy-to-staging:
uses: ./.github/workflows/deploy-shared.yml
with:
environment: staging
image-tag: ghcr.io/myorg/myapp:sha-abc123
secrets:
KUBE_CONFIG: ${{ secrets.STAGING_KUBE_CONFIG }}
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
Conditional Steps ve Environment Protection
jobs:
deploy:
runs-on: ubuntu-latest
environment:
name: production
url: https://app.company.com
steps:
- name: Sadece main branch'te çalış
if: github.ref == 'refs/heads/main'
run: echo "Main branch"
- name: Tag push'ta çalış
if: startsWith(github.ref, 'refs/tags/v')
run: echo "Version tag tespit edildi"
- name: Başarısız olursa rollback yap
if: failure()
run: |
kubectl rollout undo deployment/app -n production
echo "Rollback tamamlandı"
- name: Her durumda temizlik yap
if: always()
run: rm -f /tmp/deploy-config.yml
environment bloğu GitHub’daki environment korumalarını devreye sokar. Production environment’ını GitHub ayarlarından Required reviewers ile koruyabilirsiniz; workflow o noktaya geldiğinde onay bekler. environment.url ise deployment’ı GitHub arayüzünde tıklanabilir bir link olarak gösterir.
if koşulları için kullanabileceğiniz kontekst fonksiyonları:
- startsWith(github.ref, ‘refs/tags/’): Ref’in belirli bir değerle başlayıp başlamadığını kontrol eder
- contains(github.event.head_commit.message, ‘[skip ci]’): Commit mesajında belirli bir metin var mı diye bakar
- failure(): Önceki adımlardan herhangi biri başarısız olduysa true döner
- success(): Tüm önceki adımlar başarılıysa true döner
- always(): Her koşulda çalışır, workflow iptal edilse bile
- cancelled(): Workflow manuel iptal edildiğinde true döner
Artifact Yönetimi ve Cache Stratejileri
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Dependency cache
uses: actions/cache@v4
with:
path: |
~/.npm
node_modules
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Build
run: npm run build
- name: Build artifact yükle
uses: actions/upload-artifact@v4
with:
name: production-build-${{ github.run_number }}
path: dist/
retention-days: 30
if-no-files-found: error
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- name: Build artifact indir
uses: actions/download-artifact@v4
with:
name: production-build-${{ github.run_number }}
path: dist/
- name: Sunucuya yükle
run: |
rsync -avz --delete dist/
${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}:/var/www/app/
Cache stratejisi workflow sürelerini büyük ölçüde etkiliyor. hashFiles('**/package-lock.json') ile lock dosyası değiştiğinde cache otomatik olarak geçersiz hale geliyor. restore-keys ise tam eşleşme bulunamadığında daha eski cache’i kullanmayı deniyor. Artifact’lar ise job’lar arası dosya paylaşımı için kullanılıyor ve ayrıca workflow run geçmişinde indirilebilir kalıyor.
Sorun Giderme İpuçları
Workflow dosyalarında sıkça karşılaşılan sorunlar ve çözümleri:
- YAML indentation hatası: YAML’da tab değil space kullanın. 2 space standart. VS Code için YAML extension kurun, syntax highlighting hayat kurtarır.
- Secret değeri görünmüyor: Secret’lar fork’lanmış repolardan gelen PR’larda default olarak erişilemez. Bu bir güvenlik önlemi.
- Workflow tetiklenmiyor:
onbloğunu kontrol edin.push: branches: mainyazmak yerinepush: branches: [main]veyapush: branches:n - mainformatını kullanın.
- Self-hosted runner offline görünüyor: Runner servisinin çalıştığını kontrol edin:
sudo systemctl status actions.runner.*
- Job çok uzun sürüyor: Default timeout 6 saat.
timeout-minutes: 30ile job seviyesinde veya step seviyesinde kısıtlayabilirsiniz.
- Concurrent run sorunu: Aynı anda birden fazla deployment’ın çakışmasını önlemek için
concurrencykullanın:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
Bu ayar, aynı branch’e arka arkaya push geldiğinde önceki çalışan workflow’u iptal eder ve sadece sonuncuyu çalıştırır. Özellikle hızlı geliştirme döngülerinde runner kaynaklarını verimli kullanmayı sağlar.
Sonuç
.github/workflows dizinini doğru yapılandırmak, CI/CD pipeline’ınızın güvenilirliğini ve sürdürülebilirliğini doğrudan etkiliyor. Trigger’ları gereksiz yere geniş tutmak kaynak israfına, çok dar tutmak ise bazı değişikliklerin kontrol edilmeden geçmesine neden oluyor.
Pratik bir yaklaşım olarak küçük başlayın: Önce basit bir lint ve test workflow’u oluşturun, çalıştığını gördükten sonra Docker build, artifact yönetimi ve deployment adımlarını ekleyin. Reusable workflow’ları erken aşamada tasarlamak, organizasyon genelinde tutarlılık sağlıyor ve yeni projeler için onboarding süresini ciddi ölçüde kısaltıyor.
Secret yönetimi ve environment korumalarını başından itibaren doğru kurmak, sonradan güvenlik açığı kapatmak için saatlerce uğraşmaktan çok daha kolay. GitHub Environments üzerindeki Required reviewers ve Wait timer özellikleri, production deployment’larına insan gözü katmanı eklemek için en pratik yol.
Son olarak, workflow dosyalarınızı da kod gibi değerlendirin: Pull request üzerinden review’a açın, değişiklik gerekçelerini commit mesajlarına yazın ve önemli değişiklikleri changelog’unuzda belgeleyin. Pipeline’ınız ne kadar şeffaf olursa, ekibiniz o kadar güvenle kullanır.
