Москва: проверенные б/у ноутбуки с гарантией и проверкой перед оплатой
+7 906 722-94-45

Главная → Статьи → CI/CD пайплайн для Python-проекта

CI/CD пайплайн для Python-проекта: от коммита до выкатки

CI/CD для Python: этапы пайплайна, кэширование зависимостей, матрица версий, сборка артефакта и безопасная выкатка.

Прямой ответ

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 минут перестают ждать: изменения копят и объединяют большими порциями, что увеличивает риск.

Рекомендации:

  1. Начинайте с малого. Линтер и unit-тесты на каждый коммит дают большую часть пользы. Добавлять этапы стоит по мере появления потребности.
  2. Проверяйте воспроизводимость. Пайплайн, который проходит повторно без изменений кода, нестабилен; такие тесты разбирают, а не перезапускают.
  3. Держите конфигурацию рядом с кодом. Файл пайплайна в репозитории меняется вместе с проектом и проходит ревью.
  4. Логируйте причины падения понятно. Вывод теста должен показывать, что именно не совпало; поиск причины в тысяче строк лога — пустая трата времени.
  5. Измеряйте покрытие изменённого кода. Глобальный процент менее полезен, чем проверка того, что новые строки покрыты тестами.

Практический сценарий: выкатка отличалась от протестированной сборки

Контекст: тесты в CI проходили, но в продакшене периодически возникали ошибки импорта, которых не было в прогоне.

  1. Диагностика. Выяснилось, что образ для продакшена собирался отдельным заданием после слияния, из того же кода, но с другой версией базового образа и без файла блокировки зависимостей.
  2. Причина. Диапазоны версий в pyproject.toml допускали установку более новой версии библиотеки; между прогоном тестов и сборкой образа вышла новая версия с изменением API.
  3. Действия. (а) Ввели файл блокировки и установку строго по нему; (б) сборку образа перенесли в пайплайн тестирования: образ собирается один раз, помечается идентификатором коммита и продвигается по окружениям; (в) выкатку сделали переключением на уже собранный образ; (г) добавили шаг проверки, что выкатываемый образ совпадает по дайджесту с протестированным.
  4. Результат. Расхождения между протестированной и рабочей версией исчезли; откат занимает переключение тега вместо пересборки.
  5. Профилактика. Правило: любой артефакт попадает в окружение только из пайплайна, ручные сборки запрещены.

Вывод: пайплайн ценен не набором проверок, а гарантией, что проверенный артефакт и работающий сервис — одно и то же. Разрыв между сборкой и тестированием обесценивает оба процесса.

FAQ

Нужен ли CD, если выкатки редкие? Автоматизация выкатки окупается уже при нескольких развёртываниях в месяц: ручная выкатка содержит шаги, которые забываются, и не даёт быстрого отката.

Что делать с долгими интеграционными тестами? Выносить их в отдельное задание, запускать параллельно с другими и по возможности сокращать: часть проверок переносится на уровень unit-тестов, часть выполняется по расписанию.

Как организовать окружения? Минимум два: тестовое, куда выкатка выполняется автоматически после слияния, и продакшен, куда выкатка выполняется по явному действию. Промежуточные окружения добавляют по необходимости, а не по умолчанию.

Стоит ли запускать пайплайн на каждый пуш в ветке? Да, если он быстрый. Ранняя обратная связь дешевле разбора большого набора изменений перед слиянием. Если прогон долгий, оптимизируют пайплайн, а не отключают запуск.

Источники: GitHub Actions docs, Python Packaging User Guide, pytest docs — ссылки в шапке.