Непрерывная интеграция (CI): что это и как работает пайплайн

Непрерывная интеграция (CI): что это и как работает пайплайн Полезное

CI (Continuous Integration, непрерывная интеграция) — это практика разработки, при которой каждый участник команды часто, обычно несколько раз в день, вливает свои изменения в общую основную ветку, а каждое такое изменение автоматически собирается и проверяется тестами. Цель — узнать о конфликте или ошибке через минуты после коммита, а не через недели при «большом слиянии» перед релизом.

Важно развести два смысла слова. CI как практика — это договоренность команды: маленькие изменения, частое слияние, «красную» сборку чинят первой. CI-система (GitLab CI, GitHub Actions, Jenkins) — инструмент, который запускает сборку и тесты. Инструмент без практики дает автоматические тесты, но не непрерывную интеграцию: если ветка живет месяц, конфликт все равно всплывет в конце.

Ниже — как устроен CI-пайплайн, чем CI отличается от CD, какие бывают системы и рабочий пример конфига для GitHub Actions и GitLab CI. Шаги пайплайна прогнаны локально в контейнере python:3.14-slim (Python 3.14.7), вывод взят из реального запуска.

CI, Continuous Delivery и Continuous Deployment

Сокращение CI/CD склеивает три разные практики. Их путают чаще всего, поэтому сначала словарь.

Практика Что автоматизировано Чем заканчивается Кто решает про выкладку в прод
Continuous Integration (CI) Слияние, сборка, тесты на каждый коммит или merge request Проверенный код в основной ветке, часто — собранный артефакт Не относится к CI
Continuous Delivery (CD) Все из CI плюс подготовка релиза и выкладка на тестовые стенды Версия, готовая к выкладке в любой момент Человек нажимает кнопку
Continuous Deployment (тоже CD) Все из Continuous Delivery плюс выкладка в прод Изменение уже у пользователей Никто: прошли проверки — выкатилось

Буква D в CI/CD может означать и delivery, и deployment — в конкретной команде стоит уточнять, о чем речь. CI — фундамент для обоих: без надежных автоматических проверок автоматическая выкладка опасна.

Как работает CI-пайплайн

Пайплайн (pipeline) — это цепочка задач (jobs), которую CI-система запускает на событие в репозитории. Типовой путь одного изменения:

  1. Разработчик делает коммит в свою короткую ветку и отправляет его (git push) или открывает merge request (pull request).
  2. Система контроля версий сообщает о событии CI-системе через вебхук. Старый способ — опрос репозитория по расписанию (polling) — тоже встречается, но дает задержку.
  3. CI-система берет свободный раннер (агент, runner) — машину или контейнер, где будет выполняться работа, — и забирает туда код нужного коммита.
  4. Сборка: установка зависимостей, компиляция, сборка пакета или Docker-образа.
  5. Проверки: линтеры, модульные тесты, при необходимости интеграционные тесты и сканирование зависимостей.
  6. Результат: статус коммита («зеленый» или «красный»), отчет, уведомление автору. Успешная сборка может сохранить артефакт — собранный пакет или образ с номером сборки.
  7. Если настроена защита ветки, merge request нельзя влить, пока проверки не зеленые.

Задачи группируются в стадии (stages): задачи одной стадии обычно идут параллельно, следующая стадия стартует после успеха предыдущей. Это упрощенная модель: в GitLab CI можно связать задачи напрямую через needs, в GitHub Actions задачи по умолчанию вообще параллельны, а порядок задают через needs.

Кроме запуска на коммит, пайплайн запускают по расписанию (ночная сборка с долгими тестами), вручную или по событию из другого проекта. Номер сборки (например, GITHUB_RUN_NUMBER в GitHub Actions или CI_PIPELINE_IID в GitLab) вместе с хешем коммита позволяет потом точно сказать, из какого кода собран артефакт.

Минимальный рабочий пример

Возьмем маленький проект на Python: функция цены со скидкой и тесты к ней на встроенном unittest (без сторонних пакетов, чтобы пример работал в любой CI-системе).

# calc.py
def price_with_discount(price: int, percent: int) -> int:
    """Цена в копейках после скидки percent% (0-100), округление вниз."""
    if not 0 <= percent <= 100:
        raise ValueError("percent must be in 0..100")
    return price * (100 - percent) // 100
# test_calc.py
import unittest

from calc import price_with_discount


