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.
typeolarakstring,boolean,numberkullanabilirsiniz. - secrets: Gizli bilgileri
inputsüzerinden geçiremezsiniz, bunun için ayrısecretsbloğu var. required: falseolan parametreler için mutlakadefaultdeğ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:
inputsbloğu üzerinden secret geçirmeye çalışmak. Bunu yaparsanız değer maskelenmiyor ve log’larda görünüyor. Her zamansecretsbloğunu kullanın.
- Context erişim kısıtlamaları: Called workflow içinde
github.eventcontext’i caller’ın event’ini taşımıyor. Eğer PR bilgisine ihtiyaç varsa bunuinputolarak 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ı:
.githubrepository’nizeCODEOWNERSdosyası 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.
