· Admin

GitHub Actions ile Laravel CI/CD Pipeline: Test, Build, Deploy

Her commit sonrasi manuel test kosturmak, FTP ile dosya yuklemek, SSH'a baglanip git pull yapmak... Bunlari yillar boyunca yaptim ve her seferinde bir seyler unutuldu, bir seyler bozuldu. CI/CD pipeline kurmak ilk basta ekstra is gibi gorunuyor ama bir kez kurdugunuzda geri donusu muazzam. Bu makalede GitHub Actions ile Laravel projeleri icin nasil saglam bir pipeline kurabileceginizi, kendi projelerimde kullandigim workflow'u paylasiyorum.

Neden CI/CD?

Kisa cevap: insan hata yapar, otomasyon yapmaz. Uzun cevap:

  • Her push'ta otomatik test kosar, kirik kod main branch'e girmez
  • Kod stili tutarli kalir (Pint/CS Fixer otomatik kontrol)
  • Deploy sureci tekrarlanabilir ve denetlenebilir
  • Gece 3'te acil fix yaptiginizda bile ayni kalitede deploy olur
  • Takim buyudugunde herkes ayni surece tabi olur

Temel Workflow Yapisi

GitHub Actions workflow dosyalari .github/workflows/ dizininde YAML formatinda bulunur. Laravel icin temel yapiyi inceleyelim:

# .github/workflows/laravel.yml
name: Laravel CI/CD

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

jobs:
  test:
    runs-on: ubuntu-latest

    services:
      mysql:
        image: mysql:8.0
        env:
          MYSQL_ROOT_PASSWORD: password
          MYSQL_DATABASE: testing
        ports:
          - 3306:3306
        options: >-
          --health-cmd="mysqladmin ping"
          --health-interval=10s
          --health-timeout=5s
          --health-retries=3

    steps:
      - uses: actions/checkout@v4

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.4'
          extensions: mbstring, xml, bcmath, pdo_mysql, redis
          coverage: xdebug

      - name: Cache Composer packages
        uses: actions/cache@v4
        with:
          path: vendor
          key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
          restore-keys: ${{ runner.os }}-composer-

      - name: Install dependencies
        run: composer install --no-interaction --prefer-dist --optimize-autoloader

      - name: Copy .env
        run: cp .env.example .env

      - name: Generate key
        run: php artisan key:generate

      - name: Run migrations
        env:
          DB_CONNECTION: mysql
          DB_HOST: 127.0.0.1
          DB_PORT: 3306
          DB_DATABASE: testing
          DB_USERNAME: root
          DB_PASSWORD: password
        run: php artisan migrate --force

      - name: Run tests
        env:
          DB_CONNECTION: mysql
          DB_HOST: 127.0.0.1
          DB_PORT: 3306
          DB_DATABASE: testing
          DB_USERNAME: root
          DB_PASSWORD: password
        run: php artisan test --parallel

Bu workflow her push ve PR'da calisir, MySQL servisi ayaga kaldirir, dependency'leri yukler ve testleri kosturur.

PHPUnit Testleri Otomatik Calistirma

Test yazma aliskanliginiz yoksa CI/CD'nin anlami azalir. En azindan kritik is mantigi icin feature test'ler yazmayi oneriririm:

// tests/Feature/Auth/LoginTest.php
class LoginTest extends TestCase
{
    use RefreshDatabase;

    public function test_user_can_login_with_correct_credentials(): void
    {
        $user = User::factory()->create([
            'password' => Hash::make('password123'),
        ]);

        $response = $this->postJson('/api/login', [
            'email' => $user->email,
            'password' => 'password123',
        ]);

        $response->assertOk()
            ->assertJsonStructure(['token']);
    }

    public function test_user_cannot_login_with_wrong_password(): void
    {
        $user = User::factory()->create();

        $response = $this->postJson('/api/login', [
            'email' => $user->email,
            'password' => 'wrong-password',
        ]);

        $response->assertUnauthorized();
    }
}

php artisan test --parallel flagi testleri paralel calistirarak sureci hizlandirir. CI'da her saniye onemli.

Laravel Pint ile Kod Stili Kontrolu

Laravel Pint, CS Fixer uzerine kurulu ve Laravel'e ozel preset'lerle geliyor. CI'da sadece kontrol etmek icin --test flagini kullaniyorum:

  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.4'

      - name: Install dependencies
        run: composer install --no-interaction --prefer-dist

      - name: Run Pint
        run: vendor/bin/pint --test

--test flagi kodu degistirmez, sadece uyumsuzluk varsa hata verir. Boylece PR'larda kod stili otomatik kontrol edilir.

Pint konfigurasyonu icin projemde su ayarlari kullaniyorum:

{
    "preset": "laravel",
    "rules": {
        "simplified_null_return": true,
        "no_unused_imports": true,
        "ordered_imports": {
            "sort_algorithm": "alpha"
        }
    }
}

SSH ile Sunucuya Deploy

Deploy icin iki ana yaklasim var: self-hosted runner ve SSH action. Projelerimde SSH action tercih ediyorum cunku sunucuda runner kurmak ekstra bakim gerektiriyor.

SSH Action ile Deploy

  deploy:
    needs: [test, lint]
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'

    steps:
      - name: Deploy to production
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          port: 22
          script: |
            cd /var/www/myapp
            git pull origin main
            composer install --no-dev --optimize-autoloader
            php artisan migrate --force
            php artisan config:cache
            php artisan route:cache
            php artisan view:cache
            php artisan queue:restart
            echo "Deploy tamamlandi: $(date)"

