GitHub Actions ile Matrix Build Kullanarak Çoklu Ortam Testi
Yazılım geliştirme süreçlerinde en sık karşılaşılan sorunlardan biri şudur: “Bende çalışıyor.” Bu klasik cümle, genellikle farklı Node.js sürümleri, farklı işletim sistemleri veya farklı bağımlılık versiyonları yüzünden ortaya çıkar. GitHub Actions’ın matrix build özelliği tam da bu problemi çözmek için tasarlanmış güçlü bir araçtır. Tek bir workflow tanımıyla onlarca farklı kombinasyonu paralel olarak test edebilir, hangi konfigürasyonda neyin kırıldığını anında görebilirsiniz.
Matrix Build Nedir ve Neden Önemlidir
Matrix build, bir CI/CD pipeline’ının aynı anda birden fazla parametre kombinasyonuyla çalıştırılmasını sağlayan bir stratejidir. Düşünün ki bir Python kütüphanesi geliştiriyorsunuz ve bu kütüphanenin Python 3.8, 3.9, 3.10 ve 3.11 üzerinde çalışması gerekiyor. Üstelik hem Ubuntu hem de Windows ortamlarında test edilmesi lazım. Manuel olarak her kombinasyonu ayrı ayrı test etmek yerine, matrix build ile tüm bu kombinasyonları tek bir YAML dosyasıyla yönetebilirsiniz.
Gerçek dünya senaryosunda bir e-ticaret platformu düşünelim. Bu platformun backend servisi Node.js 16, 18 ve 20 ile çalışıyor olabilir, çünkü farklı müşteriler farklı LTS sürümleri kullanıyor. Aynı zamanda PostgreSQL 13, 14 ve 15 ile uyumlu olması gerekiyor. Bu durumda 3×3 = 9 farklı kombinasyon söz konusu. Matrix build olmadan bu 9 kombinasyonu manuel yönetmek hem zahmetli hem de hata yapmaya açıktır.
Temel Matrix Build Yapısı
En basit haliyle bir matrix build şöyle görünür:
name: Multi-Version Test
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16, 18, 20]
steps:
- uses: actions/checkout@v4
- name: Node.js ${{ matrix.node-version }} kur
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Bağımlılıkları yükle
run: npm ci
- name: Testleri çalıştır
run: npm test
Bu basit yapı, Node.js 16, 18 ve 20 için otomatik olarak 3 paralel job oluşturur. Her job bağımsız bir runner üzerinde çalışır ve sonuçlar GitHub arayüzünde ayrı ayrı görüntülenir.
Çoklu Boyutlu Matrix Yapısı
Gerçek gücü görmek için birden fazla boyut ekleyelim. Hem farklı işletim sistemleri hem de farklı Node.js sürümleri ile test yapalım:
name: Cross-Platform Test Suite
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node-version: [18, 20]
include:
- os: ubuntu-latest
node-version: 16
experimental: true
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- name: Node.js ${{ matrix.node-version }} kurulumu
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Bağımlılıkları yükle
run: npm ci
- name: Lint kontrolü
run: npm run lint
- name: Unit testler
run: npm test
- name: Build kontrolü
run: npm run build
Bu yapı 3×2 = 6 kombinasyon üretir, üstüne bir de include ile eklenen özel kombinasyonla 7 paralel job çalışır.
Include ve Exclude Kullanımı
Matrix’in en esnek özelliklerinden biri, belirli kombinasyonları dahil etmek veya hariç tutmaktır. Bir Python projesi için gerçekçi bir örnek verelim:
name: Python Compatibility Matrix
on: [push, pull_request]
jobs:
test:
strategy:
fail-fast: false
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
os: [ubuntu-latest, windows-latest]
database: [postgresql, mysql, sqlite]
exclude:
# Windows'ta PostgreSQL kurulumu sorunlu, hariç tut
- os: windows-latest
database: postgresql
# Python 3.8 ve MySQL kombinasyonu desteklenmiyor
- python-version: "3.8"
database: mysql
include:
# Sadece bu kombinasyona özel değişken ekle
- python-version: "3.12"
os: ubuntu-latest
database: postgresql
coverage: true
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- name: Python ${{ matrix.python-version }} kur
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Bağımlılıkları yükle
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install pytest pytest-cov
- name: Test çalıştır - Coverage ile
if: ${{ matrix.coverage == true }}
run: pytest --cov=./src --cov-report=xml
- name: Test çalıştır - Normal
if: ${{ matrix.coverage != true }}
run: pytest
- name: Coverage raporu yükle
if: ${{ matrix.coverage == true }}
uses: codecov/codecov-action@v4
with:
file: ./coverage.xml
fail-fast: false parametresi burada kritik bir öneme sahip. Varsayılan olarak matrix’teki herhangi bir job başarısız olduğunda diğerleri iptal edilir. fail-fast: false ayarı ile tüm kombinasyonların tamamlanmasını sağlarsınız, böylece hangi konfigürasyonların başarısız olduğunu tek seferinde görürsünüz.
Dinamik Matrix Oluşturma
Bazen matrix değerlerini statik olarak tanımlamak yerine dinamik olarak oluşturmak gerekir. Örneğin bir microservice mimarisinde hangi servislerin değiştiğini tespit edip sadece onları test etmek isteyebilirsiniz:
name: Dynamic Matrix Build
on:
push:
branches: [ main ]
jobs:
detect-changes:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2
- name: Değişen servisleri tespit et
id: set-matrix
run: |
# Değişen dizinleri bul
CHANGED=$(git diff --name-only HEAD~1 HEAD |
grep -E '^services/' |
cut -d'/' -f2 |
sort -u)
# Hiçbir şey değişmemişse tüm servisleri test et
if [ -z "$CHANGED" ]; then
CHANGED="auth payment notification inventory"
fi
# JSON array formatına çevir
MATRIX=$(echo $CHANGED | tr ' ' 'n' |
jq -R . | jq -s . |
jq -c '{service: .}')
echo "matrix=$MATRIX" >> $GITHUB_OUTPUT
echo "Tespit edilen servisler: $CHANGED"
test-services:
needs: detect-changes
runs-on: ubuntu-latest
strategy:
matrix: ${{ fromJson(needs.detect-changes.outputs.matrix) }}
steps:
- uses: actions/checkout@v4
- name: ${{ matrix.service }} servisini test et
run: |
cd services/${{ matrix.service }}
npm ci
npm test
Bu yaklaşım özellikle büyük monorepo projelerinde muazzam zaman tasarrufu sağlar. Sadece değişen parçaları test ettiğiniz için pipeline süresi dramatik şekilde kısalır.
Gerçek Dünya Senaryosu: Tam Bir API Projesi
Şimdi gerçek bir senaryo üzerine düşünelim. Bir REST API projesi geliştiriyorsunuz, farklı veritabanı sürümleriyle test etmeniz, Docker image build etmeniz ve staging ortamına deploy etmeniz gerekiyor:
name: Full CI/CD Pipeline
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
lint-and-type-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run lint
- run: npm run type-check
test-matrix:
needs: lint-and-type-check
strategy:
fail-fast: false
matrix:
node-version: [18, 20]
postgres-version: [13, 14, 15, 16]
exclude:
# Eski kombinasyonları hariç tut
- node-version: 18
postgres-version: 13
runs-on: ubuntu-latest
services:
postgres:
image: postgres:${{ matrix.postgres-version }}
env:
POSTGRES_USER: testuser
POSTGRES_PASSWORD: testpass
POSTGRES_DB: testdb
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- uses: actions/checkout@v4
- name: Node.js ${{ matrix.node-version }} kur
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Bağımlılıkları yükle
run: npm ci
- name: Veritabanı migration çalıştır
env:
DATABASE_URL: postgresql://testuser:testpass@localhost:5432/testdb
run: npm run migrate
- name: Integration testleri çalıştır
env:
DATABASE_URL: postgresql://testuser:testpass@localhost:5432/testdb
NODE_ENV: test
run: npm run test:integration
- name: Test sonuçlarını artifact olarak sakla
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results-node${{ matrix.node-version }}-pg${{ matrix.postgres-version }}
path: ./test-results/
retention-days: 7
build-and-push:
needs: test-matrix
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Container Registry'e giriş
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Docker image build et ve push et
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
Bu pipeline’da dikkat edilmesi gereken birkaç nokta var. Önce lint ve type-check çalışıyor, bu geçmeden matrix test’ler başlamıyor. Test matrix’i ise PostgreSQL servisini otomatik olarak ayağa kaldırıyor ve her kombinasyon için ayrı bir container ayağa kalkıyor.
Matrix ile Performans Optimizasyonu
Matrix build’ler paralel çalışsa da yanlış konfigüre edilmiş cache yapısı tüm hızı mahvedebilir. Doğru cache stratejisi şöyle kurulur:
name: Optimized Matrix Build
on: [push, pull_request]
jobs:
test:
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
python-version: ["3.10", "3.11", "3.12"]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- name: Python ${{ matrix.python-version }} kur
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: pip cache yolu al
id: pip-cache
run: |
echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT
- name: pip cache'i yükle
uses: actions/cache@v4
with:
path: ${{ steps.pip-cache.outputs.dir }}
# OS ve Python versiyonu bazlı cache key
key: ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('**/requirements*.txt') }}
restore-keys: |
${{ runner.os }}-pip-${{ matrix.python-version }}-
${{ runner.os }}-pip-
- name: Bağımlılıkları yükle
run: |
pip install -r requirements.txt
pip install -r requirements-dev.txt
- name: Testleri çalıştır
run: pytest -x -q --tb=short
- name: Performans benchmark
run: pytest tests/benchmarks/ --benchmark-json=benchmark-${{ matrix.os }}-${{ matrix.python-version }}.json
- name: Benchmark sonuçlarını kaydet
uses: actions/upload-artifact@v4
with:
name: benchmark-${{ matrix.os }}-py${{ matrix.python-version }}
path: benchmark-*.json
Cache key’ine matrix.python-version ve runner.os eklemek çok önemli. Aksi takdirde farklı Python sürümleri birbirinin cache’ini bozar.
Matrix Build Hata Ayıklama İpuçları
Matrix build’lerde hata ayıklamak bazen can sıkıcı olabilir. Hangi kombinasyonun neden başarısız olduğunu anlamak için bazı pratik teknikler:
name: Debug-Friendly Matrix
on: [push]
jobs:
test:
strategy:
fail-fast: false
matrix:
config:
- name: "Prod benzeri ortam"
node: 20
env: production
extra-flags: "--production"
- name: "Geliştirme ortamı"
node: 18
env: development
extra-flags: ""
- name: "Legacy destek"
node: 16
env: legacy
extra-flags: "--legacy-peer-deps"
runs-on: ubuntu-latest
name: "${{ matrix.config.name }}"
steps:
- uses: actions/checkout@v4
- name: Ortam bilgilerini göster
run: |
echo "=== Ortam Bilgileri ==="
echo "Konfigürasyon: ${{ matrix.config.name }}"
echo "Node versiyonu: ${{ matrix.config.node }}"
echo "Ortam: ${{ matrix.config.env }}"
echo "Extra flags: ${{ matrix.config.extra-flags }}"
echo "Runner OS: ${{ runner.os }}"
echo "Runner arch: ${{ runner.arch }}"
node --version 2>/dev/null || echo "Node henüz kurulmadı"
- name: Node.js kur
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.config.node }}
- name: Bağımlılıkları yükle
run: npm ci ${{ matrix.config.extra-flags }}
- name: Test çalıştır
env:
NODE_ENV: ${{ matrix.config.env }}
run: |
npm test 2>&1 | tee test-output.txt
echo "Exit code: $?"
- name: Hata durumunda log yükle
if: failure()
uses: actions/upload-artifact@v4
with:
name: failure-logs-${{ matrix.config.env }}
path: |
test-output.txt
npm-debug.log*
name alanını matrix değişkenleriyle özelleştirmek GitHub Actions arayüzünde hangi job’ın ne olduğunu anlamayı çok kolaylaştırır. Varsayılan “(ubuntu-latest, 20)” yerine “Prod benzeri ortam” gibi anlamlı isimler görmek hata ayıklarken büyük fark yaratır.
Conditional Matrix ve Output Kullanımı
Bazı durumlarda matrix job’larının çıktısına göre sonraki adımları şekillendirmek istersiniz:
name: Matrix with Outputs
on:
push:
branches: [ main ]
jobs:
compatibility-check:
strategy:
fail-fast: false
matrix:
version: [1, 2, 3]
runs-on: ubuntu-latest
outputs:
compatible-v1: ${{ steps.check.outputs.compatible }}
steps:
- uses: actions/checkout@v4
- name: Uyumluluk kontrolü yap
id: check
run: |
# Versiyon uyumluluğunu kontrol et
if ./scripts/check-compatibility.sh ${{ matrix.version }}; then
echo "compatible=true" >> $GITHUB_OUTPUT
echo "Versiyon ${{ matrix.version }}: Uyumlu"
else
echo "compatible=false" >> $GITHUB_OUTPUT
echo "Versiyon ${{ matrix.version }}: Uyumsuz"
fi
- name: Uyumluluk raporu oluştur
run: |
echo "## Uyumluluk Raporu" > report-v${{ matrix.version }}.md
echo "- Versiyon: ${{ matrix.version }}" >> report-v${{ matrix.version }}.md
echo "- Durum: $(cat $GITHUB_OUTPUT | grep compatible | cut -d= -f2)" >> report-v${{ matrix.version }}.md
- uses: actions/upload-artifact@v4
with:
name: compatibility-report-v${{ matrix.version }}
path: report-v${{ matrix.version }}.md
create-release-notes:
needs: compatibility-check
runs-on: ubuntu-latest
if: always()
steps:
- name: Raporları indir
uses: actions/download-artifact@v4
with:
pattern: compatibility-report-*
merge-multiple: true
- name: Birleşik rapor oluştur
run: |
echo "# Tam Uyumluluk Raporu" > final-report.md
cat report-v*.md >> final-report.md
cat final-report.md
Maliyet ve Sınır Yönetimi
GitHub Actions ücretsiz planlarda dakika sınırı var ve matrix build bu dakikaları hızla tüketebilir. Akıllı bir sınır yönetimi için:
- max-parallel ayarını kullanın:
max-parallel: 3ile aynı anda çalışan job sayısını sınırlayabilirsiniz - Sadece
mainvedevelopbranch’lerinde tam matrix çalıştırın, feature branch’lerinde sadece kritik kombinasyonları test edin - timeout-minutes ekleyin: Sonsuz döngüye giren bir job tüm kotanızı bitirebilir
- Cache kullanımını maksimize edin, özellikle bağımlılık yükleme adımlarında
Bir workflow’da bu optimizasyonları birlikte kullanmak şöyle görünür:
jobs:
test:
strategy:
fail-fast: false
max-parallel: 4
matrix:
node-version: [18, 20]
os: [ubuntu-latest, windows-latest]
timeout-minutes: 15
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm ci
- run: npm test
Sonuç
Matrix build, modern yazılım geliştirme süreçlerinin vazgeçilmez bir parçası haline geldi. Tek bir YAML dosyasıyla onlarca farklı konfigürasyonu test etmek, çapraz platform uyumluluk sorunlarını production’a çıkmadan yakalamak ve her ortam için anlamlı hata raporları almak artık standart bir pratik olmalı.
Başlangıçta basit bir çok boyutlu array gibi görünen matrix stratejisi, include, exclude, dinamik matrix oluşturma ve conditional çalıştırma gibi özelliklerle gerçek anlamda esnek bir test altyapısına dönüşüyor. Özellikle dinamik matrix yaklaşımı, büyük monorepo projelerinde sadece değişen parçaları test ederek hem zaman hem de CI/CD maliyetlerini önemli ölçüde düşürüyor.
Cache optimizasyonu ve max-parallel gibi ayarları ihmal etmeyin. Matrix build’ler paralel çalışma avantajını kaybedirse veya her çalışmada bağımlılıkları sıfırdan yüklerse, kazanç yerine kayıp yaşarsınız. Doğru konfigüre edilmiş bir matrix pipeline, ekibin güvenle kod gönderdiği, sorunların erken yakalandığı ve “bende çalışıyor” bahanesinin tarihe karıştığı bir ortam oluşturur. Yavaş yavaş uygulamaya başlayın, küçük bir matrix ile başlayıp deneyim kazandıkça genişletin.
