GitHub Actions на практике: автоматизируем тесты, линтинг и деплой за один вечер
Пишем реальный workflow с нуля: запуск pytest, проверка кода через Ruff и автоматический деплой на сервер при пуше в main. Разбираем структуру YAML-конфига и типичные ошибки.
Содержание
GitHub Actions на практике: автоматизируем тесты, линтинг и деплой за один вечер
Большинство разработчиков знают, что CI/CD — это правильно. Но между «знать» и «настроить рабочий пайплайн» лежит пропасть из документации, непонятных YAML-ошибок и вопроса «а почему оно не задеплоилось?». GitHub Actions закрывает этот разрыв лучше, чем большинство альтернатив: инструмент встроен прямо в репозиторий, бесплатен для публичных проектов и не требует отдельного сервера под Jenkins.
В этой статье мы пройдём весь путь с нуля: напишем workflow, который запускает тесты через pytest, проверяет стиль кода через Ruff и деплоит приложение на сервер при пуше в ветку main. По ходу разберём структуру YAML-конфига и разберём типичные ошибки, которые стоят людям часов отладки.
Что такое GitHub Actions и как это работает
GitHub Actions — это платформа автоматизации, встроенная в GitHub. Вы описываете логику в YAML-файлах, которые лежат в директории .github/workflows/ вашего репозитория. GitHub читает эти файлы и запускает описанные действия на своих (или ваших) серверах в ответ на события: пуш, pull request, расписание, ручной триггер.
Ключевые понятия:
- Workflow — весь файл автоматизации. Один репозиторий может иметь несколько workflow.
- Job — единица работы внутри workflow. Jobs по умолчанию выполняются параллельно, но можно задать зависимости.
- Step — конкретный шаг внутри job: запуск команды или использование готового action.
- Action — переиспользуемый модуль. Например,
actions/checkoutклонирует репозиторий,actions/setup-pythonустанавливает нужную версию Python. - Runner — виртуальная машина, на которой выполняется job. GitHub предоставляет Ubuntu, Windows и macOS.
Важный момент: каждый job запускается в чистом окружении. Файлы, установленные в одном job, недоступны в другом — если только вы явно не передаёте их через артефакты или кэш.
Структура YAML-файла: разбираем по частям
Прежде чем писать конкретный пайплайн, стоит понять анатомию workflow-файла. Вот минимальный скелет:
name: CI Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Run something
run: echo "Hello, CI"
Разберём каждый блок:
name— отображаемое имя workflow в интерфейсе GitHub.on— триггеры. Здесь мы запускаем пайплайн при пуше вmainилиdevelop, а также при создании pull request вmain.jobs— словарь jobs. Ключ (build) — произвольное имя.runs-on— тип runner.ubuntu-latest— самый распространённый выбор.steps— список шагов. Каждый шаг либо использует готовый action (uses), либо выполняет shell-команду (run).
Пишем реальный workflow: тесты, линтинг, деплой
Допустим, у нас есть Python-проект с такой структурой:
my-app/
├── .github/
│ └── workflows/
│ └── ci.yml
├── app/
│ └── main.py
├── tests/
│ └── test_main.py
├── requirements.txt
└── pyproject.toml
Наша цель — один файл ci.yml, который:
- Запускает тесты через pytest.
- Проверяет код через Ruff.
- Деплоит на сервер через SSH, если пуш идёт в
main.
Шаг 1. Тестирование с pytest
name: CI/CD Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
name: Run Tests
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: "pip"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install pytest
- name: Run pytest
run: pytest tests/ -v --tb=short
Несколько важных деталей:
cache: "pip" в setup-python — это встроенное кэширование зависимостей. GitHub кэширует директорию pip-кэша между запусками, и повторная установка пакетов занимает секунды вместо минут. Многие забывают про этот параметр и потом жалуются на медленные пайплайны.
--tb=short в pytest сокращает вывод трейсбека. В CI-логах это важно: полный трейсбек на сотни строк труднее читать.
Если у вас зависимости для тестов вынесены в отдельный файл (например, requirements-dev.txt), установите оба:
- name: Install dependencies
run: |
pip install -r requirements.txt
pip install -r requirements-dev.txt
Шаг 2. Линтинг через Ruff
Ruff — линтер и форматтер для Python, написанный на Rust. Он в десятки раз быстрее flake8 и заменяет сразу несколько инструментов. Добавляем отдельный job:
lint:
name: Lint with Ruff
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: "pip"
- name: Install Ruff
run: pip install ruff
- name: Run Ruff linter
run: ruff check . --output-format=github
- name: Run Ruff formatter check
run: ruff format --check .
--output-format=github — специальный флаг, который форматирует вывод ошибок так, что GitHub автоматически аннотирует их прямо в diff pull request. Это удобно: разработчик видит замечания линтера прямо в коде, а не в логах.
ruff format --check — проверяет форматирование без изменения файлов. Если код не отформатирован, шаг завершится с ненулевым кодом и job упадёт.
Настройки Ruff лучше держать в pyproject.toml:
[tool.ruff]
line-length = 88
target-version = "py312"
[tool.ruff.lint]
select = ["E", "F", "W", "I", "N"]
ignore = ["E501"]
Шаг 3. Деплой на сервер через SSH
Это самая интересная и одновременно самая опасная часть. Деплой запускается только при пуше в main — не при pull request.
Сначала нужно добавить секреты в репозиторий. Идём в Settings → Secrets and variables → Actions и добавляем:
SSH_PRIVATE_KEY— приватный SSH-ключ для подключения к серверу.SSH_HOST— IP-адрес или домен сервера.SSH_USER— пользователь на сервере.SSH_PORT— порт SSH (обычно 22).
Теперь сам job деплоя:
deploy:
name: Deploy to Production
runs-on: ubuntu-latest
needs: [test, lint]
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up SSH
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
ssh-keyscan -p ${{ secrets.SSH_PORT }} ${{ secrets.SSH_HOST }} >> ~/.ssh/known_hosts
- name: Deploy via SSH
run: |
ssh -p ${{ secrets.SSH_PORT }} ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} << 'EOF'
cd /var/www/my-app
git pull origin main
source venv/bin/activate
pip install -r requirements.txt
sudo systemctl restart my-app
EOF
Разберём критически важные детали:
needs: [test, lint] — деплой запустится только если оба предыдущих job завершились успешно. Это гарантирует, что сломанный код не попадёт на продакшн.
if: github.ref == 'refs/heads/main' && github.event_name == 'push' — двойная проверка. github.ref гарантирует, что мы деплоим только из main. github.event_name == 'push' исключает запуск при pull request (даже если PR нацелен на main).
chmod 600 ~/.ssh/id_rsa — обязательный шаг. SSH отказывается использовать ключ с широкими правами доступа.
ssh-keyscan — добавляет fingerprint сервера в known_hosts. Без этого SSH спросит подтверждение при первом подключении, и скрипт зависнет.
Heredoc с << 'EOF' — одинарные кавычки вокруг EOF важны: они предотвращают интерполяцию переменных на стороне GitHub Actions. Все переменные внутри блока будут раскрыты уже на сервере.
Собираем всё вместе
Финальный файл .github/workflows/ci.yml:
name: CI/CD Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
name: Run Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: "pip"
- run: pip install -r requirements.txt pytest
- run: pytest tests/ -v --tb=short
lint:
name: Lint with Ruff
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: "pip"
- run: pip install ruff
- run: ruff check . --output-format=github
- run: ruff format --check .
deploy:
name: Deploy to Production
runs-on: ubuntu-latest
needs: [test, lint]
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
steps:
- uses: actions/checkout@v4
- name: Set up SSH
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
ssh-keyscan -p ${{ secrets.SSH_PORT }} ${{ secrets.SSH_HOST }} >> ~/.ssh/known_hosts
- name: Deploy
run: |
ssh -p ${{ secrets.SSH_PORT }} ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} << 'EOF'
cd /var/www/my-app
git pull origin main
source venv/bin/activate
pip install -r requirements.txt
sudo systemctl restart my-app
EOF
Типичные ошибки и как их избежать
Секреты попадают в логи
Никогда не используйте echo для вывода секретов в логи. GitHub маскирует значения из secrets.*, но если вы присвоите секрет обычной переменной и выведете её — маскировка не сработает. Правило простое: секреты только в secrets.*, никогда в env напрямую без необходимости.
Деплой запускается при PR из форка
Если репозиторий публичный, pull request из форка не имеет доступа к секретам — это защита GitHub. Но если вы неправильно настроили условие if, job деплоя может попытаться запуститься и упасть с непонятной ошибкой. Всегда проверяйте github.event_name.
Кэш pip не работает
Частая причина — requirements.txt генерируется динамически или его нет в корне. Кэш pip привязан к хэшу файла зависимостей. Если файл меняется при каждом запуске, кэш будет инвалидироваться постоянно.
YAML-отступы
YAML чувствителен к отступам. Самая частая ошибка — смешивание табов и пробелов, или неправильное выравнивание вложенных блоков. Используйте линтер для YAML (например, расширение для VS Code) и валидатор вроде actionlint — он проверяет синтаксис GitHub Actions специфично.
Деплой без отката
Простой git pull и перезапуск сервиса — это хорошо для старта, но в продакшне стоит думать об откате. Если pip install упал на середине, сервис может не запуститься. Рассмотрите blue-green деплой или хотя бы проверку статуса после рестарта:
sudo systemctl restart my-app
sleep 3
systemctl is-active --quiet my-app || exit 1
Матрица версий и параллельные тесты
Если нужно тестировать на нескольких версиях Python, GitHub Actions поддерживает матричную стратегию:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: "pip"
- run: pip install -r requirements.txt pytest
- run: pytest tests/ -v
GitHub запустит три параллельных job — по одному на каждую версию. Это бесплатно и занимает столько же времени, сколько один запуск.
Мониторинг и уведомления
Базовый пайплайн работает, но вы хотите знать о падениях. GitHub по умолчанию присылает email при падении workflow. Для Slack или Telegram можно добавить шаг в конце job:
- name: Notify on failure
if: failure()
uses: slackapi/slack-github-action@v1.27.0
with:
payload: |
{
"text": "Pipeline failed in ${{ github.repository }} on branch ${{ github.ref_name }}"
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
if: failure() — условие, которое срабатывает только если предыдущие шаги упали. Это стандартный паттерн для уведомлений об ошибках.
Когда GitHub Actions не хватает
GitHub Actions отлично подходит для большинства проектов, но у него есть ограничения. Бесплатный план даёт 2000 минут в месяц для приватных репозиториев. Сложные пайплайны с долгими тестами быстро исчерпывают лимит. В таких случаях рассматривают self-hosted runners — собственные серверы, зарегистрированные как runner в GitHub.
Также GitHub Actions не идеален для очень сложных пайплайнов с динамической генерацией jobs, условными ветками и сложными зависимостями — там лучше смотреть в сторону Argo Workflows или Tekton. Но для 90% проектов описанного подхода более чем достаточно.
Если вы хотите разобраться в CI/CD системно — не только с GitHub Actions, но и с концепциями pipeline as code, управлением секретами, стратегиями деплоя и мониторингом — стоит посмотреть на структурированные материалы. Один из вариантов — курс «CI-CD от теории до практики», который охватывает эту тему от основ до production-ready конфигураций.
Итог
За один вечер можно получить рабочий пайплайн, который автоматически проверяет код и деплоит его на сервер. Ключевые принципы, которые стоит запомнить:
- Разделяйте jobs по ответственности: тесты, линтинг, деплой — отдельно.
- Используйте
needsдля управления зависимостями между jobs. - Всегда проверяйте условие деплоя через
github.refиgithub.event_name. - Кэшируйте зависимости — это экономит минуты на каждом запуске.
- Храните чувствительные данные только в секретах репозитория.
GitHub Actions — это не магия и не сложная инфраструктура. Это YAML-файл с чёткой логикой. Как только вы понимаете структуру, написать пайплайн под любой проект становится вопросом получаса. Если хочется разобраться глубже — в том числе с более сложными сценариями деплоя и production-практиками — курс «CI-CD от теории до практики» может стать хорошей следующей точкой.
Комментарии
Пока нет комментариев