Битрикс и CI/CD в одном предложении до сих пор вызывают скепсис: «там же половина настроек в базе», «там админка правит файлы», «у нас деплой через FileZilla и так работает». Работает — ровно до вечера пятницы, когда кто-то заливает половину файлов, а вторую половину не успевает.
Полноценный CI/CD для Битрикса действительно строится сложнее, чем для Laravel: часть состояния живёт в базе, часть файлов правится на боевом сервере, а ядро занимает гигабайты. Но базовый уровень — линтер на pull request, автоматический деплой из main и возможность откатиться — настраивается за вечер и закрывает большинство реальных проблем.
Ниже рабочие workflow с оговорками про грабли, на которые я наступал.
Зачем CI/CD для Битрикс
Типичный процесс деплоя Битрикс-сайта:
- Подключиться по SSH/FTP
- Залить файлы
- Надеяться, что ничего не сломалось
- Откатывать вручную, если сломалось
С CI/CD:
git pushв main- Автоматически: линтинг → тесты → деплой
- Откат одной кнопкой
Что настроим: Автоматический деплой при push в main, проверка кода линтером, деплой на dev-сервер из feature-веток, Telegram-уведомления о статусе.
Структура проекта
project/
├── .github/
│ └── workflows/
│ ├── deploy.yml # Деплой на prod
│ ├── deploy-dev.yml # Деплой на dev
│ └── lint.yml # Проверка кода
├── local/
│ ├── php_interface/
│ └── components/
├── composer.json
├── phpcs.xml # Правила линтера
└── .env.exampleШаг 1: Линтинг PHP
Создаём workflow для проверки кода:
# .github/workflows/lint.yml
name: Lint PHP
on:
pull_request:
branches: [main, develop]
push:
branches: [main, develop]
jobs:
phpcs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
tools: composer, cs2pr
- 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 dependencies
run: composer install --no-progress --prefer-dist
- name: Run PHP_CodeSniffer
run: vendor/bin/phpcs --report=checkstyle | cs2prКонфигурация PHP_CodeSniffer
<!-- phpcs.xml -->
<?xml version="1.0"?>
<ruleset name="Bitrix Project">
<description>Coding standard for Bitrix project</description>
<!-- Проверяем только наш код -->
<file>./local</file>
<!-- Исключаем сторонние библиотеки -->
<exclude-pattern>*/vendor/*</exclude-pattern>
<exclude-pattern>*/node_modules/*</exclude-pattern>
<exclude-pattern>*/bitrix/*</exclude-pattern>
<!-- Базовый стандарт PSR-12 -->
<rule ref="PSR12">
<!-- Битрикс использует CamelCase для некоторых методов -->
<exclude name="PSR1.Methods.CamelCapsMethodName"/>
</rule>
<!-- Дополнительные проверки -->
<rule ref="Generic.Files.LineLength">
<properties>
<property name="lineLimit" value="120"/>
<property name="absoluteLineLimit" value="150"/>
</properties>
</rule>
</ruleset>composer.json
{
"require-dev": {
"squizlabs/php_codesniffer": "^3.7"
},
"scripts": {
"lint": "phpcs",
"lint:fix": "phpcbf"
}
}Шаг 2: Деплой на production
# .github/workflows/deploy.yml
name: Deploy to Production
on:
push:
branches: [main]
# Запрещаем параллельные деплои
concurrency:
group: production
cancel-in-progress: false
jobs:
deploy:
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- name: Setup SSH
uses: webfactory/ssh-agent@v0.8.0
with:
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
- name: Add known hosts
# ssh-keyscan принимает тот ключ, который сервер отдаст в момент запуска,
# то есть проверки подлинности хоста фактически нет. Надёжнее хранить
# известный отпечаток в secrets.SSH_KNOWN_HOSTS и писать его в файл
run: |
mkdir -p ~/.ssh
ssh-keyscan -H ${{ secrets.SSH_HOST }} >> ~/.ssh/known_hosts
- name: Deploy via rsync
env:
SSH_HOST: ${{ secrets.SSH_HOST }}
SSH_USER: ${{ secrets.SSH_USER }}
DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }}
run: |
rsync -avz --delete \
--exclude='.git' \
--exclude='.github' \
--exclude='node_modules' \
--exclude='.env' \
--exclude='bitrix/php_interface/dbconn.php' \
--exclude='bitrix/.settings.php' \
--exclude='upload' \
--exclude='bitrix/backup' \
--exclude='bitrix/cache' \
--exclude='bitrix/managed_cache' \
--exclude='bitrix/stack_cache' \
./ $SSH_USER@$SSH_HOST:$DEPLOY_PATH/
- name: Clear Bitrix cache
env:
SSH_HOST: ${{ secrets.SSH_HOST }}
SSH_USER: ${{ secrets.SSH_USER }}
DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }}
run: |
ssh $SSH_USER@$SSH_HOST << 'EOF'
cd $DEPLOY_PATH
rm -rf bitrix/cache/*
rm -rf bitrix/managed_cache/*
rm -rf bitrix/stack_cache/*
echo "Cache cleared"
EOF
- name: Notify Telegram
if: always()
# В проде тег или SHA вместо @master: сторонний action выполняется
# в раннере с доступом к секретам, и @master — это «что автор выложит,
# то и запустится в вашем пайплайне»
uses: appleboy/telegram-action@master
with:
to: ${{ secrets.TELEGRAM_CHAT_ID }}
token: ${{ secrets.TELEGRAM_BOT_TOKEN }}
format: html
message: |
${{ job.status == 'success' && '✅' || '❌' }} <b>Deploy to Production</b>
Status: ${{ job.status }}
Commit: <code>${{ github.sha }}</code>
Author: ${{ github.actor }}
<a href="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}">View logs</a>Внимание на heredoc с закавыченным разделителем. В шаге очистки кэша используется << 'EOF', а кавычки вокруг EOF запрещают подстановку переменных на стороне GitHub Actions. На сервер уезжает буквальная строка cd $DEPLOY_PATH, где $DEPLOY_PATH — переменная удалённого шелла, а её там нет. cd без аргумента отправляет в домашнюю директорию, и дальнейший rm -rf bitrix/cache/* в лучшем случае не делает ничего, а в худшем — работает не там, где вы думаете.
Передавайте путь явно:
run: |
ssh $SSH_USER@$SSH_HOST "cd '$DEPLOY_PATH' && rm -rf bitrix/cache/* bitrix/managed_cache/* bitrix/stack_cache/*"Никогда не деплойте dbconn.php, .settings.php и .env из репозитория. Эти файлы настраиваются на сервере и исключаются из rsync. Список исключений здесь — не формальность, а защита от самой дорогой ошибки в этом workflow: одна строка --delete без нужного --exclude унесёт upload/ со всеми загруженными файлами.
Про --delete и атомарность. rsync льёт файлы прямо в живой сайт: несколько секунд проект работает в состоянии «часть файлов новая, часть старая», и в этот момент возможны фатальные ошибки у пользователей. Для небольшого проекта это приемлемо, для нагруженного — нет. Промышленный вариант: заливать в новую директорию releases/<sha> и переключать симлинк одной атомарной операцией, тогда же становится тривиальным откат — переключить симлинк обратно.
Минимальное улучшение без переделки схемы — флаг --delay-updates: rsync сначала передаст всё во временные файлы и затем переименует их одним пакетом, сократив окно несогласованности.
Шаг 3: Деплой на dev-сервер
Для feature-веток используем отдельный workflow:
# .github/workflows/deploy-dev.yml
name: Deploy to Development
on:
push:
branches:
- develop
- 'feature/**'
concurrency:
group: development
cancel-in-progress: true # Отменяем предыдущий деплой
jobs:
deploy:
runs-on: ubuntu-latest
environment: development
steps:
- uses: actions/checkout@v4
- name: Setup SSH
uses: webfactory/ssh-agent@v0.8.0
with:
ssh-private-key: ${{ secrets.DEV_SSH_PRIVATE_KEY }}
- name: Add known hosts
run: |
mkdir -p ~/.ssh
ssh-keyscan -H ${{ secrets.DEV_SSH_HOST }} >> ~/.ssh/known_hosts
- name: Deploy
env:
SSH_HOST: ${{ secrets.DEV_SSH_HOST }}
SSH_USER: ${{ secrets.DEV_SSH_USER }}
DEPLOY_PATH: ${{ secrets.DEV_DEPLOY_PATH }}
run: |
rsync -avz --delete \
--exclude='.git' \
--exclude='.github' \
--exclude='node_modules' \
--exclude='.env' \
--exclude='bitrix/php_interface/dbconn.php' \
--exclude='bitrix/.settings.php' \
--exclude='upload' \
./ $SSH_USER@$SSH_HOST:$DEPLOY_PATH/
- name: Run migrations
env:
SSH_HOST: ${{ secrets.DEV_SSH_HOST }}
SSH_USER: ${{ secrets.DEV_SSH_USER }}
DEPLOY_PATH: ${{ secrets.DEV_DEPLOY_PATH }}
run: |
ssh $SSH_USER@$SSH_HOST << 'EOF'
cd $DEPLOY_PATH
php local/tools/migrate.php
EOFШаг 4: Настройка секретов
В Settings → Secrets and variables → Actions добавьте:
| Secret | Описание |
|---|---|
SSH_PRIVATE_KEY | Приватный ключ для подключения |
SSH_HOST | IP или домен сервера |
SSH_USER | Пользователь SSH |
DEPLOY_PATH | Путь до корня сайта |
TELEGRAM_BOT_TOKEN | Токен бота для уведомлений |
TELEGRAM_CHAT_ID | ID чата/группы |
Генерация SSH-ключа
# Генерируем ключ без пароля
ssh-keygen -t ed25519 -C "github-actions" -f ~/.ssh/github_actions -N ""
# Копируем публичный ключ на сервер
ssh-copy-id -i ~/.ssh/github_actions.pub user@server
# Приватный ключ добавляем в GitHub Secrets
cat ~/.ssh/github_actionsШаг 5: Защита веток
В Settings → Branches → Add branch protection rule:
Branch name pattern: main
☑ Require a pull request before merging
☑ Require status checks to pass before merging
└── Required checks: "phpcs"
☑ Require branches to be up to date before mergingТеперь нельзя влить в main без прохождения линтера.
Продвинутые сценарии
Деплой базы данных (миграции)
<?php
// local/tools/migrate.php
use Bitrix\Main\Loader;
use Bitrix\Main\Application;
require $_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/prolog_before.php';
$connection = Application::getConnection();
// Миграции хранятся в local/migrations/, а журнал применённых — ВНЕ директории
// деплоя: rsync --delete снесёт любой файл, которого нет в репозитории
$migrationsDir = $_SERVER['DOCUMENT_ROOT'] . '/local/migrations/';
$appliedFile = '/var/lib/bitrix-deploy/migrations.applied';
// Получаем список применённых миграций
$applied = file_exists($appliedFile)
? array_filter(explode("\n", file_get_contents($appliedFile)))
: [];
// Находим новые миграции
$migrations = glob($migrationsDir . '*.sql');
sort($migrations);
foreach ($migrations as $file) {
$name = basename($file);
if (in_array($name, $applied)) {
continue;
}
echo "Applying: $name\n";
$sql = file_get_contents($file);
try {
$connection->executeSqlBatch($sql);
} catch (\Throwable $e) {
// Останавливаемся на первой ошибке: продолжать применять миграции
// поверх неприменившейся — верный способ получить рассинхрон схемы
fwrite(STDERR, "FAILED: {$name}: {$e->getMessage()}\n");
exit(1);
}
file_put_contents($appliedFile, $name . "\n", FILE_APPEND);
echo "Done: $name\n";
}
echo "All migrations applied.\n";Две мины в этом скрипте, которые я перенёс из боевого опыта в комментарии выше.
Первая — журнал применённых миграций лежал внутри директории сайта. Следующий деплой с rsync --delete его удалял (в репозитории такого файла нет), и на очередном запуске все миграции применялись заново. На ALTER TABLE это заканчивается ошибкой, на INSERT — дублями данных. Журнал должен жить вне того, что синхронизируется.
Вторая — отсутствие остановки на ошибке. Без exit(1) пайплайн зеленеет, хотя схема базы осталась на середине пути, и узнаёте вы об этом от пользователей.
Третья, которую я оставляю как есть, но о которой стоит помнить: .sql-файлов недостаточно. Инфоблоки, свойства, настройки модулей и права в Битриксе создаются через API, а не запросами, поэтому реальные миграции — это PHP-классы с up()/down(). Готовые решения (sprint.migration, arrilot/bitrix-migrations) закрывают вопрос и умеют откатываться.
Откат на предыдущую версию
# .github/workflows/rollback.yml
name: Rollback
on:
workflow_dispatch:
inputs:
commit:
description: 'Commit SHA to rollback to'
required: true
jobs:
rollback:
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.commit }}
# ... остальные шаги деплояКэширование composer
- 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 }}
vendor
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-composer-Итоги
Получившийся пайплайн закрывает базовый набор: линтер на pull request, автоматический деплой из main, отдельный контур для feature-веток, уведомления и защита веток.
Что важнее конкретных YAML-файлов — несколько принципов, которые определяют, будет ли этим пайплайном кто-то пользоваться.
Деплой должен быть скучным. Если выкладка занимает десять минут и иногда падает, команда начнёт выкладывать реже и большими кусками — то есть ровно так, как рисковее всего. Быстрый и предсказуемый деплой снижает размер изменения, а размер изменения — главный фактор риска.
Откат должен быть быстрее, чем разбор причины. В момент инцидента нужно вернуть рабочую версию, а не понять, что сломалось. Схема с releases/ и симлинком даёт откат за секунду; workflow_dispatch с чужим SHA — за время полного деплоя, что уже хуже, но всё равно лучше ручного git checkout на боевом сервере.
Конфигурация и данные не ездят с кодом. dbconn.php, .settings.php, upload/ и журнал миграций живут на сервере. Любой файл, который создаётся на боевом окружении и не лежит в репозитории, — кандидат на удаление ближайшим rsync --delete.
Секреты в GitHub Actions — это доступ на боевой сервер. SSH-ключ для деплоя стоит выпускать отдельным, ограничивать по командам через authorized_keys (command=, no-port-forwarding) и не переиспользовать для чего-то ещё. Сторонние actions пинуйте по SHA: они выполняются рядом с этими секретами.
Дальнейшие шаги в порядке отдачи: PHPStan (находит реальные ошибки, а не стиль), автотесты хотя бы на критичные сценарии оформления заказа, атомарный деплой через симлинк, отдельная миграционная система вместо самописных .sql.
Комментарии
Система комментариев скоро будет подключена. А пока вы можете написать мне в Telegram или на email.