Dikkat etmeniz gereken noktalar:

  • needs: [test, lint] ile deploy, test ve lint basarili olduktan sonra calisir
  • if kosulu ile sadece main branch'e push yapildiginda deploy tetiklenir
  • --no-dev flagi production'da dev dependency'leri yuklemez
  • --force flagi production'da migration onayini atlar
  • queue:restart ile queue worker'lar yeni kodu yukler

Deploy Script Olarak Ayirma

Deploy komutlarini dogrudan workflow'a yazmak yerine sunucuda bir script dosyasi olusturmayi tercih ediyorum:

#!/bin/bash
# /var/www/myapp/deploy.sh

set -e

echo "Deploy basladi: $(date)"

cd /var/www/myapp

# Maintenance mode
php artisan down --retry=60

# Git pull
git pull origin main

# Composer
composer install --no-dev --optimize-autoloader --no-interaction

# Migration
php artisan migrate --force

# Cache
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan event:cache

# Queue restart
php artisan queue:restart

# Maintenance mode kapat
php artisan up

echo "Deploy tamamlandi: $(date)"

Workflow'da sadece bu scripti cagiriyorum:

script: |
  cd /var/www/myapp && bash deploy.sh

Environment Secrets Yonetimi

GitHub Actions'da hassas bilgileri asla workflow dosyasina yazmayin. Repository Settings > Secrets and variables > Actions altindan tanimlayin.

Tipik olarak su secret'lari tanimliyorum:

  • SERVER_HOST - Sunucu IP adresi
  • SERVER_USER - SSH kullanici adi
  • SSH_PRIVATE_KEY - SSH ozel anahtar
  • SLACK_WEBHOOK - Deploy bildirimi icin (opsiyonel)
# Secret kullanimi
${{ secrets.SERVER_HOST }}
${{ secrets.SSH_PRIVATE_KEY }}

Environment'lari ayirmak icin GitHub Environments ozelligini kullanabilirsiniz:

deploy-staging:
    needs: [test]
    runs-on: ubuntu-latest
    environment: staging
    steps:
      # staging secret'lari kullanilir

deploy-production:
    needs: [test, deploy-staging]
    runs-on: ubuntu-latest
    environment: production
    steps:
      # production secret'lari kullanilir

Tam Workflow Ornegi

Projelerimde kullandigim workflow'un tam hali:

name: Laravel CI/CD

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

jobs:
  test:
    runs-on: ubuntu-latest
    services:
      mysql:
        image: mysql:8.0
        env:
          MYSQL_ROOT_PASSWORD: password
          MYSQL_DATABASE: testing
        ports:
          - 3306:3306
        options: >-
          --health-cmd="mysqladmin ping"
          --health-interval=10s
          --health-timeout=5s
          --health-retries=3

    steps:
      - uses: actions/checkout@v4

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.4'
          extensions: mbstring, xml, bcmath, pdo_mysql
          coverage: xdebug

      - name: Cache Composer
        uses: actions/cache@v4
        with:
          path: vendor
          key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}

      - name: Install dependencies
        run: composer install --no-interaction --prefer-dist

      - name: Prepare Laravel
        run: |
          cp .env.example .env
          php artisan key:generate

      - name: Run tests
        env:
          DB_CONNECTION: mysql
          DB_HOST: 127.0.0.1
          DB_PORT: 3306
          DB_DATABASE: testing
          DB_USERNAME: root
          DB_PASSWORD: password
        run: php artisan test --parallel

  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: shivammathur/setup-php@v2
        with:
          php-version: '8.4'
      - run: composer install --no-interaction --prefer-dist
      - run: vendor/bin/pint --test

  deploy:
    needs: [test, lint]
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    steps:
      - name: Deploy via SSH
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: cd /var/www/myapp && bash deploy.sh

      - name: Notify Slack
        if: always()
        uses: 8398a7/action-slack@v3
        with:
          status: ${{ job.status }}
          fields: repo,message,commit,author
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}

Ekstra Ipuclari

Artifact olarak test coverage saklama:

- name: Upload coverage
  uses: actions/upload-artifact@v4
  with:
    name: coverage-report
    path: coverage/

PR'lara otomatik yorum ekleme:

- name: Comment PR
  if: github.event_name == 'pull_request'
  uses: marocchino/sticky-pull-request-comment@v2
  with:
    message: "Testler basariyla gecti. Deploy icin main'e merge edebilirsiniz."

Schedule ile periyodik calistirma:

on:
  schedule:
    - cron: '0 6 * * 1' # Her pazartesi sabah 6'da

Bu pattern'i security audit veya dependency guncelleme kontrolu icin kullaniyorum.

Sonuc

CI/CD pipeline kurmak bir kerelik yatirimdir ve karsiligini her gun alirsiniz. Test'ler otomatik kosulur, kod stili tutarli kalir, deploy sureci tekrarlanabilir olur. Projelerimde bu workflow'u kurdugumdan beri production'da yasadigim sorunlar gozle gorulur sekilde azaldi. Kucuk projeler icin bile en azindan test + lint pipeline'i kurmani tavsiye ederim. Deploy otomasyonunu sonra ekleyebilirsiniz ama test otomasyonunu ilk gunden baslatin.