Главная/Статьи/CI/CD для Битрикс-проектов на GitHub Actions

CI/CD для Битрикс-проектов на GitHub Actions

Настраиваем автоматический деплой Битрикс-сайта: линтинг, тесты, деплой на dev/prod через SSH. Пошаговый гайд с рабочими workflow.

ДМ
Дмитрий Мещеряков
📅 17 августа 2026 г.📖 9 мин чтения

Битрикс и CI/CD в одном предложении до сих пор вызывают скепсис: «там же половина настроек в базе», «там админка правит файлы», «у нас деплой через FileZilla и так работает». Работает — ровно до вечера пятницы, когда кто-то заливает половину файлов, а вторую половину не успевает.

Полноценный CI/CD для Битрикса действительно строится сложнее, чем для Laravel: часть состояния живёт в базе, часть файлов правится на боевом сервере, а ядро занимает гигабайты. Но базовый уровень — линтер на pull request, автоматический деплой из main и возможность откатиться — настраивается за вечер и закрывает большинство реальных проблем.

Ниже рабочие workflow с оговорками про грабли, на которые я наступал.

Зачем CI/CD для Битрикс

Типичный процесс деплоя Битрикс-сайта:

  1. Подключиться по SSH/FTP
  2. Залить файлы
  3. Надеяться, что ничего не сломалось
  4. Откатывать вручную, если сломалось

С CI/CD:

  1. git push в main
  2. Автоматически: линтинг → тесты → деплой
  3. Откат одной кнопкой
💡 Совет

Что настроим: Автоматический деплой при push в main, проверка кода линтером, деплой на dev-сервер из feature-веток, Telegram-уведомления о статусе.

Структура проекта

bash
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 для проверки кода:

yaml
# .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

xml
<!-- 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

json
{
    "require-dev": {
        "squizlabs/php_codesniffer": "^3.7"
    },
    "scripts": {
        "lint": "phpcs",
        "lint:fix": "phpcbf"
    }
}

Шаг 2: Деплой на production

yaml
# .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/* в лучшем случае не делает ничего, а в худшем — работает не там, где вы думаете.

Передавайте путь явно:

yaml
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:

yaml
# .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_HOSTIP или домен сервера
SSH_USERПользователь SSH
DEPLOY_PATHПуть до корня сайта
TELEGRAM_BOT_TOKENТокен бота для уведомлений
TELEGRAM_CHAT_IDID чата/группы

Генерация SSH-ключа

bash
# Генерируем ключ без пароля
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:

text
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
<?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) закрывают вопрос и умеют откатываться.

Откат на предыдущую версию

yaml
# .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

yaml
- 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.