GitHub Actions Yeniden Kullanılabilir İş Akışları ile Şablon Paylaşımı

Ekibinizde birden fazla repository yönetiyorsanız ve her birinde benzer CI/CD pipeline’ları tekrar tekrar yazıyorsanız, bu yazı tam size göre. GitHub Actions’ın Reusable Workflows özelliği, workflow tanımlarınızı merkezi bir yerden yönetmenizi ve tüm repository’lerinizde yeniden kullanmanızı sağlıyor. Hem zaman kazandırıyor hem de tutarlılığı artırıyor.

Reusable Workflow Nedir?

GitHub Actions’ta normalde her repository kendi .github/workflows/ dizininde kendi workflow dosyalarını barındırır. Ama büyük organizasyonlarda onlarca, hatta yüzlerce repository’niz olduğunda her birinde aynı deployment veya test pipeline’ını kopyala-yapıştır yöntemiyle oluşturmak bir kabus haline gelir.

Reusable Workflow, bir workflow dosyasını başka bir repository’deki (ya da aynı repository’deki) workflow’dan çağırmanıza izin veriyor. Bunu bir fonksiyon çağırmak gibi düşünebilirsiniz: bir kere tanımlıyorsunuz, her yerden çağırıyorsunuz.

Özellikle şu senaryolarda hayat kurtarıcı oluyor:

  • Mikroservis mimarisi: 20 ayrı servisiniz var ve hepsi aynı Docker build ve push adımlarını çalıştırıyor.
  • Çoklu ortam dağıtımı: Dev, staging, production ortamları için aynı deployment mantığını kullanıyorsunuz.
  • Ekip standardizasyonu: Security scan, lint, test adımlarının tüm projelerde tutarlı çalışmasını istiyorsunuz.
  • Bakım kolaylığı: Workflow’da bir değişiklik yapmanız gerektiğinde tek bir yerden güncelleyip her yere yansıtmak istiyorsunuz.

Temel Yapı: Caller ve Called Workflow

Sistemin iki bileşeni var. Called workflow (şablon), workflow_call trigger’ı ile tanımlanan ve çağrılmayı bekleyen dosya. Caller workflow ise bu şablonu çağıran ve kullanmak isteyen taraf.

Called Workflow Anatomisi

Bir called workflow’un temel iskeleti şu şekilde:

# .github/workflows/reusable-docker-build.yml
name: Reusable Docker Build and Push

on:
  workflow_call:
    inputs:
      image-name:
        required: true
        type: string
        description: "Docker image adı (örn: myapp/api)"
      tag:
        required: false
        type: string
        default: "latest"
        description: "Docker image tag"
      dockerfile-path:
        required: false
        type: string
        default: "./Dockerfile"
        description: "Dockerfile'ın konumu"
      registry:
        required: false
        type: string
        default: "ghcr.io"
        description: "Container registry adresi"
    secrets:
      registry-username:
        required: true
      registry-password:
        required: true

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

      - name: Docker Login
        uses: docker/login-action@v3
        with:
          registry: ${{ inputs.registry }}
          username: ${{ secrets.registry-username }}
          password: ${{ secrets.registry-password }}

      - name: Build and Push
        uses: docker/build-push-action@v5
        with:
          context: .
          file: ${{ inputs.dockerfile-path }}
          push: true
          tags: ${{ inputs.registry }}/${{ inputs.image-name }}:${{ inputs.tag }}

Burada dikkat etmeniz gereken birkaç önemli nokta var:

  • inputs: Caller’dan alacağınız parametreler. type olarak string, boolean, number kullanabilirsiniz.
  • secrets: Gizli bilgileri inputs üzerinden geçiremezsiniz, bunun için ayrı secrets bloğu var.
  • required: false olan parametreler için mutlaka default değer belirtmenizi öneririm, yoksa boş değer sürprizlerle karşılaşabilirsiniz.

Caller Workflow’dan Çağırmak

