GitHub Actions для Laravel-проектов: настраиваем CI/CD от тестов до zero-downtime деплоя — PROG-TIME

GitHub Actions для Laravel-проектов: настраиваем CI/CD от тестов до zero-downtime деплоя

27.05.2026

После того как мы оптимизировали Docker-образы для PHP-приложения, следующий логичный шаг — автоматизировать сборку, тестирование и деплой. В этой статье разберём, как настроить полноценный CI/CD-пайплайн для Laravel-проекта на GitHub Actions: от запуска PHPUnit на каждом пуше до автоматической выкладки на боевой сервер по SSH.

Почему именно GitHub Actions

Если ваш код уже лежит в GitHub, отдельный CI-сервер (Jenkins, GitLab CI, Drone) — это лишний инфраструктурный слой. GitHub Actions встроены в репозиторий, бесплатны для публичных проектов и дают щедрый лимит минут для приватных. Главные плюсы для типичного Laravel-проекта:

  • workflow описывается обычным YAML-файлом рядом с кодом;
  • огромная экосистема готовых actions — от setup-php до деплоя в Kubernetes;
  • матричные сборки позволяют гонять тесты сразу на нескольких версиях PHP;
  • секреты (SSH-ключи, токены) хранятся в настройках репозитория и не утекают в логи.

Структура каталога

Все workflow живут в каталоге .github/workflows/ в корне репозитория. Один файл — один пайплайн. Для Laravel-проекта удобно разделить так:

.github/
└── workflows/
    ├── tests.yml      # запуск PHPUnit и линтеров
    └── deploy.yml     # деплой на production

Workflow для тестов

Базовый пайплайн, который запускается на каждый push и pull_request, ставит зависимости, прогоняет миграции на временной БД и выполняет тесты:

name: Tests

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  phpunit:
    runs-on: ubuntu-latest

    services:
      mysql:
        image: mysql:8.0
        env:
          MYSQL_ROOT_PASSWORD: root
          MYSQL_DATABASE: testing
        ports:
          - 3306:3306
        options: >-
          --health-cmd="mysqladmin ping"
          --health-interval=10s
          --health-timeout=5s
          --health-retries=5

    strategy:
      matrix:
        php: ['8.2', '8.3']

    steps:
      - uses: actions/checkout@v4

      - name: Setup PHP ${{ matrix.php }}
        uses: shivammathur/setup-php@v2
        with:
          php-version: ${{ matrix.php }}
          extensions: mbstring, pdo_mysql, bcmath, redis
          coverage: none

      - name: Cache Composer dependencies
        uses: actions/cache@v4
        with:
          path: vendor
          key: composer-${{ hashFiles('composer.lock') }}

      - name: Install dependencies
        run: composer install --no-interaction --prefer-dist --no-progress

      - name: Prepare Laravel
        run: |
          cp .env.example .env
          php artisan key:generate
          php artisan config:clear

      - name: Run migrations
        env:
          DB_CONNECTION: mysql
          DB_HOST: 127.0.0.1
          DB_PORT: 3306
          DB_DATABASE: testing
          DB_USERNAME: root
          DB_PASSWORD: root
        run: php artisan migrate --force

      - name: Run PHPUnit
        env:
          DB_CONNECTION: mysql
          DB_HOST: 127.0.0.1
          DB_PORT: 3306
          DB_DATABASE: testing
          DB_USERNAME: root
          DB_PASSWORD: root
        run: php artisan test --parallel

Несколько важных моментов:

  • services поднимает реальный MySQL прямо в раннере — никаких SQLite-костылей, тесты работают на той же СУБД, что и продакшен;
  • strategy.matrix прогоняет один и тот же job на разных версиях PHP, чтобы заранее ловить несовместимости;
  • actions/cache экономит 30–60 секунд на установке Composer-зависимостей между запусками;
  • php artisan test —parallel распараллеливает PHPUnit по ядрам раннера — особенно заметно на больших проектах.

Статический анализ и стиль кода

В тот же workflow удобно добавить отдельный job для Pint (Laravel Pint — обёртка вокруг PHP-CS-Fixer) и Larastan (статический анализатор на базе PHPStan):

  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: shivammathur/setup-php@v2
        with:
          php-version: '8.3'
      - run: composer install --no-interaction --prefer-dist
      - run: ./vendor/bin/pint --test
      - run: ./vendor/bin/phpstan analyse --memory-limit=2G

Флаг --test у Pint означает «только проверка, без правки файлов» — именно то, что нужно в CI.

Деплой на сервер по SSH

После того как тесты зелёные, можно автоматически выкатывать изменения на боевой сервер. Самый простой способ — workflow, который срабатывает только на push в main и подключается по SSH:

name: Deploy

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Deploy via SSH
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.SSH_HOST }}
          username: ${{ secrets.SSH_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            cd /var/www/myproject
            git pull origin main
            composer install --no-dev --optimize-autoloader --no-interaction
            php artisan migrate --force
            php artisan config:cache
            php artisan route:cache
            php artisan view:cache
            php artisan queue:restart
            sudo systemctl reload php8.3-fpm

Хост, имя пользователя и приватный ключ хранятся в Settings → Secrets and variables → Actions. В скрипте принципиально важны три вещи:

  • --no-dev --optimize-autoloader у Composer — мы не тащим на продакшен PHPUnit и Pint, а заодно собираем оптимизированный classmap;
  • группа config:cache / route:cache / view:cache сильно ускоряет boot Laravel;
  • queue:restart мягко сигналит воркерам очереди, что нужно перезапуститься и подхватить новый код.

Zero-downtime через символические ссылки

Простой git pull на проде имеет неприятный нюанс: на время composer install и пересборки кэшей пользователи могут ловить 500-е. Чтобы избавиться от простоев, заведите на сервере структуру в стиле Capistrano:

/var/www/myproject/
├── current -> releases/20260527-143000/
├── releases/
│   ├── 20260527-120000/
│   └── 20260527-143000/
└── shared/
    ├── .env
    └── storage/

Workflow клонирует код в новый каталог releases/<timestamp>, прокидывает симлинки на shared/.env и shared/storage, прогоняет миграции, и только в самом конце атомарно переключает current на новую версию командой ln -sfn. Откат до прошлой версии занимает одну команду.

Уведомления в Telegram

Финальный штрих — оповещение в чат при успешном или провалившемся деплое. Добавьте отдельный шаг в конце workflow:

      - name: Notify Telegram
        if: always()
        uses: appleboy/telegram-action@master
        with:
          to: ${{ secrets.TG_CHAT_ID }}
          token: ${{ secrets.TG_BOT_TOKEN }}
          message: |
            Deploy ${{ job.status }}
            Repo: ${{ github.repository }}
            Commit: ${{ github.event.head_commit.message }}
            Author: ${{ github.actor }}

Условие if: always() гарантирует, что уведомление придёт и при падении пайплайна — иначе вы узнаете о сломанном деплое только от пользователей.

Что в итоге

За два YAML-файла мы получили полноценный CI/CD: на каждый PR прогоняются тесты на нескольких версиях PHP, статический анализ и стиль-чекер, а после слияния в main изменения автоматически выкатываются на сервер с уведомлением в Telegram. Дальше пайплайн можно расширять — добавить отдельный stage для staging-окружения, прогон Dusk-тестов в headless-Chrome, сборку Docker-образа и пуш в registry. Но даже базовая версия из этой статьи закрывает 80% болей среднего Laravel-проекта и экономит часы ручной работы каждую неделю.