GitHub Actions ile WordPress Deployment Otomasyonu
Zamanında bir WordPress sitesini güncellemek için sunucuya SSH ile bağlanıp dosyaları tek tek FTP’ye atmak zorunda kalmış biri olarak söyleyeyim: bu işi elle yapmak hem yorucu hem de hata yapmaya açık. Bir gün production’a yanlış dosyayı attım, sitenin yarısı bozuldu, saatlerce uğraştım. O günden sonra her şeyi otomasyona bırakmaya karar verdim. GitHub Actions bu noktada gerçek bir kurtarıcı oluyor.
Bu yazıda sıfırdan başlayıp production’da çalışan bir WordPress CI/CD pipeline’ı kurmanın tüm adımlarını anlatacağım. Sadece “şu YAML dosyasını kopyala yapıştır” değil, neden böyle yaptığımızı da açıklayacağım.
Neden GitHub Actions?
Jenkins kurarsın, bakım istersin. GitLab CI kullanırsın, ayrı bir platform öğrenirsin. Ama GitHub Actions zaten kodun olduğu yerde. Ayrı bir CI sunucusu kurmak yok, aylık ekstra ücret yok (public repolar için tamamen ücretsiz, private repolar için aylık 2000 dakika ücretsiz geliyor), ekibin alışmak için ciddi zaman harcaması yok.
WordPress deployment için GitHub Actions’ın bize sağladıkları:
- Push tetiklemeli deployment:
mainbranch’e push atınca otomatik deploy - Secrets yönetimi: SSH key, veritabanı şifresi gibi hassas bilgileri güvenle saklama
- Çoklu ortam desteği: Staging ve production için farklı workflow’lar
- Rollback imkanı: Bir şeyler ters gidince önceki versiyona dönme
- Bildirim entegrasyonu: Slack, e-posta veya Telegram’a deployment bildirimi gönderme
Proje Yapısını Hazırlama
Önce bir WordPress projesinin klasik GitHub repository yapısına bakalım. Burada önemli bir nokta: wp-config.php ve uploads/ dizini asla Git’e girmemeli.
wordpress-site/
├── .github/
│ └── workflows/
│ ├── deploy-staging.yml
│ └── deploy-production.yml
├── wp-content/
│ ├── themes/
│ │ └── benim-temam/
│ ├── plugins/
│ │ └── ozel-plugin/
│ └── mu-plugins/
├── .gitignore
├── composer.json
└── README.md
.gitignore dosyası kritik. Yanlış yapılandırılırsa ya çok fazla dosya commit edilir ya da gerekli dosyalar eksik kalır:
# .gitignore icerigi
wp-config.php
wp-config-local.php
.env
.env.*
# WordPress core dosyalari (Composer ile yonetiliyorsa)
/wp-admin/
/wp-includes/
/wp-*.php
/index.php
/xmlrpc.php
/license.txt
/readme.html
# Uploads dizini
wp-content/uploads/
wp-content/upgrade/
wp-content/cache/
wp-content/backup-db/
# Node modulleri
node_modules/
vendor/
# OS dosyalari
.DS_Store
Thumbs.db
# IDE dosyalari
.idea/
.vscode/
*.swp
Sunucu Tarafını Hazırlama
GitHub Actions, sunucunuza bağlanabilmek için SSH erişimine ihtiyaç duyar. Bunun için özel bir deployment kullanıcısı oluşturmanızı şiddetle tavsiye ederim. Root ile deployment yapmak güvenlik açısından kabul edilemez.
# Sunucuda deployment kullanicisi olustur
sudo adduser deployer
sudo usermod -aG www-data deployer
# Deployment dizini olustur ve izinleri ayarla
sudo mkdir -p /var/www/wordpress-site
sudo chown deployer:www-data /var/www/wordpress-site
sudo chmod 2775 /var/www/wordpress-site
# SSH dizinini olustur
sudo -u deployer mkdir -p /home/deployer/.ssh
sudo -u deployer chmod 700 /home/deployer/.ssh
Şimdi GitHub Actions için SSH key çifti oluşturalım. Bunu local makinende yap, sunucuda değil:
# ED25519 key olustur (RSA'dan daha guvenli ve hizli)
ssh-keygen -t ed25519 -C "github-actions-deployer" -f ~/.ssh/github_actions_deployer
# Public key'i sunucuya ekle
ssh-copy-id -i ~/.ssh/github_actions_deployer.pub deployer@sunucu-ip-adresi
# Ya da manuel olarak
cat ~/.ssh/github_actions_deployer.pub | ssh deployer@sunucu-ip-adresi "cat >> ~/.ssh/authorized_keys"
# Private key'i ekrana yazdir - bu GitHub Secrets'a gidecek
cat ~/.ssh/github_actions_deployer
Private key’i kopyalayıp GitHub repository’sine gidiyoruz: Settings > Secrets and variables > Actions > New repository secret yolunu takip ediyoruz.
Eklememiz gereken secret’lar:
- SSH_PRIVATE_KEY: Az önce kopyaladığımız private key
- SSH_HOST: Sunucunun IP adresi veya domain’i
- SSH_USERNAME:
deployer - SSH_PORT: Genellikle 22, değiştirdiyseniz o port
- WP_DB_NAME: Production veritabanı adı
- WP_DB_USER: Veritabanı kullanıcısı
- WP_DB_PASSWORD: Veritabanı şifresi
- SLACK_WEBHOOK_URL: Bildirimler için (opsiyonel)
Temel Deployment Workflow’u
Şimdi asıl kısma geliyoruz. Staging için workflow dosyamızı yazalım:
# .github/workflows/deploy-staging.yml
name: Deploy to Staging
on:
push:
branches:
- develop
workflow_dispatch:
env:
PHP_VERSION: '8.2'
NODE_VERSION: '18'
jobs:
test:
name: Run Tests
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ env.PHP_VERSION }}
extensions: mbstring, intl, gd, zip
coverage: none
- name: Install Composer dependencies
run: composer install --prefer-dist --no-progress --no-dev
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install npm dependencies
run: npm ci
- name: Build assets
run: npm run build
deploy:
name: Deploy to Staging Server
runs-on: ubuntu-latest
needs: test
environment: staging
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ env.PHP_VERSION }}
- name: Install Composer dependencies
run: composer install --prefer-dist --no-progress --no-dev --optimize-autoloader
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- name: Install npm ve build
run: |
npm ci
npm run build
- name: Deploy via SSH
uses: appleboy/[email protected]
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USERNAME }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
port: ${{ secrets.SSH_PORT }}
script: |
cd /var/www/staging-site
git fetch origin develop
git reset --hard origin/develop
composer install --no-dev --optimize-autoloader
wp cache flush --allow-root
echo "Staging deployment tamamlandi: $(date)"
Production Deployment Workflow’u
Production için çok daha dikkatli olmalıyız. Burada backup alma, maintenance mode, rollback mekanizması gibi ekstra adımlar şart:
# .github/workflows/deploy-production.yml
name: Deploy to Production
on:
push:
branches:
- main
tags:
- 'v*.*.*'
workflow_dispatch:
inputs:
reason:
description: 'Deployment nedeni'
required: true
default: 'Manuel deployment'
jobs:
security-check:
name: Security Scan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Check for sensitive data
run: |
# Hassas veri kontrolu
if grep -r "password" --include="*.php" --exclude-dir=vendor . | grep -v "password_hash|wp_check_password|#|//"; then
echo "UYARI: Kod icinde sifre benzeri ifade bulundu!"
exit 1
fi
echo "Guvenlik kontrolu gecti"
build:
name: Build Application
runs-on: ubuntu-latest
needs: security-check
steps:
- uses: actions/checkout@v4
- name: Setup PHP 8.2
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
extensions: mbstring, intl, gd, zip, opcache
- name: Composer install
run: composer install --no-dev --optimize-autoloader --prefer-dist
- name: Node setup ve build
uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
- run: npm ci && npm run build:production
- name: Build artifacts'i sakla
uses: actions/upload-artifact@v4
with:
name: build-${{ github.sha }}
path: |
wp-content/
vendor/
composer.lock
retention-days: 7
deploy-production:
name: Production Deploy
runs-on: ubuntu-latest
needs: build
environment: production
steps:
- uses: actions/checkout@v4
- name: Artifact'lari indir
uses: actions/download-artifact@v4
with:
name: build-${{ github.sha }}
- name: SSH ile production deploy
uses: appleboy/[email protected]
with:
host: ${{ secrets.PROD_SSH_HOST }}
username: ${{ secrets.PROD_SSH_USERNAME }}
key: ${{ secrets.PROD_SSH_PRIVATE_KEY }}
port: ${{ secrets.PROD_SSH_PORT }}
script_stop: true
script: |
set -e
SITE_DIR="/var/www/production-site"
BACKUP_DIR="/var/backups/wordpress/$(date +%Y%m%d_%H%M%S)"
echo "=== Backup aliniyor ==="
mkdir -p $BACKUP_DIR
cp -r $SITE_DIR/wp-content/themes $BACKUP_DIR/
cp -r $SITE_DIR/wp-content/plugins $BACKUP_DIR/
mysqldump -u ${{ secrets.WP_DB_USER }} -p${{ secrets.WP_DB_PASSWORD }}
${{ secrets.WP_DB_NAME }} | gzip > $BACKUP_DIR/database.sql.gz
echo "=== Maintenance mode aktif ==="
wp maintenance-mode activate --path=$SITE_DIR --allow-root
echo "=== Dosyalar guncelleniyor ==="
cd $SITE_DIR
git fetch origin main
git reset --hard origin/main
echo "=== Composer yukleniyor ==="
composer install --no-dev --optimize-autoloader
echo "=== Cache temizleniyor ==="
wp cache flush --path=$SITE_DIR --allow-root
wp rewrite flush --path=$SITE_DIR --allow-root
echo "=== Maintenance mode kapatiliyor ==="
wp maintenance-mode deactivate --path=$SITE_DIR --allow-root
echo "=== Eski backuplar temizleniyor (30 gundan eski) ==="
find /var/backups/wordpress/ -maxdepth 1 -type d -mtime +30 -exec rm -rf {} +
echo "Deployment basarili: $(date)"
echo "Commit: ${{ github.sha }}"
WP-CLI Entegrasyonu
WP-CLI olmadan WordPress otomasyonu düşünülemez. Sunucuya WP-CLI kurulumu:
# WP-CLI kurulumu
curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
chmod +x wp-cli.phar
sudo mv wp-cli.phar /usr/local/bin/wp
# Kurulumu dogrula
wp --info
# Deployment scriptinde yararli WP-CLI komutlari
wp core update-db --path=/var/www/site --allow-root
wp plugin activate --all --path=/var/www/site --allow-root
wp cache flush --path=/var/www/site --allow-root
wp cron event run --due-now --path=/var/www/site --allow-root
wp search-replace 'staging.site.com' 'www.site.com' --path=/var/www/site --allow-root
Rollback Mekanizması
Her şeyin ters gideceğini varsayarak planlama yapmak iyi bir sysadmin’in özelliğidir. Ayrı bir rollback workflow’u:
# .github/workflows/rollback.yml
name: Rollback Production
on:
workflow_dispatch:
inputs:
backup_timestamp:
description: 'Rollback yapilacak backup tarihi (YYYYMMDD_HHMMSS)'
required: true
confirm:
description: 'ROLLBACK yazarak onaylayin'
required: true
jobs:
rollback:
name: Rollback to Previous Version
runs-on: ubuntu-latest
environment: production
steps:
- name: Onay kontrolu
run: |
if [ "${{ github.event.inputs.confirm }}" != "ROLLBACK" ]; then
echo "Onay gecersiz. 'ROLLBACK' yazmaniz gerekiyor."
exit 1
fi
- name: Rollback islemi
uses: appleboy/[email protected]
with:
host: ${{ secrets.PROD_SSH_HOST }}
username: ${{ secrets.PROD_SSH_USERNAME }}
key: ${{ secrets.PROD_SSH_PRIVATE_KEY }}
port: ${{ secrets.PROD_SSH_PORT }}
script: |
set -e
SITE_DIR="/var/www/production-site"
BACKUP_DIR="/var/backups/wordpress/${{ github.event.inputs.backup_timestamp }}"
if [ ! -d "$BACKUP_DIR" ]; then
echo "HATA: Backup dizini bulunamadi: $BACKUP_DIR"
exit 1
fi
echo "=== Maintenance mode aktif ==="
wp maintenance-mode activate --path=$SITE_DIR --allow-root
echo "=== Temalar ve pluginler geri yukleniyor ==="
rsync -av $BACKUP_DIR/themes/ $SITE_DIR/wp-content/themes/
rsync -av $BACKUP_DIR/plugins/ $SITE_DIR/wp-content/plugins/
echo "=== Veritabani geri yukleniyor ==="
gunzip < $BACKUP_DIR/database.sql.gz | mysql -u
${{ secrets.WP_DB_USER }} -p${{ secrets.WP_DB_PASSWORD }}
${{ secrets.WP_DB_NAME }}
echo "=== Cache temizleniyor ==="
wp cache flush --path=$SITE_DIR --allow-root
echo "=== Maintenance mode kapatiliyor ==="
wp maintenance-mode deactivate --path=$SITE_DIR --allow-root
echo "Rollback tamamlandi: $BACKUP_DIR"
Slack Bildirimi Entegrasyonu
Deployment’ın başarılı mı yoksa başarısız mı olduğunu anında öğrenmek için Slack entegrasyonu ekleyelim. Bu adımı production workflow’unun sonuna ekliyoruz:
- name: Basari bildirimi
if: success()
uses: slackapi/[email protected]
with:
payload: |
{
"text": ":rocket: *Production Deployment Basarili!*",
"attachments": [
{
"color": "good",
"fields": [
{
"title": "Proje",
"value": "${{ github.repository }}",
"short": true
},
{
"title": "Branch",
"value": "${{ github.ref_name }}",
"short": true
},
{
"title": "Commit",
"value": "${{ github.sha }}",
"short": false
},
{
"title": "Yapan",
"value": "${{ github.actor }}",
"short": true
}
]
}
]
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
- name: Hata bildirimi
if: failure()
uses: slackapi/[email protected]
with:
payload: |
{
"text": ":fire: *Production Deployment BASARISIZ!*nHemen kontrol edin: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
Sık Karşılaşılan Sorunlar ve Çözümleri
SSH bağlantısı kurulamıyor: En yaygın sorun. Kontrol listesi şöyle:
- SSH_PRIVATE_KEY secret’ına private key’in tamamını yapıştırdın mı?
-----BEGIN OPENSSH PRIVATE KEY-----ve-----END OPENSSH PRIVATE KEY-----dahil. - Sunucudaki
~/.ssh/authorized_keysdosyasında public key doğru mu? ~/.ssh/authorized_keysdosyasının izinleri 600 olmalı:chmod 600 ~/.ssh/authorized_keys- Firewall SSH portunu engelliyor olabilir
wp: command not found hatası: WP-CLI PATH’de olmayabilir. SSH üzerinden çalıştırırken tam path kullanmayı dene: /usr/local/bin/wp veya .bashrc‘yi kaynakla: source ~/.bashrc && wp cache flush
Dosya izin sorunları: Deployment sırasında Apache/Nginx kullanıcısı ile deployer kullanıcısı çakışabilir.
# Sunucuda bu ayarlari yap
sudo chown -R deployer:www-data /var/www/site
sudo find /var/www/site -type d -exec chmod 2775 {} ;
sudo find /var/www/site -type f -exec chmod 664 {} ;
Composer memory limit hatası: GitHub Actions runner’da bazen bellek yetersiz kalabiliyor.
# Composer komutuna memory limit ekle
php -d memory_limit=512M /usr/local/bin/composer install --no-dev
Güvenlik İpuçları
Deployment otomasyonunu kurarken güvenliği ihmal etmek büyük hata olur:
- Secrets rotasyonu: SSH key ve veritabanı şifrelerini düzenli aralıklarla değiştir. 3-6 ayda bir rutin haline getir.
- Environment koruması: GitHub’da production environment için
required reviewersayarla. Bu sayede birinin onayı olmadan production’a deploy yapılamaz. - Branch protection:
mainbranch’e direkt push’u engelle, sadece PR üzerinden merge’e izin ver. - Artifact temizliği:
retention-daysdeğerini makul tut, gereksiz yere build artifact’larını sonsuza kadar saklama. - Deployment kullanıcısı izinleri:
deployerkullanıcısına sadece ihtiyaç duyduğu dizinlere erişim ver, sudo yetkisi verme. - Log takibi: Deployment loglarını düzenli gözden geçir, başarısız denemeleri takip et.
Sonuç
Bu noktaya kadar geldiysen artık elimde bir WordPress deployment pipeline’ın var demektir. Şimdi develop branch’ine push atınca staging’e, main branch’e merge edince production’a otomatik deploy olacak. Bir şeyler ters giderse Slack’ten anında haber alacaksın ve rollback mekanizman hazır bekliyor olacak.
Başta karmaşık görünüyor ama bir kez kurduğunda neden daha önce yapmadığını soruyorsun kendine. Elle FTP ile dosya atmak, SSH’a girip git pull yazmak… bunların hepsi geride kalıyor.
Bir sonraki adım olarak şunları düşünebilirsin: PHP Unit testlerini workflow’a eklemek, staging ortamında otomatik smoke test çalıştırmak, database migration’larını otomatikleştirmek. Ama bunlar ileri seviye konular. Şimdilik bu temeli sağlam kur, üzerine inşa etmesi çok daha kolay olacak.
Workflow’larını yazarken bir şeyi unutma: GitHub Actions logları oldukça ayrıntılı. Bir şey çalışmıyorsa logları dikkatlice oku, çoğunlukla cevap orada gizlidir.