Şimdi bu workflow’u başka bir repository’den nasıl çağırırız:

# myapp-api/.github/workflows/deploy.yml
name: Deploy API

on:
  push:
    branches:
      - main

jobs:
  docker-build:
    uses: myorg/.github/.github/workflows/reusable-docker-build.yml@main
    with:
      image-name: "myorg/api"
      tag: ${{ github.sha }}
      dockerfile-path: "./docker/Dockerfile.prod"
    secrets:
      registry-username: ${{ secrets.GHCR_USERNAME }}
      registry-password: ${{ secrets.GHCR_TOKEN }}

Sözdizimi şu şekilde: {owner}/{repo}/.github/workflows/{filename}@{ref}. ref olarak branch adı, tag veya commit SHA kullanabilirsiniz. Production ortamlarında belirli bir tag veya commit SHA kullanmak daha güvenli.

Gerçek Dünya Senaryosu: Merkezi Workflow Repository’si

Organizasyonunuzda merkezi bir .github repository’si oluşturmak en yaygın ve pratik yaklaşım. Bu repository’nin özel adı {organization-name}/.github oluyor ve GitHub bu repository’ye özel bir anlam yüklüyor.

Örnek bir yapı düşünelim: 15 mikroservisi olan bir e-ticaret platformu yönetiyorsunuz. Her servis için şunları yapmak istiyorsunuz:

  • Unit test çalıştırmak
  • Docker image build etmek
  • Kubernetes’e deploy etmek
  • Slack’e bildirim göndermek

Merkezi Test Workflow’u

# .github/.github/workflows/reusable-test.yml
name: Reusable Test Suite

on:
  workflow_call:
    inputs:
      node-version:
        required: false
        type: string
        default: "20"
      test-command:
        required: false
        type: string
        default: "npm test"
      coverage-threshold:
        required: false
        type: number
        default: 80
      working-directory:
        required: false
        type: string
        default: "."
    outputs:
      coverage-percent:
        description: "Test coverage yüzdesi"
        value: ${{ jobs.test.outputs.coverage }}

jobs:
  test:
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: ${{ inputs.working-directory }}
    outputs:
      coverage: ${{ steps.coverage.outputs.percent }}
    
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ inputs.node-version }}
          cache: "npm"
          cache-dependency-path: "${{ inputs.working-directory }}/package-lock.json"

      - name: Install Dependencies
        run: npm ci

      - name: Run Tests
        run: ${{ inputs.test-command }} --coverage

      - name: Check Coverage Threshold
        id: coverage
        run: |
          COVERAGE=$(cat coverage/coverage-summary.json | jq '.total.lines.pct')
          echo "percent=$COVERAGE" >> $GITHUB_OUTPUT
          if (( $(echo "$COVERAGE < ${{ inputs.coverage-threshold }}" | bc -l) )); then
            echo "Coverage $COVERAGE% is below threshold ${{ inputs.coverage-threshold }}%"
            exit 1
          fi
          echo "Coverage check passed: $COVERAGE%"

Kubernetes Deployment Workflow’u

# .github/.github/workflows/reusable-k8s-deploy.yml
name: Reusable Kubernetes Deployment

