Прямой ответ
CI/CD-пайплайн для Python-проекта — последовательность автоматических шагов от коммита до работающего сервиса. Его задача: обнаружить проблему как можно раньше и сделать выкатку рутинной операцией.
Типовые этапы:
| Этап | Что делает | Время |
|---|---|---|
| Линтинг и форматирование | ruff, проверка стиля и типов | 10–30 с |
| Статический анализ типов | mypy по аннотированной части кода | 20–60 с |
| Unit-тесты | Быстрые тесты без внешних зависимостей | 30–120 с |
| Интеграционные тесты | Тесты с БД и брокером в контейнерах | 2–8 мин |
| Сборка пакета или образа | wheel, sdist или Docker-образ | 1–3 мин |
| Публикация артефакта | Реестр пакетов или реестр образов | 30–60 с |
| Выкатка | Обновление окружения | 1–10 мин |
Первые четыре этапа выполняются на каждый коммит, остальные — после слияния в основную ветку.
Критичные детали реализации:
1. Фиксация зависимостей. Пайплайн должен устанавливать одни и те же версии при каждом запуске. Практика — файл блокировки (requirements.txt с точными версиями, uv.lock, poetry.lock), а не только объявление диапазонов в pyproject.toml.
2. Кэширование. Установка зависимостей занимает основную часть времени. Кэш по хешу файла блокировки сокращает прогон в разы — подробно в статье про скорость сборки и кэширование.
3. Матрица версий. Если библиотека поддерживает несколько версий Python, тесты прогоняются по матрице. Для приложения обычно достаточно одной целевой версии — о настройке матрицы в статье про GitHub Actions.
4. Разделение уровней тестов. Прогон всех тестов на каждый коммит замедляет обратную связь. Практика — запускать быстрые тесты всегда, тяжёлые по расписанию или после слияния; о распределении в статье про пирамиду тестирования.
name: ci
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: "3.12" }
- name: Install deps
run: |
python -m pip install --upgrade pip
pip install -r requirements.lock.txt
- name: Lint & types
run: |
ruff check src tests
mypy src
- name: Unit tests
run: pytest -m unit --cov=src --cov-report=xml
- name: Integration tests
run: pytest -m integration
Практика: что отличает рабочий пайплайн от декоративного
1. Пайплайн блокирует слияние. Если падение проверок не мешает объединить изменения, он не выполняет свою функцию. Обязательные проверки настраиваются в правилах ветки.
2. Артефакт собирается один раз. Ошибка — собирать образ отдельно для каждого окружения: тогда в продакшен попадает не тот двоичный код, который прошёл тесты. Правильно: собрать один артефакт, проверить и продвигать его по окружениям. О хранении версий — в статье про артефакты сборки.
3. Выкатка отделима от сборки. Возможность выкатить ранее собранный артефакт позволяет откатиться быстро: не пересобирать, а переключиться на предыдущую версию.
4. Секреты не попадают в журнал. Переменные окружения с токенами маскируются, а команды, способные их вывести, проверяются. Утечка секретов в логи пайплайна — частый инцидент.
5. Время прогона — метрика. Пайплайн дольше 15 минут перестают ждать: изменения копят и объединяют большими порциями, что увеличивает риск.
Рекомендации:
- Начинайте с малого. Линтер и unit-тесты на каждый коммит дают большую часть пользы. Добавлять этапы стоит по мере появления потребности.
- Проверяйте воспроизводимость. Пайплайн, который проходит повторно без изменений кода, нестабилен; такие тесты разбирают, а не перезапускают.
- Держите конфигурацию рядом с кодом. Файл пайплайна в репозитории меняется вместе с проектом и проходит ревью.
- Логируйте причины падения понятно. Вывод теста должен показывать, что именно не совпало; поиск причины в тысяче строк лога — пустая трата времени.
- Измеряйте покрытие изменённого кода. Глобальный процент менее полезен, чем проверка того, что новые строки покрыты тестами.
Практический сценарий: выкатка отличалась от протестированной сборки
Контекст: тесты в CI проходили, но в продакшене периодически возникали ошибки импорта, которых не было в прогоне.
- Диагностика. Выяснилось, что образ для продакшена собирался отдельным заданием после слияния, из того же кода, но с другой версией базового образа и без файла блокировки зависимостей.
- Причина. Диапазоны версий в
pyproject.tomlдопускали установку более новой версии библиотеки; между прогоном тестов и сборкой образа вышла новая версия с изменением API. - Действия. (а) Ввели файл блокировки и установку строго по нему; (б) сборку образа перенесли в пайплайн тестирования: образ собирается один раз, помечается идентификатором коммита и продвигается по окружениям; (в) выкатку сделали переключением на уже собранный образ; (г) добавили шаг проверки, что выкатываемый образ совпадает по дайджесту с протестированным.
- Результат. Расхождения между протестированной и рабочей версией исчезли; откат занимает переключение тега вместо пересборки.
- Профилактика. Правило: любой артефакт попадает в окружение только из пайплайна, ручные сборки запрещены.
Вывод: пайплайн ценен не набором проверок, а гарантией, что проверенный артефакт и работающий сервис — одно и то же. Разрыв между сборкой и тестированием обесценивает оба процесса.
FAQ
Нужен ли CD, если выкатки редкие? Автоматизация выкатки окупается уже при нескольких развёртываниях в месяц: ручная выкатка содержит шаги, которые забываются, и не даёт быстрого отката.
Что делать с долгими интеграционными тестами? Выносить их в отдельное задание, запускать параллельно с другими и по возможности сокращать: часть проверок переносится на уровень unit-тестов, часть выполняется по расписанию.
Как организовать окружения? Минимум два: тестовое, куда выкатка выполняется автоматически после слияния, и продакшен, куда выкатка выполняется по явному действию. Промежуточные окружения добавляют по необходимости, а не по умолчанию.
Стоит ли запускать пайплайн на каждый пуш в ветке? Да, если он быстрый. Ранняя обратная связь дешевле разбора большого набора изменений перед слиянием. Если прогон долгий, оптимизируют пайплайн, а не отключают запуск.
Источники: GitHub Actions docs, Python Packaging User Guide, pytest docs — ссылки в шапке.