class PriceTest(unittest.TestCase):
    def test_discount(self):
        self.assertEqual(price_with_discount(10000, 15), 8500)

    def test_no_discount(self):
        self.assertEqual(price_with_discount(999, 0), 999)

    def test_bad_percent(self):
        with self.assertRaises(ValueError):
            price_with_discount(1000, 150)


if __name__ == "__main__":
    unittest.main()

Пайплайн будет делать ровно то, что разработчик может запустить у себя: проверку, что код компилируется, и тесты. Это главный принцип — команды CI должны воспроизводиться локально.

python -m compileall -q .
python -m unittest -v
test_bad_percent (test_calc.PriceTest.test_bad_percent) ... ok
test_discount (test_calc.PriceTest.test_discount) ... ok
test_no_discount (test_calc.PriceTest.test_no_discount) ... ok

----------------------------------------------------------------------
Ran 3 tests in 0.000s

OK

Конфиг для GitHub Actions

Файл кладется в репозиторий по пути .github/workflows/ci.yml:

name: CI

on:
  push:
    branches: [main]
  pull_request:

permissions:
  contents: read

jobs:
  test:
    runs-on: ubuntu-latest
    timeout-minutes: 10
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-python@v6
        with:
          python-version: "3.14"
      - name: Build (compile check)
        run: python -m compileall -q .
      - name: Unit tests
        run: python -m unittest -v

Разбор по строкам: on — события запуска (push в main и любой pull request); permissions: contents: read — токен задачи получает только право чтения репозитория; runs-on — тип раннера; timeout-minutes не дает зависшему тесту занимать раннер часами; actions/checkout забирает код коммита, actions/setup-python ставит нужную версию Python; дальше идут те же две команды, что и локально.

Тот же пайплайн в GitLab CI

В GitLab конфиг лежит в корне репозитория в файле .gitlab-ci.yml:

stages:
  - build
  - test

default:
  image: python:3.14-slim

workflow:
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

build:
  stage: build
  script:
    - python -m compileall -q .

unit-tests:
  stage: test
  script:
    - python --version
    - python -m unittest -v

Здесь явно заданы две стадии: build, потом test. image — Docker-образ, в котором выполняются команды (для раннера с Docker-исполнителем). Блок workflow: rules запускает пайплайн для merge request и для коммитов в ветку по умолчанию, чтобы не плодить дубли на каждую ветку.

Что происходит, когда тест падает

CI полезна тем, что ловит ошибку до слияния. Допустим, разработчик «упростил» функцию и убрал проверку диапазона:

# calc.py после неудачной правки
def price_with_discount(price: int, percent: int) -> int:
    return price * (100 - percent) // 100

Пайплайн запускает те же тесты и получает:

F..
======================================================================
FAIL: test_bad_percent (test_calc.PriceTest.test_bad_percent)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/app/test_calc.py", line 15, in test_bad_percent
    with self.assertRaises(ValueError):
         ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^
AssertionError: ValueError not raised

----------------------------------------------------------------------
Ran 3 tests in 0.001s

FAILED (failures=1)

Команда python -m unittest завершается с кодом 1. Для CI-системы любой ненулевой код выхода команды означает провал шага: задача становится красной, следующие стадии не запускаются, а при защищенной ветке merge request не вольется. Исправление — вернуть проверку if not 0 <= percent <= 100 и отправить новый коммит; пайплайн перезапустится сам.

Так же работает шаг сборки: файл с синтаксической ошибкой compileall не скомпилирует и вернет код 1, и до тестов дело не дойдет.

Инструменты CI: что выбрать

Выбор обычно определяется тем, где уже лежит код, а не «лучшей» системой.

