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_dispatch gibi.
  • 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-latest var; 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.

Bir yanıt yazın

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