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: 3 ile aynı anda çalışan job sayısını sınırlayabilirsiniz
  • Sadece main ve develop branch’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.

Bir yanıt yazın

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