on:
  workflow_call:
    inputs:
      environment:
        required: true
        type: string
        description: "Hedef ortam: dev, staging, production"
      service-name:
        required: true
        type: string
      image-tag:
        required: true
        type: string
      namespace:
        required: false
        type: string
        default: "default"
      helm-chart-path:
        required: false
        type: string
        default: "./helm"
      dry-run:
        required: false
        type: boolean
        default: false
    secrets:
      kubeconfig:
        required: true
      helm-values-secret:
        required: false

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: ${{ inputs.environment }}
    
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup kubectl
        uses: azure/setup-kubectl@v3

      - name: Setup Helm
        uses: azure/setup-helm@v3
        with:
          version: "3.13.0"

      - name: Configure kubeconfig
        run: |
          mkdir -p ~/.kube
          echo "${{ secrets.kubeconfig }}" | base64 -d > ~/.kube/config
          chmod 600 ~/.kube/config

      - name: Helm Deploy
        run: |
          EXTRA_ARGS=""
          if [ "${{ inputs.dry-run }}" == "true" ]; then
            EXTRA_ARGS="--dry-run"
          fi
          
          helm upgrade --install ${{ inputs.service-name }} 
            ${{ inputs.helm-chart-path }} 
            --namespace ${{ inputs.namespace }} 
            --create-namespace 
            --set image.tag=${{ inputs.image-tag }} 
            --set environment=${{ inputs.environment }} 
            --wait 
            --timeout 5m 
            $EXTRA_ARGS

      - name: Verify Deployment
        if: ${{ !inputs.dry-run }}
        run: |
          kubectl rollout status deployment/${{ inputs.service-name }} 
            -n ${{ inputs.namespace }} 
            --timeout=300s

Workflow Zincirleme: Karmaşık Pipeline’lar

Reusable workflow’ların gerçek gücü, bunları birbirini takip eden job’larda kullanarak oluşturduğunuz zincirde ortaya çıkıyor. Bir servisin tam CI/CD pipeline’ı şöyle görünebilir:

# payment-service/.github/workflows/cicd.yml
name: Payment Service CI/CD

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

env:
  SERVICE_NAME: payment-service
  IMAGE_NAME: myorg/payment-service

jobs:
  test:
    uses: myorg/.github/.github/workflows/[email protected]
    with:
      node-version: "18"
      test-command: "npm run test:unit"
      coverage-threshold: 85

  security-scan:
    needs: test
    uses: myorg/.github/.github/workflows/[email protected]
    with:
      scan-type: "full"
      fail-on-high: true
    secrets:
      snyk-token: ${{ secrets.SNYK_TOKEN }}

  build:
    needs: [test, security-scan]
    uses: myorg/.github/.github/workflows/[email protected]
    with:
      image-name: ${{ env.IMAGE_NAME }}
      tag: ${{ github.sha }}
    secrets:
      registry-username: ${{ secrets.GHCR_USERNAME }}
      registry-password: ${{ secrets.GHCR_TOKEN }}

  deploy-staging:
    needs: build
    if: github.ref == 'refs/heads/main'
    uses: myorg/.github/.github/workflows/[email protected]
    with:
      environment: staging
      service-name: ${{ env.SERVICE_NAME }}
      image-tag: ${{ github.sha }}
      namespace: staging
    secrets:
      kubeconfig: ${{ secrets.STAGING_KUBECONFIG }}

  deploy-production:
    needs: deploy-staging
    if: github.ref == 'refs/heads/main'
    uses: myorg/.github/.github/workflows/[email protected]
    with:
      environment: production
      service-name: ${{ env.SERVICE_NAME }}
      image-tag: ${{ github.sha }}
      namespace: production
    secrets:
      kubeconfig: ${{ secrets.PROD_KUBECONFIG }}

Bu yapıda payment-service repository’sinde sadece 60 satırlık bir dosya var ama arkasında tam teşekküllü bir pipeline çalışıyor.

Versiyonlama Stratejisi

Reusable workflow’larınızı versiyonlamak kritik önem taşıyor. @main kullanmak başlangıç için kolay ama production’da tehlikeli. Bir hata yaptığınızda tüm servisleri birden bozabilirsiniz.

Önerilen yaklaşım semantic versioning ile tag kullanmak:

# Kötü pratik - her zaman main branch'i kullanır
uses: myorg/.github/.github/workflows/reusable-deploy.yml@main

# İyi pratik - belirli bir versiyon
uses: myorg/.github/.github/workflows/[email protected]

# Kabul edilebilir - major version tag
uses: myorg/.github/.github/workflows/reusable-deploy.yml@v2

.github repository’nize bir CHANGELOG.md ekleyin ve her değişikliği belgeleyin. Breaking change yaptığınızda major versiyon numarasını artırın ve servis ekiplerine migration süresi tanıyın.