Система Где живет конфиг Когда брать
GitLab CI/CD .gitlab-ci.yml в репозитории Код в GitLab (облако или свой сервер); раннеры можно ставить у себя
GitHub Actions .github/workflows/*.yml Код на GitHub; много готовых действий (actions) в маркетплейсе
Jenkins Jenkinsfile (Groovy) или настройки в интерфейсе Свой сервер, нестандартные сценарии, много плагинов; требует администрирования
TeamCity, Bitbucket Pipelines и другие Свой формат Когда в компании уже принят этот стек

Принцип везде один: событие в репозитории -> раннер -> команды сборки и проверок -> статус. Перенос пайплайна между системами — это в основном переписывание YAML, если сами команды сборки лежат в скриптах проекта, а не в интерфейсе CI.

Правила, без которых CI не работает

  • Маленькие изменения и короткие ветки. Ветка, которая живет неделями, возвращает ту самую «интеграцию в конце», от которой CI избавляет.
  • Быстрая обратная связь. Частый ориентир — основная проверка укладывается примерно в 10 минут; долгие тесты выносят в отдельную стадию или ночную сборку.
  • Красная основная ветка — приоритет. Пока сборка сломана, новые изменения не вливают, иначе ошибки накладываются друг на друга.
  • Тесты, которым доверяют. Нестабильный («флаки») тест, который падает случайно, приучает нажимать «перезапустить» и пропускать настоящие ошибки.
  • Воспроизводимость. Фиксированные версии языка и зависимостей, одинаковые команды локально и в CI.

Безопасность пайплайна

CI-система исполняет код из репозитория и часто имеет доступ к секретам, поэтому ее настраивают как чувствительную инфраструктуру:

  • пароли и токены — только в защищенных переменных CI-системы (masked/protected в GitLab, secrets в GitHub), не в коде и не в логах;
  • токену задачи — минимальные права (permissions: contents: read в примере выше), права на запись — только там, где они нужны;
  • пайплайны из форков и чужих merge request не должны получать секреты продакшена; в GitHub для событий pull_request из форков секреты по умолчанию не передаются;
  • сторонние actions и образы — из проверенных источников, для чувствительных проектов фиксировать версию по хешу коммита, а не по плавающему тегу;
  • свои раннеры для публичных репозиториев не использовать: чужой код выполнится на вашей машине.

Это базовый набор, а не полная защита: дальше идут изоляция раннеров, аудит и сканирование зависимостей.

Если не получилось

  • Локально тесты зеленые, в CI красные. Сравните версию языка (python --version в логе задачи), переменные окружения и часовой пояс; проверьте, что нужные файлы закоммичены, а не лежат только у вас.
  • Пайплайн не запустился. В GitHub файл должен лежать именно в .github/workflows/, в GitLab — .gitlab-ci.yml в корне; проверьте отступы YAML и условия on / rules.
  • Задача висит в очереди. Нет свободного раннера с подходящими тегами или исчерпаны минуты облачных раннеров.
  • Тест падает через раз. Ищите зависимость от времени, порядка тестов, сети или общего состояния; такой тест чинят, а не перезапускают.

Выводы

  • CI — это практика частого слияния в общую ветку с автоматической сборкой и тестами на каждое изменение; CI-система — только инструмент для нее.
  • Пайплайн: событие в репозитории -> раннер -> сборка -> проверки -> статус; ненулевой код выхода любой команды делает задачу красной.
  • CI заканчивается проверенным кодом; Continuous Delivery доводит до готового релиза, Continuous Deployment — до автоматической выкладки в прод.
  • GitLab CI, GitHub Actions и Jenkins устроены по одному принципу, выбор обычно диктует место хранения кода.
  • Пайплайн имеет доступ к секретам, поэтому минимальные права и защищенные переменные — часть настройки с первого дня.

Где применяется / связь с практикой

Освойте тему на практике

Настройка CI — одна из первых задач DevOps-инженера и частая задача бэкенд-разработчика и автоматизатора тестирования: собрать пайплайн, подключить тесты, дотянуть его до выкладки на стенды и в прод. Системно этому учат на курсе «DevOps практики и инструменты»: от пайплайнов CI/CD до контейнеров и инфраструктуры как кода. Посмотреть формат занятий и разобрать отдельные темы можно на бесплатных открытых уроках Otus.

FAQ

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

Чем ночная сборка отличается от CI?
Ночная сборка (nightly build) запускается раз в сутки по расписанию и обычно гоняет долгие тесты. Она дополняет CI, но не заменяет: ошибка найдется только на следующий день, а не через минуты после коммита.

Можно ли настроить CI для личного проекта бесплатно?
Да, GitHub и GitLab дают бесплатный лимит минут облачных раннеров, а для своих раннеров минуты на сентябрь 2026 года не тарифицируются: GitHub объявлял плату за свои раннеры в приватных репозиториях с марта 2026 года, но отложил ее. Размер лимитов и правила меняются, актуальные значения — на страницах тарифов сервисов.

OTUS Журнал
Скидка 5% 12-19 октября на курсы (popup)
Бесплатные открытые уроки (поп-ап)