GitHub Actions ile CI/CD Pipeline Kurulumu
Bir projenin her commit’inde “acaba testler geçti mi?” diye elle kontrol etmek, bir noktadan sonra dayanılmaz hale geliyor. Özellikle ekip büyüdükçe, bu manuel süreç hem zaman kaybı hem de hata kaynağı oluyor. GitHub Actions tam da bu noktada hayatı kurtarıyor: repository’nin içine birkaç YAML dosyası koyuyorsun, gerisini GitHub hallediyor. Bu yazıda, sıfırdan gerçek bir CI/CD pipeline kuracağız; hem teorik hem pratik, hem de üretimde karşılaştığım bazı tuzakları paylaşacağım.
GitHub Actions’ın Temel Mantığı
GitHub Actions, olay tabanlı bir otomasyon sistemi. Bir event tetikleniyor (push, pull request, schedule…), bu event bir workflow’u başlatıyor, workflow içindeki job’lar runner makinelerde çalışıyor. Bu kadar basit ama ayrıntılar oldukça derin.
Birkaç kavramı net anlamamız gerekiyor:
- Workflow:
.github/workflows/dizinindeki YAML dosyaları. Her dosya bir workflow. - Event: Workflow’u tetikleyen olay.
push,pull_request,workflow_dispatchgibi. - Job: Workflow içindeki bağımsız çalışma birimi. Aynı anda paralel çalışabilirler.
- Step: Job içindeki her bir komut veya action.
- Action: Yeniden kullanılabilir step paketi. GitHub Marketplace’ten hazır alınabilir ya da kendin yazabilirsin.
- Runner: Job’ların çalıştığı makine. GitHub’ın ücretsiz sunduğu
ubuntu-latest,windows-latest,macos-latestvar; ya da self-hosted runner kurabilirsin.
Dosya yapısına bakalım:
proje/
├── .github/
│ └── workflows/
│ ├── ci.yml
│ └── deploy.yml
├── src/
└── ...
İlk Workflow Dosyası: Basit Bir CI
Önce çok basit bir örnekle başlayalım. Node.js projesi için test çalıştıran bir workflow:
name: CI Pipeline
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test:
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: Testleri çalıştır
run: npm test
- name: Build al
run: npm run build
Bu dosyayı .github/workflows/ci.yml olarak kaydettiğinde, main veya develop branch’ine her push’ta ve main‘e açılan her PR’da otomatik çalışır.
Birkaç önemli nokta: npm install değil npm ci kullandım. ci komutu, package-lock.json‘ı baz alır ve tutarlı kurulum yapar. CI ortamlarında her zaman npm ci tercih edilmeli.
Matrix Strategy ile Çoklu Ortam Testi
Gerçek dünyada “bizim uygulama 3 farklı Node.js sürümünde çalışıyor, hepsinde test etmemiz lazım” gibi durumlar çıkıyor. Matrix strategy tam bu iş için:
name: Multi-Environment CI
on:
push:
branches: [ main ]
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ ubuntu-latest, windows-latest ]
node-version: [ 18, 20, 21 ]
exclude:
- os: windows-latest
node-version: 21
steps:
- uses: actions/checkout@v4
- name: Node.js ${{ matrix.node-version }} kur
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm test
Bu konfigürasyonla toplam 5 paralel job çalışır (2 OS x 3 sürüm, eksi 1 exclude). Matristen bir kombinasyonu hariç tutmak için exclude kullanıyoruz. Tersine, belirli kombinasyonlara ekstra değişken eklemek için include var.
Secret ve Environment Variable Yönetimi
CI/CD’nin en kritik konularından biri secret yönetimi. Database şifreleri, API anahtarları, deployment token’ları… Bunları YAML dosyasına yazmak felakete davet etmek demek.
GitHub’da secret yönetimi şu seviyelerde yapılabiliyor:
- Repository secrets: Sadece o repo’ya özel
- Organization secrets: Tüm org repo’larına açılabilir
- Environment secrets: Belirli deployment environment’larına özel
Secret’ları Settings > Secrets and variables > Actions yolundan ekliyorsun. Workflow içinde kullanımı:
jobs:
deploy:
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- name: Docker'a login ol
run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
- name: Uygulamayı deploy et
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
API_KEY: ${{ secrets.API_KEY }}
DEPLOY_ENV: production
run: ./scripts/deploy.sh
Kritik uyarı: ${{ secrets.FALAN }} değerini log’a yazdırmaya çalışsan bile GitHub bunu * olarak maskeler. Ama secret’ı bir dosyaya yazıp o dosyayı artifact olarak upload edersen maskeleme olmaz. Bunu yaşadım, başkasının da başına gelmesini istemem.
Bağımlılık Cache’leme
Her workflow çalışmasında bağımlılıkları sıfırdan yüklemek, özellikle büyük projelerde dakikalar alıyor. Cache mekanizması bunu çözer:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Cache node_modules
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Python bağımlılıklarını cache'le
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Node bağımlılıkları
run: npm ci
- name: Python bağımlılıkları
run: pip install -r requirements.txt
Cache key mantığı önemli: hashFiles('**/package-lock.json') ifadesi, lock dosyası değiştiğinde yeni bir cache anahtarı üretir. Böylece bağımlılıklar değiştiğinde cache temizlenmiş olur, değişmediğinde eski cache kullanılır. restore-keys ise tam eşleşme bulunamazsa daha genel bir cache’i kullanmayı dener.
Docker Build ve Registry Push
Modern deployment’ların büyük çoğunluğu container tabanlı. İşte production’da kullandığım bir Docker workflow’u:
name: Docker Build and Push
on:
push:
branches: [ main ]
tags:
- 'v*.*.*'
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Docker Buildx kur
uses: docker/setup-buildx-action@v3
- name: GitHub Container Registry'ye login
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Image metadata üret
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha
- name: Build ve push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
Burada GITHUB_TOKEN kullandım, ekstra secret eklemeye gerek yok. GitHub her workflow çalışmasında bu token’ı otomatik üretiyor. permissions bloğu ile token’a packages: write yetkisi verdim, böylece GitHub Container Registry’ye push yapabiliyor.
cache-from: type=gha satırı da önemli: Docker layer cache’ini GitHub Actions cache altyapısında saklar. Büyük image’larda build süresini önemli ölçüde kısaltır.
Job Bağımlılıkları ve Deployment Pipeline
Gerçek bir CD pipeline’ı genellikle şu aşamaları içerir: test, build, staging’e deploy, smoke test, production’a deploy. Bu aşamalar sıralı ve birbirinin başarısına bağlı olmalı:
name: Full CD Pipeline
on:
push:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm test
- run: npm run lint
build:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build ve push
run: |
docker build -t ghcr.io/${{ github.repository }}:${{ github.sha }} .
docker push ghcr.io/${{ github.repository }}:${{ github.sha }}
deploy-staging:
needs: build
runs-on: ubuntu-latest
environment: staging
steps:
- uses: actions/checkout@v4
- name: Staging'e deploy et
env:
KUBE_CONFIG: ${{ secrets.STAGING_KUBE_CONFIG }}
run: |
echo "$KUBE_CONFIG" | base64 -d > kubeconfig.yaml
kubectl --kubeconfig kubeconfig.yaml set image deployment/myapp
myapp=ghcr.io/${{ github.repository }}:${{ github.sha }}
kubectl --kubeconfig kubeconfig.yaml rollout status deployment/myapp
deploy-production:
needs: deploy-staging
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- name: Production'a deploy et
env:
KUBE_CONFIG: ${{ secrets.PROD_KUBE_CONFIG }}
run: |
echo "$KUBE_CONFIG" | base64 -d > kubeconfig.yaml
kubectl --kubeconfig kubeconfig.yaml set image deployment/myapp
myapp=ghcr.io/${{ github.repository }}:${{ github.sha }}
kubectl --kubeconfig kubeconfig.yaml rollout status deployment/myapp
needs ile job bağımlılıklarını zincirledim. test başarısız olursa build çalışmaz, build çalışmaz ise deploy-staging çalışmaz. Production’da environment: production tanımını kullanıyorum. Bu sayede GitHub’ın environment protection rules’unu devreye alabilirim; mesela “production deploy için en az 2 kişinin review’u gereksin” gibi kurallar.
Self-Hosted Runner Kurulumu
GitHub’ın ücretsiz runner’ları çoğu senaryo için yeterli, ama bazı durumlarda self-hosted runner şart oluyor:
- Private network erişimi gerekiyorsa (iç veritabanları, internal servisleri)
- Özel donanım gerekiyorsa (GPU, HSM gibi)
- Çok uzun süren build’ler için (ücretsiz planda 6 saat limiti var)
- Veri güvenliği sebepleriyle kodu dışarı çıkarmak istemiyorsan
Runner kurulumu için önce GitHub’da Settings > Actions > Runners > New self-hosted runner diyorsun, oradan işletim sistemine göre komutlar geliyor. Linux için genel akış şöyle:
# Runner dizini oluştur
mkdir actions-runner && cd actions-runner
# En güncel sürümü indirmek için GitHub'daki komutu kullan
# Örnek:
curl -o actions-runner-linux-x64-2.311.0.tar.gz -L
https://github.com/actions/runner/releases/download/v2.311.0/actions-runner-linux-x64-2.311.0.tar.gz
# Arşivi aç
tar xzf ./actions-runner-linux-x64-2.311.0.tar.gz
# Konfigüre et (token GitHub UI'dan alınıyor)
./config.sh --url https://github.com/ORGADI/REPO
--token BURAYA_TOKEN_GELECEK
--name "prod-runner-01"
--labels "self-hosted,linux,production"
# Servis olarak çalıştır
sudo ./svc.sh install
sudo ./svc.sh start
Workflow’da self-hosted runner kullanmak için:
jobs:
deploy:
runs-on: [ self-hosted, linux, production ]
Bir uyarı: Self-hosted runner’ları public repo’larda kullanmak tehlikeli. Herhangi biri PR açıp kötü amaçlı kod çalıştırabilir. Public repo’larda mutlaka pull_request_target yerine pull_request event’ini kullan ve fork’lardan gelen PR’larda runner’ı devre dışı bırak.
Workflow’ları Yeniden Kullanmak: Reusable Workflows
Birden fazla repo’da aynı CI adımlarını tekrar tekrar yazmak, bakım kabusu. Reusable workflow’lar bu sorunu çözüyor:
# .github/workflows/reusable-test.yml (merkezi repo'da)
name: Reusable Test Workflow
on:
workflow_call:
inputs:
node-version:
required: false
type: string
default: '20'
secrets:
NPM_TOKEN:
required: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
- name: Private registry konfigürasyonu
run: echo "//npm.pkg.github.com/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc
- run: npm ci
- run: npm test
- run: npm run lint
Bunu kullanan başka bir workflow:
# Başka bir repo'da
name: CI
on:
push:
branches: [ main ]
jobs:
run-tests:
uses: myorg/shared-workflows/.github/workflows/reusable-test.yml@main
with:
node-version: '18'
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
Koşullu Çalıştırma ve İleri Düzey İfadeler
Bazen belirli koşullarda adımları atlamak veya sadece belirli durumlarda çalıştırmak gerekiyor:
jobs:
smart-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Değişen dosyaları kontrol et
id: changes
run: |
if git diff --name-only HEAD~1 HEAD | grep -q '^src/'; then
echo "src_changed=true" >> $GITHUB_OUTPUT
else
echo "src_changed=false" >> $GITHUB_OUTPUT
fi
- name: Build al (sadece src değiştiyse)
if: steps.changes.outputs.src_changed == 'true'
run: npm run build
- name: Notification gönder (her zaman, başarısız olsa bile)
if: always()
run: |
STATUS="${{ job.status }}"
curl -X POST "${{ secrets.SLACK_WEBHOOK }}"
-H 'Content-type: application/json'
-d "{"text": "Deploy durumu: $STATUS"}"
- name: Sadece main branch'te çalış
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
run: ./scripts/tag-release.sh
$GITHUB_OUTPUT mekanizması, step’ler arası veri taşımak için kullanılıyor. Eski yöntem olan set-output deprecated oldu, artık bu dosyaya yazma yöntemi tercih ediliyor.
Sık Karşılaşılan Sorunlar ve Çözümler
Birkaç yıllık GitHub Actions kullanımından derlediğim problem-çözüm listesi:
Permission denied hataları: Genellikle GITHUB_TOKEN yetersiz yetkilere sahip. Workflow’un başına ya da job seviyesine permissions bloğu ekle. Varsayılan yetkiler org ayarlarına göre değişiyor.
Workflow tetiklenmiyor: YAML syntax hatası olabilir. on: bloğundaki branch adlarını kontrol et. Default branch main ise master yazmak işe yaramaz.
Cache hit olmuyor: key ifadesindeki hashFiles doğru dosyayı işaret ediyor mu? Wildcard yollarını gözden geçir.
Docker login başarısız: permissions: packages: write ekli mi? Organization’da “Allow GitHub Actions to create and approve pull requests” ve package write yetkisi açık mı?
Self-hosted runner offline görünüyor: Servis çalışıyor mu? sudo systemctl status actions.runner.* ile kontrol et. Runner makinesi outbound HTTPS erişimine sahip mi?
Sonuç
GitHub Actions, kurumsal CI/CD araçlarına kıyasla öğrenme eğrisi düşük ama yetenek seti oldukça geniş bir platform. Repository ile iç içe geçmiş workflow dosyaları, versiyonlama ve takım çalışmasını kolaylaştırıyor.
Başlangıç için tavsiyem şu: Önce sadece test çalıştıran basit bir CI workflow’uyla başla. Çalışır hale gelince matrix ekle, sonra cache’lemeyi öğren, ardından Docker build’i entegre et ve en son CD pipeline’ını kur. Her adımı küçük tutmak, neyin neden çalışmadığını anlamayı kolaylaştırır.
Bir de şunu söyleyeyim: Workflow dosyalarını da kod gibi review et. PR’da gözden kaçan bir rm -rf veya yanlış bir secret referansı, production’da ciddi sorunlara yol açabilir. Workflow dosyalarını gözden geçirmeyi code review sürecinin parçası haline getirmek, uzun vadede çok zaman kazandırıyor.
