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 calisirifkosulu ile sadece main branch'e push yapildiginda deploy tetiklenir--no-devflagi production'da dev dependency'leri yuklemez--forceflagi production'da migration onayini atlarqueue:restartile 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 adresiSERVER_USER- SSH kullanici adiSSH_PRIVATE_KEY- SSH ozel anahtarSLACK_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.