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