Versiyonlama için basit bir tag push scripti:

#!/bin/bash
# tag-release.sh
VERSION=$1

if [ -z "$VERSION" ]; then
  echo "Kullanim: ./tag-release.sh v2.1.0"
  exit 1
fi

git tag -a $VERSION -m "Release $VERSION"
git push origin $VERSION

# Major version tag'ini de güncelle
MAJOR=$(echo $VERSION | cut -d'.' -f1)
git tag -fa $MAJOR -m "Update $MAJOR to $VERSION"
git push origin $MAJOR --force

echo "Versiyon $VERSION yayinlandi"

Gelişmiş Özellikler: Matrix ve Outputs

Reusable workflow’larda matrix stratejisini de kullanabilirsiniz. Örneğin birden fazla Python versiyonunda test etmek istiyorsanız:

# caller workflow
jobs:
  multi-version-test:
    strategy:
      matrix:
        python-version: ["3.10", "3.11", "3.12"]
    uses: myorg/.github/.github/workflows/reusable-python-test.yml@v1
    with:
      python-version: ${{ matrix.python-version }}
    secrets: inherit

secrets: inherit kullanımına dikkat edin. Bu, caller workflow’un erişebildiği tüm secret’ları called workflow’a otomatik olarak geçirir. Pratik ama dikkatli kullanın. Hangi secret’ların paylaşıldığını takip etmek zorlaşabilir.

Outputs özelliği de çok işe yarıyor. Bir workflow’dan çıktı alıp sonraki job’da kullanabilirsiniz:

# Caller workflow'da outputs kullanımı
jobs:
  build:
    uses: myorg/.github/.github/workflows/reusable-build.yml@v1
    with:
      image-name: "myapp"

  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - name: Deploy
        run: |
          echo "Image digest: ${{ needs.build.outputs.image-digest }}"
          echo "Build number: ${{ needs.build.outputs.build-number }}"
          # Bu değerleri deployment'ta kullan

Yaygın Hatalar ve Çözümleri

Reusable workflow kullanırken sıkça karşılaşılan sorunları listeleyelim:

  • Secret geçirme sorunu: inputs bloğu üzerinden secret geçirmeye çalışmak. Bunu yaparsanız değer maskelenmiyor ve log’larda görünüyor. Her zaman secrets bloğunu kullanın.
  • Context erişim kısıtlamaları: Called workflow içinde github.event context’i caller’ın event’ini taşımıyor. Eğer PR bilgisine ihtiyaç varsa bunu input olarak geçirmeniz gerekiyor.
  • Nested reusable workflow: Bir called workflow başka bir called workflow’u çağıramaz. En fazla bir seviye derinliğe inebilirsiniz. Bunu tasarım aşamasında göz önünde bulundurun.
  • Workflow dosyası konumu: Sadece .github/workflows/ dizinindeki dosyalar çağrılabilir. Alt klasörler çalışmıyor.
  • Permissions: Called workflow’un ihtiyaç duyduğu GitHub permissions’ları caller tarafından açıkça belirtilmeli veya devralınmalı.

Güvenlik Düşünceleri

Reusable workflow’lar organizasyonunuzdaki güvenlik standardınızı yükseltmek için mükemmel bir fırsat sunuyor ama yanlış kullanıldığında risk de oluşturabiliyor.

Önemli noktalar:

  • Repository erişimi: Bir called workflow yalnızca içinde bulunduğu repository’nin kaynaklarına doğrudan erişebilir. Bu hem bir kısıtlama hem de güvenlik özelliği.
  • CODEOWNERS kullanımı: .github repository’nize CODEOWNERS dosyası ekleyin ve workflow değişikliklerini inceleme zorunluluğu koyun. Tek bir kişinin tüm pipeline’ları değiştirmesinin önüne geçin.
  • Harici repository güveni: Başka bir organizasyonun workflow’unu kullanacaksanız commit SHA ile kilitleyin, branch veya tag ile değil.
