Автоматизация разработки: CI/CD для WordPress проектов

28 октября 2025
Автоматизация разработки: CI/CD для WordPress проектов

За 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 настраивается долго, но окупается уже на второй неделе работы.

Хотите узнать стоимость сайта?

Обсудим проект и рассчитаем стомость вашего сайта

    Нажимая на кнопку, вы даете согласие на обработку своих персональных данных

    This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.

    Ваша заявка принята!

    Мы перезвоним вам в ближайшее время.