За 16 лет работы с WordPress перешел путь от ручных FTP-загрузок до полностью автоматизированных пайплайнов. Первый проект с CI/CD настраивал три дня — теперь разворачиваю полноценный pipeline за час. На одном клиентском проекте автоматизация сократила время деплоя с 45 минут до 3 минут, а количество ошибок на продакшене упало до нуля. Сегодня поделюсь проверенной методологией, которая работает на реальных проектах.
Зачем WordPress нужен CI/CD
Проблемы ручного деплоя
На проекте корпоративного сайта клиент обновлял продакшн через FileZilla. Однажды разработчик забыл загрузить обновленный CSS-файл — сайт сломался на 2 часа в пятницу вечером. Убытки составили $15,000 из-за остановки онлайн-заказов.
Типичные проблемы:
- Человеческий фактор (забыли файл, не те права доступа)
- Нет отката при проблемах
- Отсутствие тестирования перед деплоем
- Невозможность отследить, кто и что изменил
- Время простоя при обновлении
Реальная польза автоматизации
После внедрения CI/CD на том же проекте:
До автоматизации:
- Время деплоя: 30-45 минут
- Ошибки на продакшене: 3-5/месяц
- Откат изменений: 15-20 минут
- Тестирование: выборочное, ручное
После автоматизации:
- Время деплоя: 2-3 минуты
- Ошибки на продакшене: 0-1/квартал
- Откат изменений: 1 минута (git revert)
- Тестирование: 100% автоматическое
Архитектура CI/CD пайплайна
Схема полного цикла
┌──────────────────────────────────────────────────┐
│ Developer commits to feature branch │
└────────────────┬─────────────────────────────────┘
│
┌────────────────▼─────────────────────────────────┐
│ GitHub Actions triggered │
│ • Lint PHP (PHPCS) │
│ • Lint JS/CSS (ESLint, Stylelint) │
│ • Run PHPUnit tests │
│ • Build assets (npm run build) │
└────────────────┬─────────────────────────────────┘
│
┌────────────────▼─────────────────────────────────┐
│ Pull Request created │
│ • Code review │
│ • Automated preview deploy │
└────────────────┬─────────────────────────────────┘
│
┌────────────────▼─────────────────────────────────┐
│ Merge to develop branch │
│ • Auto-deploy to staging │
│ • Run E2E tests (Playwright) │
│ • Performance tests (Lighthouse) │
└────────────────┬─────────────────────────────────┘
│
┌────────────────▼─────────────────────────────────┐
│ Merge to main branch │
│ • Create backup │
│ • Deploy to production │
│ • Smoke tests │
│ • Slack/Email notification │
└──────────────────────────────────────────────────┘
GitHub Actions: настройка с нуля
Базовая структура workflow
Начнем с простого workflow для проверки качества кода:
text<code># .github/workflows/code-quality.yml
name: Code Quality Check
on:
pull_request:
branches: [main, develop]
push:
branches: [main, develop]
jobs:
lint-php:
name: PHP Linting (PHPCS)
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
tools: composer, cs2pr
coverage: none
- name: Get Composer cache directory
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache Composer dependencies
uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: ${{ runner.os }}-composer-
- name: Install Composer dependencies
run: composer install --prefer-dist --no-progress --no-suggest --optimize-autoloader
- name: Run PHPCS
run: |
composer require --dev squizlabs/php_codesniffer
composer require --dev wp-coding-standards/wpcs
./vendor/bin/phpcs --config-set installed_paths vendor/wp-coding-standards/wpcs
./vendor/bin/phpcs --standard=WordPress \
--extensions=php \
--report=checkstyle \
wp-content/themes/custom-theme/ \
wp-content/plugins/custom-plugin/ \
| cs2pr
- name: PHP Syntax Check
run: find . -name "*.php" -not -path "./vendor/*" -print0 | xargs -0 -n1 -P4 php -l
lint-js:
name: JavaScript Linting (ESLint)
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run ESLint
run: npm run lint:js
- name: Run Stylelint
run: npm run lint:css
security-check:
name: Security Scan
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Run Composer audit
run: composer audit
- name: Run npm audit
run: npm audit --audit-level=high
- name: PHP Security Checker
uses: symfonycorp/security-checker-action@v5
</code>PHPUnit тестирование с WordPress
Полноценное тестирование WordPress кода требует настройки тестового окружения:
text<code># .github/workflows/phpunit-tests.yml
name: PHPUnit Tests
on:
pull_request:
branches: [main, develop]
push:
branches: [main, develop]
jobs:
phpunit:
name: PHPUnit (PHP ${{ matrix.php }} / WP ${{ matrix.wordpress }})
runs-on: ubuntu-latest
strategy:
matrix:
php: ['8.0', '8.1', '8.2']
wordpress: ['latest', '6.4']
services:
mysql:
image: mysql:8.0
env:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: wordpress_test
ports:
- 3306:3306
options: >-
--health-cmd="mysqladmin ping"
--health-interval=10s
--health-timeout=5s
--health-retries=3
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, mysql, mysqli, pdo_mysql, bcmath, soap, intl, gd, exif, iconv
coverage: none
tools: composer, wp-cli
- name: Install Composer dependencies
run: composer install --prefer-dist --no-progress
- name: Setup WordPress test environment
env:
WP_VERSION: ${{ matrix.wordpress }}
DB_HOST: 127.0.0.1
DB_PORT: ${{ job.services.mysql.ports[3306] }}
run: |
bash bin/install-wp-tests.sh wordpress_test root root $DB_HOST:$DB_PORT $WP_VERSION
- name: Run PHPUnit tests
run: |
vendor/bin/phpunit --configuration phpunit.xml.dist
- name: Upload coverage reports
if: matrix.php == '8.2' && matrix.wordpress == 'latest'
uses: codecov/codecov-action@v3
with:
files: ./coverage.xml
fail_ci_if_error: true
</code>bin/install-wp-tests.sh — скрипт установки WordPress Test Library:
bash<code>#!/usr/bin/env bash
if [ $# -lt 3 ]; then
echo "usage: $0 <db-name> <db-user> <db-pass> [db-host] [wp-version] [skip-database-creation]"
exit 1
fi
DB_NAME=$1
DB_USER=$2
DB_PASS=$3
DB_HOST=${4-localhost}
WP_VERSION=${5-latest}
SKIP_DB_CREATE=${6-false}
TMPDIR=${TMPDIR-/tmp}
TMPDIR=$(echo $TMPDIR | sed -e "s/\/$//")
WP_TESTS_DIR=${WP_TESTS_DIR-$TMPDIR/wordpress-tests-lib}
WP_CORE_DIR=${WP_CORE_DIR-$TMPDIR/wordpress/}
download() {
if [ `which curl` ]; then
curl -s "$1" > "$2";
elif [ `which wget` ]; then
wget -nv -O "$2" "$1"
fi
}
if [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+$ ]]; then
WP_TESTS_TAG="branches/$WP_VERSION"
elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0-9]+ ]]; then
if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then
WP_TESTS_TAG="tags/${WP_VERSION//\.0/}"
else
WP_TESTS_TAG="tags/$WP_VERSION"
fi
elif [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then
WP_TESTS_TAG="trunk"
else
WP_TESTS_TAG="tags/$WP_VERSION"
fi
install_wp() {
if [ -d $WP_CORE_DIR ]; then
return;
fi
mkdir -p $WP_CORE_DIR
if [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then
mkdir -p $TMPDIR/wordpress-trunk
rm -rf $TMPDIR/wordpress-trunk/*
svn export --quiet https://core.svn.wordpress.org/trunk $TMPDIR/wordpress-trunk/wordpress
mv $TMPDIR/wordpress-trunk/wordpress/* $WP_CORE_DIR
else
if [ $WP_VERSION == 'latest' ]; then
local ARCHIVE_NAME='latest'
elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+ ]]; then
local ARCHIVE_NAME="wordpress-$WP_VERSION"
else
local ARCHIVE_NAME="wordpress-${WP_VERSION%??}"
fi
download https://wordpress.org/${ARCHIVE_NAME}.tar.gz $TMPDIR/wordpress.tar.gz
tar --strip-components=1 -zxmf $TMPDIR/wordpress.tar.gz -C $WP_CORE_DIR
fi
download https://raw.github.com/markoheijnen/wp-mysqli/master/db.php $WP_CORE_DIR/wp-content/db.php
}
install_test_suite() {
if [ ! -d $WP_TESTS_DIR ]; then
mkdir -p $WP_TESTS_DIR
rm -rf $WP_TESTS_DIR/{includes,data}
svn export --quiet --ignore-externals https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/includes/ $WP_TESTS_DIR/includes
svn export --quiet --ignore-externals https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/data/ $WP_TESTS_DIR/data
fi
if [ ! -f wp-tests-config.php ]; then
download https://develop.svn.wordpress.org/${WP_TESTS_TAG}/wp-tests-config-sample.php "$WP_TESTS_DIR"/wp-tests-config.php
WP_CORE_DIR_ESCAPED=$(echo $WP_CORE_DIR | sed 's:/:\\/:g')
sed -i "s:dirname( __FILE__ ) . '/src/':'$WP_CORE_DIR_ESCAPED':" "$WP_TESTS_DIR"/wp-tests-config.php
sed -i "s/youremptytestdbnamehere/$DB_NAME/" "$WP_TESTS_DIR"/wp-tests-config.php
sed -i "s/yourusernamehere/$DB_USER/" "$WP_TESTS_DIR"/wp-tests-config.php
sed -i "s/yourpasswordhere/$DB_PASS/" "$WP_TESTS_DIR"/wp-tests-config.php
sed -i "s|localhost|${DB_HOST}|" "$WP_TESTS_DIR"/wp-tests-config.php
fi
}
recreate_db() {
shopt -s nocasematch
if [[ $1 =~ ^(y|yes)$ ]]; then
mysqladmin drop $DB_NAME -h $DB_HOST --user="$DB_USER" --password="$DB_PASS"$EXTRA --silent
create_db
echo "Recreated the database ($DB_NAME)."
else
echo "Leaving the existing database ($DB_NAME) in place."
fi
shopt -u nocasematch
}
create_db() {
mysqladmin create $DB_NAME -h $DB_HOST --user="$DB_USER" --password="$DB_PASS"$EXTRA --silent
}
install_db() {
if [ ${SKIP_DB_CREATE} = "true" ]; then
return 0
fi
RESULT=`mysql -h $DB_HOST --user="$DB_USER" --password="$DB_PASS"$EXTRA --skip-column-names -e "SHOW DATABASES LIKE '$DB_NAME'"`
if [ "$RESULT" == $DB_NAME ]; then
echo "Database $DB_NAME exists"
recreate_db
else
create_db
fi
}
install_wp
install_test_suite
install_db
</code>Docker в CI/CD Pipeline
Production-ready Docker setup
Docker обеспечивает идентичность окружений от разработки до продакшена:
text<code># docker-compose.yml
version: '3.8'
services:
nginx:
image: nginx:alpine
container_name: wp_nginx
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./wordpress:/var/www/html
- ./nginx/conf.d:/etc/nginx/conf.d
- ./nginx/ssl:/etc/nginx/ssl
- ./logs/nginx:/var/log/nginx
depends_on:
- wordpress
networks:
- wp_network
wordpress:
build:
context: ./docker/wordpress
dockerfile: Dockerfile
container_name: wp_app
restart: unless-stopped
environment:
WORDPRESS_DB_HOST: mysql:3306
WORDPRESS_DB_NAME: ${DB_NAME}
WORDPRESS_DB_USER: ${DB_USER}
WORDPRESS_DB_PASSWORD: ${DB_PASSWORD}
WORDPRESS_TABLE_PREFIX: ${DB_PREFIX}
WORDPRESS_DEBUG: ${WP_DEBUG}
PHP_MEMORY_LIMIT: ${PHP_MEMORY_LIMIT}
PHP_MAX_UPLOAD_SIZE: ${PHP_MAX_UPLOAD_SIZE}
volumes:
- ./wordpress:/var/www/html
- ./php/php.ini:/usr/local/etc/php/conf.d/custom.ini
- ./logs/php:/var/log/php
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy
networks:
- wp_network
mysql:
image: mysql:8.0
container_name: wp_mysql
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
MYSQL_DATABASE: ${DB_NAME}
MYSQL_USER: ${DB_USER}
MYSQL_PASSWORD: ${DB_PASSWORD}
volumes:
- mysql_data:/var/lib/mysql
- ./mysql/my.cnf:/etc/mysql/conf.d/custom.cnf
- ./backups/mysql:/backups
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
networks:
- wp_network
redis:
image: redis:alpine
container_name: wp_redis
restart: unless-stopped
command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
networks:
- wp_network
wp-cli:
image: wordpress:cli
container_name: wp_cli
user: "33:33"
depends_on:
- wordpress
- mysql
volumes:
- ./wordpress:/var/www/html
environment:
WORDPRESS_DB_HOST: mysql:3306
WORDPRESS_DB_NAME: ${DB_NAME}
WORDPRESS_DB_USER: ${DB_USER}
WORDPRESS_DB_PASSWORD: ${DB_PASSWORD}
networks:
- wp_network
volumes:
mysql_data:
redis_data:
networks:
wp_network:
driver: bridge
</code>Dockerfile для production WordPress
text<code># docker/wordpress/Dockerfile
FROM wordpress:6.4-php8.2-fpm-alpine
# Установка системных зависимостей
RUN apk add --no-cache \
bash \
git \
vim \
curl \
zip \
unzip \
mysql-client \
imagemagick \
imagemagick-dev
# Установка PHP расширений
RUN apk add --no-cache --virtual .build-deps \
autoconf \
g++ \
make \
&& pecl install imagick redis \
&& docker-php-ext-enable imagick redis \
&& apk del .build-deps
# Дополнительные PHP расширения
RUN docker-php-ext-install \
opcache \
exif \
mysqli \
pdo_mysql
# Установка WP-CLI
RUN curl -O https://raw.githubusercontent.com/wp-cli/wp-cli/v2.9.0/bin/wp-cli.phar \
&& chmod +x wp-cli.phar \
&& mv wp-cli.phar /usr/local/bin/wp
# Установка Composer
COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer
# Оптимизация OPcache для production
RUN { \
echo 'opcache.memory_consumption=256'; \
echo 'opcache.interned_strings_buffer=16'; \
echo 'opcache.max_accelerated_files=10000'; \
echo 'opcache.revalidate_freq=60'; \
echo 'opcache.fast_shutdown=1'; \
echo 'opcache.enable_cli=1'; \
} > /usr/local/etc/php/conf.d/opcache-recommended.ini
# Установка прав доступа
RUN chown -R www-data:www-data /var/www/html
WORKDIR /var/www/html
EXPOSE 9000
CMD ["php-fpm"]
</code>Автоматический деплой с нулевым downtime
GitHub Actions workflow для деплоя
Пайплайн с автоматическим бэкапом и rollback:
text<code># .github/workflows/deploy-production.yml
name: Deploy to Production
on:
push:
branches:
- main
workflow_dispatch:
jobs:
backup:
name: Create Backup
runs-on: ubuntu-latest
steps:
- name: Backup Database
uses: appleboy/ssh-action@v1.0.0
with:
host: ${{ secrets.PROD_HOST }}
username: ${{ secrets.PROD_USER }}
key: ${{ secrets.PROD_SSH_KEY }}
script: |
DATE=$(date +%Y%m%d_%H%M%S)
mkdir -p /backups/pre-deploy
# Database backup
wp db export /backups/pre-deploy/db_${DATE}.sql --path=/var/www/html
# Files backup
tar -czf /backups/pre-deploy/files_${DATE}.tar.gz \
--exclude='/var/www/html/wp-content/cache' \
--exclude='/var/www/html/wp-content/uploads/backups' \
/var/www/html
# Upload to S3
aws s3 cp /backups/pre-deploy/db_${DATE}.sql \
s3://${{ secrets.S3_BACKUP_BUCKET }}/pre-deploy/ \
--region us-east-1
aws s3 cp /backups/pre-deploy/files_${DATE}.tar.gz \
s3://${{ secrets.S3_BACKUP_BUCKET }}/pre-deploy/ \
--region us-east-1
# Keep only last 5 local backups
cd /backups/pre-deploy
ls -t | tail -n +6 | xargs -r rm
- name: Store backup info
id: backup-info
run: |
echo "backup_date=$(date +%Y%m%d_%H%M%S)" >> $GITHUB_OUTPUT
deploy:
name: Deploy Application
needs: backup
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
tools: composer
- name: Install dependencies
run: |
npm ci
composer install --no-dev --optimize-autoloader --prefer-dist
- name: Build assets
run: npm run build
env:
NODE_ENV: production
- name: Create deployment package
run: |
mkdir -p deploy-package
rsync -av --exclude-from='.deployignore' ./ deploy-package/
tar -czf deployment.tar.gz -C deploy-package .
- name: Upload to server
uses: appleboy/scp-action@v0.1.4
with:
host: ${{ secrets.PROD_HOST }}
username: ${{ secrets.PROD_USER }}
key: ${{ secrets.PROD_SSH_KEY }}
source: "deployment.tar.gz"
target: "/tmp/"
- name: Deploy with zero downtime
uses: appleboy/ssh-action@v1.0.0
with:
host: ${{ secrets.PROD_HOST }}
username: ${{ secrets.PROD_USER }}
key: ${{ secrets.PROD_SSH_KEY }}
script: |
set -e
RELEASE_DIR="/var/www/releases/$(date +%Y%m%d_%H%M%S)"
CURRENT_DIR="/var/www/html"
SHARED_DIR="/var/www/shared"
# Создание директории релиза
mkdir -p $RELEASE_DIR
# Распаковка
tar -xzf /tmp/deployment.tar.gz -C $RELEASE_DIR
# Создание symlinks на shared ресурсы
ln -nfs $SHARED_DIR/wp-config.php $RELEASE_DIR/wp-config.php
ln -nfs $SHARED_DIR/wp-content/uploads $RELEASE_DIR/wp-content/uploads
ln -nfs $SHARED_DIR/.env $RELEASE_DIR/.env
# Обновление БД (если нужно)
wp core update-db --path=$RELEASE_DIR --allow-root
# Атомарная смена symlink
ln -nfs $RELEASE_DIR $CURRENT_DIR
# Очистка кэша
wp cache flush --path=$CURRENT_DIR --allow-root
wp rewrite flush --path=$CURRENT_DIR --allow-root
# Перезапуск PHP-FPM
sudo systemctl reload php8.2-fpm
# Удаление старых релизов (оставляем последние 5)
cd /var/www/releases
ls -t | tail -n +6 | xargs -r rm -rf
# Удаление временных файлов
rm -f /tmp/deployment.tar.gz
echo "Deployment completed successfully!"
smoke-test:
name: Smoke Tests
needs: deploy
runs-on: ubuntu-latest
steps:
- name: Check homepage
run: |
HTTP_CODE=$(curl -o /dev/null -s -w "%{http_code}" https://${{ secrets.PROD_DOMAIN }})
if [ $HTTP_CODE -ne 200 ]; then
echo "Homepage returned $HTTP_CODE"
exit 1
fi
- name: Check REST API
run: |
curl -f https://${{ secrets.PROD_DOMAIN }}/wp-json/wp/v2/posts?per_page=1 || exit 1
- name: Check critical pages
run: |
PAGES=("/about" "/contact" "/shop")
for page in "${PAGES[@]}"; do
HTTP_CODE=$(curl -o /dev/null -s -w "%{http_code}" https://${{ secrets.PROD_DOMAIN }}${page})
if [ $HTTP_CODE -ne 200 ]; then
echo "Page ${page} returned $HTTP_CODE"
exit 1
fi
done
notify:
name: Notify Team
needs: [deploy, smoke-test]
runs-on: ubuntu-latest
if: always()
steps:
- name: Send Slack notification
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
text: |
Production deployment ${{ job.status }}
Branch: ${{ github.ref }}
Commit: ${{ github.sha }}
Author: ${{ github.actor }}
webhook_url: ${{ secrets.SLACK_WEBHOOK }}
if: always()
- name: Send email notification
if: failure()
uses: dawidd6/action-send-mail@v3
with:
server_address: ${{ secrets.SMTP_SERVER }}
server_port: ${{ secrets.SMTP_PORT }}
username: ${{ secrets.SMTP_USERNAME }}
password: ${{ secrets.SMTP_PASSWORD }}
subject: "⚠️ Production Deployment Failed"
to: ${{ secrets.ALERT_EMAIL }}
from: "CI/CD <noreply@example.com>"
body: |
Production deployment has failed.
Repository: ${{ github.repository }}
Branch: ${{ github.ref }}
Commit: ${{ github.sha }}
Author: ${{ github.actor }}
Check the GitHub Actions log for details:
${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
</code>Автоматические бэкапы
Стратегия бэкапов 3-2-1
На production проектах использую правило 3-2-1:
- 3 копии данных
- 2 разных типа носителей (локальный диск + облако)
- 1 копия offsite (AWS S3, Google Cloud Storage)
Автоматизация через cron
bash<code>#!/bin/bash
<em># /usr/local/bin/wordpress-backup.sh</em>
set -e
<em># Конфигурация</em>
SITE_PATH="/var/www/html"
BACKUP_DIR="/var/backups/wordpress"
S3_BUCKET="s3://my-wordpress-backups"
RETENTION_DAYS=30
DATE=$(date +%Y%m%d_%H%M%S)
<em># Создание директорий</em>
mkdir -p $BACKUP_DIR/{db,files}
<em># Database backup</em>
wp db export $BACKUP_DIR/db/database_${DATE}.sql \
--path=$SITE_PATH \
--allow-root
<em># Сжатие БД</em>
gzip $BACKUP_DIR/db/database_${DATE}.sql
<em># Files backup (исключая временные файлы)</em>
tar -czf $BACKUP_DIR/files/files_${DATE}.tar.gz \
--exclude='wp-content/cache' \
--exclude='wp-content/uploads/backups' \
--exclude='wp-content/upgrade' \
-C $SITE_PATH .
<em># Отправка в S3</em>
aws s3 sync $BACKUP_DIR/db/ $S3_BUCKET/db/ \
--storage-class STANDARD_IA \
--delete
aws s3 sync $BACKUP_DIR/files/ $S3_BUCKET/files/ \
--storage-class STANDARD_IA \
--delete
<em># Удаление старых локальных бэкапов</em>
find $BACKUP_DIR/db/ -name "*.sql.gz" -mtime +$RETENTION_DAYS -delete
find $BACKUP_DIR/files/ -name "*.tar.gz" -mtime +$RETENTION_DAYS -delete
<em># Проверка целостности последнего бэкапа</em>
LATEST_DB=$(ls -t $BACKUP_DIR/db/*.sql.gz | head -1)
gunzip -t $LATEST_DB
LATEST_FILES=$(ls -t $BACKUP_DIR/files/*.tar.gz | head -1)
tar -tzf $LATEST_FILES > /dev/null
<em># Отправка уведомления</em>
if [ $? -eq 0 ]; then
echo "Backup completed successfully at $(date)" | \
mail -s "WordPress Backup Success" admin@example.com
else
echo "Backup failed at $(date)" | \
mail -s "⚠️ WordPress Backup FAILED" admin@example.com
exit 1
fi
</code>Crontab запись:
bash<code><em># Ежедневный бэкап в 2 AM</em>
0 2 * * * /usr/local/bin/wordpress-backup.sh >> /var/log/wordpress-backup.log 2>&1
<em># Еженедельный полный бэкап в воскресенье</em>
0 3 * * 0 /usr/local/bin/wordpress-full-backup.sh >> /var/log/wordpress-backup.log 2>&1
</code>GitHub Actions для бэкапов
text<code># .github/workflows/scheduled-backup.yml
name: Scheduled Backup
on:
schedule:
# Каждый день в 02:00 UTC
- cron: '0 2 * * *'
workflow_dispatch: # Ручной запуск
jobs:
backup:
name: Create and Upload Backup
runs-on: ubuntu-latest
steps:
- name: Create backup on server
uses: appleboy/ssh-action@v1.0.0
with:
host: ${{ secrets.PROD_HOST }}
username: ${{ secrets.PROD_USER }}
key: ${{ secrets.PROD_SSH_KEY }}
script: |
/usr/local/bin/wordpress-backup.sh
- name: Verify backup uploaded to S3
run: |
DATE=$(date +%Y%m%d)
aws s3 ls s3://${{ secrets.S3_BACKUP_BUCKET }}/db/ \
--region us-east-1 | grep $DATE
- name: Notify on failure
if: failure()
uses: 8398a7/action-slack@v3
with:
status: failure
text: "Scheduled backup failed!"
webhook_url: ${{ secrets.SLACK_WEBHOOK }}
</code>Rollback стратегия
Автоматический откат при проблемах
text<code># .github/workflows/rollback.yml
name: Rollback Deployment
on:
workflow_dispatch:
inputs:
release_version:
description: 'Release version to rollback to (e.g., 20251028_143022)'
required: true
type: string
jobs:
rollback:
name: Rollback to Previous Release
runs-on: ubuntu-latest
steps:
- name: Confirm rollback
run: |
echo "Rolling back to release: ${{ inputs.release_version }}"
echo "This will restore the database and files from backup"
- name: Execute rollback
uses: appleboy/ssh-action@v1.0.0
with:
host: ${{ secrets.PROD_HOST }}
username: ${{ secrets.PROD_USER }}
key: ${{ secrets.PROD_SSH_KEY }}
script: |
set -e
RELEASE_VERSION="${{ inputs.release_version }}"
RELEASE_DIR="/var/www/releases/$RELEASE_VERSION"
CURRENT_DIR="/var/www/html"
BACKUP_DIR="/backups/pre-deploy"
# Проверка существования релиза
if [ ! -d "$RELEASE_DIR" ]; then
echo "Release $RELEASE_VERSION not found!"
exit 1
fi
# Создание бэкапа текущего состояния
DATE=$(date +%Y%m%d_%H%M%S)
wp db export /tmp/pre_rollback_${DATE}.sql \
--path=$CURRENT_DIR \
--allow-root
# Восстановление database из бэкапа
BACKUP_DB=$(find $BACKUP_DIR -name "db_*.sql" -type f | sort -r | head -1)
wp db import $BACKUP_DB \
--path=$CURRENT_DIR \
--allow-root
# Переключение symlink на старый релиз
ln -nfs $RELEASE_DIR $CURRENT_DIR
# Очистка кэша
wp cache flush --path=$CURRENT_DIR --allow-root
# Перезапуск PHP-FPM
sudo systemctl reload php8.2-fpm
echo "Rollback to $RELEASE_VERSION completed!"
- name: Verify rollback
run: |
sleep 10
HTTP_CODE=$(curl -o /dev/null -s -w "%{http_code}" https://${{ secrets.PROD_DOMAIN }})
if [ $HTTP_CODE -ne 200 ]; then
echo "Site returned $HTTP_CODE after rollback!"
exit 1
fi
- name: Notify team
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
text: |
🔄 Rollback completed
Version: ${{ inputs.release_version }}
Triggered by: ${{ github.actor }}
webhook_url: ${{ secrets.SLACK_WEBHOOK }}
if: always()
</code>Мониторинг и алерты
Интеграция с мониторингом
text<code># .github/workflows/monitoring.yml
name: Production Monitoring
on:
schedule:
- cron: '*/15 * * * *' # Каждые 15 минут
workflow_dispatch:
jobs:
health-check:
name: Health Check
runs-on: ubuntu-latest
steps:
- name: Check site availability
run: |
RESPONSE=$(curl -s -o /dev/null -w "%{http_code}:%{time_total}" https://${{ secrets.PROD_DOMAIN }})
HTTP_CODE=$(echo $RESPONSE | cut -d: -f1)
RESPONSE_TIME=$(echo $RESPONSE | cut -d: -f2)
echo "HTTP Code: $HTTP_CODE"
echo "Response Time: ${RESPONSE_TIME}s"
if [ $HTTP_CODE -ne 200 ]; then
echo "Site is down! HTTP $HTTP_CODE"
exit 1
fi
if (( $(echo "$RESPONSE_TIME > 3.0" | bc -l) )); then
echo "Site is slow! Response time: ${RESPONSE_TIME}s"
exit 1
fi
- name: Check database connection
uses: appleboy/ssh-action@v1.0.0
with:
host: ${{ secrets.PROD_HOST }}
username: ${{ secrets.PROD_USER }}
key: ${{ secrets.PROD_SSH_KEY }}
script: |
wp db check --path=/var/www/html --allow-root
- name: Check disk space
uses: appleboy/ssh-action@v1.0.0
with:
host: ${{ secrets.PROD_HOST }}
username: ${{ secrets.PROD_USER }}
key: ${{ secrets.PROD_SSH_KEY }}
script: |
USAGE=$(df -h / | awk 'NR==2 {print $5}' | sed 's/%//')
echo "Disk usage: ${USAGE}%"
if [ $USAGE -gt 80 ]; then
echo "⚠️ Disk usage is over 80%!"
exit 1
fi
- name: Alert on failure
if: failure()
uses: 8398a7/action-slack@v3
with:
status: failure
text: "🚨 Production health check failed!"
webhook_url: ${{ secrets.SLACK_WEBHOOK }}
</code>CI/CD для WordPress в 2025 — это не просто автоматизация деплоя, а комплексная система обеспечения качества, безопасности и стабильности. GitHub Actions бесплатен для публичных репозиториев и дает 2000 минут/месяц для приватных — этого хватает на большинство проектов. Docker гарантирует идентичность окружений, автоматическое тестирование ловит баги до продакшена, а стратегия бэкапов 3-2-1 защищает от катастроф. Начинайте с простого workflow для code quality, добавляйте PHPUnit тесты, настраивайте автоматический деплой на staging, только потом переходите к production. Первый полноценный pipeline настраивается долго, но окупается уже на второй неделе работы.