# Güvenli harici kullanım - SHA ile kilitle
uses: external-org/shared-workflows/.github/workflows/security-scan.yml@a1b2c3d4e5f6
  • Environment protection rules: Özellikle production deployment workflow’larında GitHub Environments’ı kullanın ve manual approval ekleyin.

Bakım ve Monitoring

Workflow şablonlarınızı merkezi yönetiyorsanız bir şeylerin bozulduğunu erken fark etmek önemli. Bunun için .github repository’nizde bir “smoke test” workflow’u ekleyebilirsiniz:

# .github/.github/workflows/self-test.yml
name: Workflow Self Test

on:
  push:
    branches: [main]
  schedule:
    - cron: "0 6 * * 1"  # Her Pazartesi sabah

jobs:
  test-docker-workflow:
    uses: ./.github/workflows/reusable-docker-build.yml
    with:
      image-name: "test/smoke-test"
      tag: "test-${{ github.run_id }}"
      dockerfile-path: "./test/Dockerfile.test"
    secrets:
      registry-username: ${{ secrets.GHCR_USERNAME }}
      registry-password: ${{ secrets.GHCR_TOKEN }}

  test-deploy-workflow:
    uses: ./.github/workflows/reusable-k8s-deploy.yml
    with:
      environment: "dev"
      service-name: "smoke-test"
      image-tag: "latest"
      dry-run: true
      namespace: "smoke-test"
    secrets:
      kubeconfig: ${{ secrets.DEV_KUBECONFIG }}

Bu workflow her Pazartesi çalışarak şablonlarınızın hala düzgün çalıştığını doğruluyor. Birisi workflow’u kırdığında haberdar oluyorsunuz.

Göç Stratejisi: Mevcut Workflow’ları Dönüştürmek

Onlarca repository’de halihazırda workflow’larınız varsa hepsini birden dönüştürmeye çalışmayın. Pratik bir yaklaşım:

İlk hafta: En az 3 repository’de kullanılan ortak pattern’ları belirleyin. Genellikle bunlar test, build ve deploy adımları oluyor.

İkinci hafta: .github repository’sini oluşturun ve ilk reusable workflow’ları yazın. Sadece bir pilot servis için caller workflow oluşturun ve test edin.

Üçüncü ve dördüncü haftalar: Pilot başarılıysa diğer servisleri birer birer taşıyın. Her taşımanın ardından eski workflow dosyasını silmek yerine önce ikisini paralel çalıştırın.

İlk ay sonrası: Artık yeni projeler için direkt reusable workflow kullanın. Eski projeler için migration zamanı geldiğinde standart sürece dahil edin.

Sonuç

Reusable Workflows, orta ve büyük ölçekli ekipler için bir lüks değil gerçek anlamda operasyonel bir zorunluluk. Kopyala-yapıştır workflow kültürü kısa vadede kolay görünüyor ama zamanla “biz neden farklı bir security scanner kullanıyoruz” veya “neden bu servis farklı bir node versiyonunda build alıyor” gibi sorularla boğuluyorsunuz.

Merkezi workflow yönetiminin getirdiği avantajlar somut: bir güvenlik açığını onlarca repository’de tek tek düzeltmek yerine tek bir dosyayı güncelleyip tag atmak yeterli oluyor. Yeni bir servis projeye dahil olduğunda Dockerfile yazmak bilmese bile doğru pratikleri otomatik olarak devralıyor.

Başlamak için devasa bir .github repository’si kurmak gerekmiyor. Önce ekibinizde en sık tekrar eden üç adımı belirleyin, onları reusable workflow’a çevirin ve iki serviste test edin. Faydayı gördükten sonra kapsamı genişletmek çok daha kolay oluyor. Versiyonlamayı başından doğru kurun, gerisi kendiliğinden gelişiyor.

Bir yanıt yazın

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