В корпоративной веб-разработке ручная доставка обновлений на боевой сервер (по протоколам FTP, SFTP или через прямую замену файлов в консоли) является главным источником аварий (Incidents) и простоев интернет-магазинов.
При отсутствии автоматизированного конвейера непрерывной интеграции и доставки (CI/CD) команды разработчиков регулярно сталкиваются со следующими критическими проблемами:
rsync или FTP перезаписывает рабочую директорию темы, файл functions.php уже обновился и вызывает новый метод класса, а сам файл класса еще находится в процессе передачи по сети. Любой посетитель или поисковый бот, открывший сайт в этот 5-секундный интервал, получает фатальную ошибку Fatal error: Class not found и неработающий интерфейс.echo $_GET['tab'] или прямой SQL-запрос в обход $wpdb->prepare(). В монолитной системе WordPress это открывает мгновенную уязвимость для взлома./dist, минифицированных скриптов и папок node_modules / vendor в систему контроля версий Git приводит к конфликтам слияния (Merge Conflicts) и раздуванию репозитория до сотен мегабайт.Промышленный стандарт решения — построение непрерывного конвейера на базе GitHub Actions с матричным тестированием, строгим линтингом через WordPress Coding Standards (WPCS) и стратегией атомарного деплоя через символические ссылки (Atomic Symlink Deployment).
Никогда не перезаписывайте файлы темы или плагина «на живую» (In-place)!
Профессиональный деплой организуется по паттерну Capistrano / Deployer:
releases/, каталог shared/ (для wp-content/uploads и файла wp-config.php) и симлинк current, указывающий на текущую активную сборку.releases/20260924_120000.ln -sfn releases/20260924_120000 current. Время переключения составляет менее 1 миллисекунды (один системный вызов ядра Linux), исключая «белый экран смерти». При ошибке возврат на предыдущий релиз выполняется обратной сменой симлинка за долю секунды.Конвейер CI/CD делится на две изолированные фазы: Continuous Integration (CI) — проверка качества и сборка на виртуальных машинах GitHub, и Continuous Delivery (CD) — атомарное развертывание на целевом хостинге.
Разработчик пушит коммит в ветку main. GitHub Actions отменяет устаревшие незавершенные сборки через группу concurrency, предотвращая конфликт одновременных деплоев.
Пайплайн запускается на чистом образе ubuntu-latest с изоляцией окружения.
Запускается параллельный статический анализ: PHPCS со стандартами WordPress-Core/Extra/Security, линтинг типов PHPStan (phpstan-wordpress) и ESLint для блоков Gutenberg.
При обнаружении любой уязвимости (например, отсутствия esc_html) процесс немедленно прерывается.
На раннере компилируются фронтенд-ассеты (Vite/Tailwind) в папку dist, а Composer устанавливает только production-зависимости (--no-dev --optimize-autoloader).
Каталог node_modules и файлы тестов не упаковываются в релиз.
Собранный релиз передается через rsync по SSH в каталог releases/$TIMESTAMP. Внутренний скрипт выполняет атомарную подмену симлинка current -> new_release.
Сайт ни на долю секунды не оказывается в промежуточном состоянии.
Через WP-CLI сбрасывается объектный кэш Redis, инициируется сброс байткода OPcache в пуле PHP-FPM, и проверяется HTTP 200 главной страницы. При сбое срабатывает автооткат.
Уведомление со статусом релиза отправляется в Telegram/Slack команды.
Триггер GitHub Actions с блокировкой конкурентных сборок (Concurrency Group)
Параллельный прогон PHPCS (WordPress Coding Standards) и PHPStan Level 6
Vite компилирует JS/CSS в /dist; Composer генерирует оптимизированный Classmap
Передача релиза в /var/www/agenticlab/releases/20260924_153000 по зашифрованному SSH
Атомарный вызов ln -sfn; wp cache flush; триггер сброса OPcache в PHP-FPM
Проверка ответа 200 OK. При ошибке возврат на releases/previous за 100 мс
Почему традиционное копирование файлов темы через команду `rsync -avz --delete ./my-theme/ user@host:/var/www/html/wp-content/themes/my-theme/` приводит к периодическим сбоям 500 Fatal Error на высоконагруженном сайте во время деплоя?
После успешного атомарного деплоя и переключения симлинка current посетители сайта по-прежнему видят старый функционал, а новые методы не вызываются. Почему это происходит и как правильно организовать инвалидацию кэша?
Ниже представлены четыре готовых инфраструктурных компонента, образующих надежный конвейер CI/CD.
Пайплайн запускается на каждый pull_request и push в рабочие ветки: проверяет синтаксис, стандарты WordPress (WPCS), типизацию PHPStan и собирает ассеты.
name: Continuous Integration & Quality Gate
on:
push:
branches: [ "main", "develop" ]
pull_request:
branches: [ "main" ]
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
code-quality:
name: Code Quality & Standards (PHP & JS)
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Setup PHP Environment
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
extensions: mbstring, intl, pdo_mysql, mysqli, zip
tools: composer:v2
coverage: none
- name: Setup Node.js 20
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
cache-dependency-path: 'wp-content/themes/agenticlab-theme/package-lock.json'
- name: Cache Composer Dependencies
uses: actions/cache@v4
with:
path: ~/.composer/cache/files
key: composer-${{ runner.os }}-${{ hashFiles('**/composer.lock') }}
restore-keys: composer-${{ runner.os }}-
- name: Install PHP Dependencies
run: |
cd wp-content/themes/agenticlab-theme
composer install --no-interaction --prefer-dist
- name: Run PHP CodeSniffer (WordPress Standards)
run: |
cd wp-content/themes/agenticlab-theme
./vendor/bin/phpcs --standard=phpcs.xml.dist -s
- name: Run PHPStan (Static Analysis)
run: |
cd wp-content/themes/agenticlab-theme
./vendor/bin/phpstan analyse --configuration=phpstan.neon --memory-limit=1G
- name: Install JS Dependencies & Build Assets
run: |
cd wp-content/themes/agenticlab-theme
npm ci
npm run build
- name: Run Frontend Tests
run: |
cd wp-content/themes/agenticlab-theme
npm test
Строгий набор правил, подключающий официальные сниффы WordPress Core, WordPress Extra (безопасность вывода, nonce-проверки) и проверку совместимости с PHP 8.2+.
<?xml version="1.0"?>
<ruleset name="AgenticLab WordPress Engineering Standards">
<description>Строгие правила качества и безопасности для WordPress тем и плагинов.</description>
<!-- Сканируемые пути проекта -->
<file>.</file>
<!-- Исключаемые каталоги зависимостей и сборщиков -->
<exclude-pattern>*/vendor/*</exclude-pattern>
<exclude-pattern>*/node_modules/*</exclude-pattern>
<exclude-pattern>*/dist/*</exclude-pattern>
<exclude-pattern>*/tests/*</exclude-pattern>
<exclude-pattern>*/assets/build/*</exclude-pattern>
<!-- Вывод подробной информации и названий сниффов при ошибках -->
<arg value="sp"/>
<arg name="colors"/>
<arg name="basepath" value="."/>
<arg name="parallel" value="8"/>
<arg name="extensions" value="php"/>
<!-- 1. Официальные стандарты WordPress: безопасность и чистота кода -->
<rule ref="WordPress-Core"/>
<rule ref="WordPress-Extra"/>
<rule ref="WordPress-Docs"/>
<!-- 2. Проверка экранирования и санитизации данных (Критично против XSS) -->
<rule ref="WordPress.Security.EscapeOutput"/>
<rule ref="WordPress.Security.ValidatedSanitizedInput"/>
<rule ref="WordPress.Security.NonceVerification"/>
<!-- 3. Совместимость с целевой версией PHP 8.2–8.3 -->
<config name="testVersion" value="8.2-8.3"/>
<rule ref="PHPCompatibilityWP"/>
<!-- 4. Настройка префикса хуков и глобальных функций для защиты от коллизий -->
<rule ref="WordPress.NamingConventions.PrefixAllGlobals">
<properties>
<property name="prefixes" type="array">
<element value="agenticlab"/>
<element value="AgenticLab"/>
</property>
</properties>
</rule>
</ruleset>
Пайплайн развертывания на продакшн с контролем параллелизма, генерацией production-архива, передачей через SSH/rsync и вызовом скрипта атомарного переключения.
name: Atomic Production Deployment
on:
push:
branches:
- main
# Запрет одновременных деплоев: новый релиз встает в очередь
concurrency:
group: production-deploy
cancel-in-progress: false
jobs:
deploy:
name: Atomic Release Deploy
runs-on: ubuntu-latest
environment: production
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Setup PHP 8.3
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
extensions: mbstring, zip
tools: composer:v2
- name: Setup Node.js 20
uses: actions/setup-node@v4
with:
node-version: 20
- name: Build Production Assets
run: |
cd wp-content/themes/agenticlab-theme
npm ci
npm run build
- name: Install Production PHP Dependencies
run: |
cd wp-content/themes/agenticlab-theme
composer install --no-dev --optimize-autoloader --no-interaction
- name: Configure SSH Key
uses: webfactory/[email protected]
with:
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
- name: Execute Atomic Deploy Script
env:
SSH_HOST: ${{ secrets.PROD_HOST }}
SSH_USER: ${{ secrets.PROD_USER }}
TARGET_DIR: "/var/www/agenticlab"
RELEASE_ID: ${{ github.run_id }}_${{ github.run_number }}
run: |
# Создание уникального каталога для нового релиза
RELEASE_PATH="${TARGET_DIR}/releases/${RELEASE_ID}"
ssh -o StrictHostKeyChecking=accept-new ${SSH_USER}@${SSH_HOST} "mkdir -p ${RELEASE_PATH}/wp-content/themes/agenticlab-theme"
# Передача собранного кода темы в изолированный каталог
rsync -avz --delete \
--exclude='.git*' \
--exclude='node_modules/' \
--exclude='tests/' \
wp-content/themes/agenticlab-theme/ \
${SSH_USER}@${SSH_HOST}:${RELEASE_PATH}/wp-content/themes/agenticlab-theme/
# Запуск атомарного переключения симлинка и обслуживания кэшей
ssh ${SSH_USER}@${SSH_HOST} "bash ${TARGET_DIR}/scripts/deploy-atomic.sh ${RELEASE_PATH} ${RELEASE_ID}"
- name: Verify Live Production Health
run: |
HEALTH_URL="https://agenticlab.dev"
STATUS=$(curl -s -o /dev/null -w "%{http_code}" ${HEALTH_URL})
if [ "$STATUS" -ne 200 ]; then
echo "❌ CRITICAL: Production returned HTTP $STATUS after deploy!"
exit 1
fi
echo "✅ Production is healthy (HTTP 200 OK)."
Скрипт исполняется непосредственно на хосте: переключает симлинк current, создает ссылки на shared/uploads, сбрасывает кэши, проверяет доступность и автоматически откатывается при ошибке.
#!/usr/bin/env bash
# ==============================================================================
# deploy-atomic.sh — Серверный скрипт атомарного симлинк-переключения релиза
# ==============================================================================
set -euo pipefail
NEW_RELEASE_PATH="${1}"
RELEASE_ID="${2}"
BASE_DIR="/var/www/agenticlab"
SHARED_DIR="${BASE_DIR}/shared"
CURRENT_SYMLINK="${BASE_DIR}/current"
PREVIOUS_RELEASE_FILE="${BASE_DIR}/.previous_release"
echo "🔗 [1/5] Привязка общих разделов (Shared: uploads, wp-config)..."
# Линкуем общую папку uploads внутрь нового релиза
mkdir -p "${NEW_RELEASE_PATH}/wp-content"
ln -sfn "${SHARED_DIR}/uploads" "${NEW_RELEASE_PATH}/wp-content/uploads"
ln -sfn "${SHARED_DIR}/wp-config.php" "${NEW_RELEASE_PATH}/wp-config.php"
# Сохраняем текущий активный релиз для возможности мгновенного отката
if [ -L "${CURRENT_SYMLINK}" ]; then
readlink "${CURRENT_SYMLINK}" > "${PREVIOUS_RELEASE_FILE}"
fi
echo "⚡ [2/5] Атомарная подмена симлинка current -> new_release..."
# ln -sfn создает атомарную замену симлинка без разрыва файловых дескрипторов
ln -sfn "${NEW_RELEASE_PATH}" "${CURRENT_SYMLINK}"
echo "🧹 [3/5] Инвалидация кэшей (WP-CLI & OPcache)..."
# Сброс объектного кэша Redis через WP-CLI
wp cache flush --path="${CURRENT_SYMLINK}"
# Сброс байт-кода OPcache в пуле PHP-FPM
sudo systemctl reload php8.3-fpm
echo "🩺 [4/5] Экспресс-тест жизнеспособности сайта..."
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -H "Host: agenticlab.dev" "http://127.0.0.1/")
if [ "${HTTP_STATUS}" -ne 200 ] && [ "${HTTP_STATUS}" -ne 301 ]; then
echo "🚨 СБОЙ РЕЛИЗА! Сервер вернул HTTP ${HTTP_STATUS}. Инициируем АВТООТКАТ..."
if [ -f "${PREVIOUS_RELEASE_FILE}" ]; then
PREV_PATH=$(cat "${PREVIOUS_RELEASE_FILE}")
ln -sfn "${PREV_PATH}" "${CURRENT_SYMLINK}"
sudo systemctl reload php8.3-fpm
echo "⏪ Успешный откат на предыдущую версию: ${PREV_PATH}"
fi
exit 1
fi
echo "🗑️ [5/5] Ротация старых релизов (сохраняем последние 5 версий)..."
cd "${BASE_DIR}/releases"
ls -1dt */ | tail -n +6 | xargs -r rm -rf
echo "🎉 Релиз ${RELEASE_ID} успешно активирован!"
Class not found или Parse error.releases/, shared/ и атомарной сменой симлинка ln -sfn.sudo systemctl reload php8.3-fpm или скрипт сброса opcache_reset() в каждый деплой.releases/ до исчерпания диска#No space left on device.releases/, старые релизы никогда не удаляются.tail -n +6 | xargs rm -rf).dbDelta.