Что происходит с вашим кодом до того, как он попадет в продакшен

Вы только что закончили фичу. Запустили локально — работает. Залили код. Что дальше?

Между этим пушем и моментом, когда ваш код выполняется в продакшене, происходит много всего. Не просто сборка артефакта, а проверка того, насколько код безопасен, корректен и поддерживаем. Если пропустить эти проверки, вы играете в рулетку: надеетесь, что каждый разработчик в команде пишет идеальный код каждый раз. Так не бывает.

Процесс автоматических проверок при каждом пуше кода называется непрерывной интеграцией (Continuous Integration, CI). Идея проста: ловить проблемы на ранних стадиях, когда их исправление стоит дешево. Баг, найденный через пять минут после пуша, стоит чашки кофе. Баг, найденный в продакшене, — инцидента, отката и бессонной ночи.

Начинаем с юнит-тестов

Первое, что запускается в большинстве пайплайнов, — юнит-тесты. Но то, что считать юнит-тестом, важнее, чем думает большинство команд.

Хороший юнит-тест не проверяет отдельную функцию или метод просто потому, что они существуют. Он проверяет осмысленное поведение через релевантную точку входа. Для бэкенд-сервиса это обычно API-эндпоинт или use case. Тест вызывает этот эндпоинт и проверяет, соответствует ли ответ ожиданиям. Под капотом запрос проходит через контроллер, сервисный слой, доменную логику и границу репозитория. Внутренний путь приложения выполняется по-настоящему; внешние сервисы — продакшен-базы данных, очереди сообщений, сторонние API — заменяются контролируемыми тестовыми дублерами, локальными тестовыми инстансами, моками или стабами.

У такого подхода есть практическое преимущество: вы можете изменить внутреннюю реализацию функции, не ломая тест. Тесту важно, что делает система, а не как она это делает. Это означает, что вы можете свободно рефакторить, пока поведение остается неизменным.

Юнит-тесты должны быть быстрыми. Если они выполняются дольше нескольких секунд, что-то не так. Из-за скорости их можно запускать на каждый коммит. Это дает разработчикам мгновенную обратную связь: «Ваше изменение что-то сломало, исправляйте сейчас».

Линтинг отлавливает стиль и запахи кода

После юнит-тестов обычно идет линтинг. Линтинг не проверяет логику. Он проверяет, соответствует ли код единым правилам стиля и есть ли паттерны, которые часто приводят к багам.

Линтер ловит такие вещи:

  • Переменная объявлена, но не используется
  • Слишком длинная функция, которую стоит разбить
  • Несогласованные отступы или именование
  • Потенциально опасные паттерны, которые выглядят корректно, но таковыми не являются

Линтинг может казаться мелочью по сравнению с корректностью, но он важнее, чем вы думаете. Код, который выглядит единообразно, легче читать. Код, который легче читать, легче ревьюить. Код, который легче ревьюить, содержит меньше багов. Это цепочка, которая начинается с простой автоматической проверки.

Интеграционные тесты проверяют связи

Юнит-тесты доказывают, что поведение приложения работает, пока внешние соседи контролируются. Интеграционные тесты доказывают, что приложение может работать с реальными зависимостями.

Для бэкенд-сервиса интеграционный тест может поднять реальную тестовую базу данных, запустить сервис и проверить, что API-вызов действительно корректно читает и записывает данные. Или проверить, что фоновый воркер может отправить сообщение в очередь и обработать ответ.

Интеграционные тесты медленнее юнит-тестов. Им нужны реальные зависимости: база данных, очередь, возможно, кеш. Поэтому они запускаются после юнит-тестов, а не до. Если юнит-тест упал, нет смысла запускать интеграционные — изменение уже сломано.

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

Сканирование безопасности ищет уязвимости в вашем коде

Сканирование безопасности проверяет написанный вами код на распространенные уязвимости: SQL-инъекции, межсайтовый скриптинг (XSS), некорректное использование криптографических функций.

Это автоматизированные инструменты, которые ищут известные опасные паттерны. Они не идеальны: могут что-то пропустить и выдать ложные срабатывания. Но они ловят низко висящие фрукты, которые человек-ревьюер может упустить.

Уязвимость, найденная во время CI, стоит тикета и исправления. Уязвимость, найденная в продакшене, стоит взлома, раскрытия данных и потери доверия.

Проверка зависимостей ищет уязвимости в ваших библиотеках

Почти ни один бэкенд-сервис не пишется с нуля. Вы используете фреймворки, библиотеки, пакеты. Каждый из них — это код, написанный кем-то другим, и в этом коде могут быть уязвимости.

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

Эта проверка важна, потому что уязвимости в зависимостях распространены и часто серьезны. Инцидент с Log4j в 2021 году — известный пример: широко используемая библиотека логирования имела уязвимость, позволяющую удаленное выполнение кода. Команды, у которых была проверка зависимостей в пайплайне, узнали об этом быстро. Команды, у которых ее не было, потратили дни на выяснение того, что они используют.

Порядок проверок имеет значение

Последовательность этих проверок не случайна. Она следует практической логике:

  1. Юнит-тесты и линтинг запускаются первыми, потому что они быстрые. Они сразу отсеивают очевидно сломанные или грязные изменения.
  2. Интеграционные тесты идут следующими. Они медленнее, но все еще важны. Если юнит-тесты прошли, а интеграционные упали, вы знаете, что компоненты не работают вместе.
  3. Сканирование безопасности и проверка зависимостей запускаются последними. Они часто самые медленные и реже падают на типичных изменениях.

Такой порядок дает разработчикам быструю обратную связь по распространенным проблемам. Если вы сломали юнит-тест, вы узнаете об этом через секунды. Если вы добавили уязвимую зависимость, вы узнаете об этом через минуты, а не дни.

Диаграмма ниже показывает поток пайплайна и то, какие проверки блокируют сборку.

Вот минимальный GitHub Actions workflow, который реализует этот порядок:

name: CI Pipeline

on: [push]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - run: npm run lint

  unit-test:
    needs: lint
    runs-on: ubuntu-latest
    steps:
      - run: npm test -- --coverage

  integration-test:
    needs: unit-test
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16
    steps:
      - run: npm run test:integration

  security-scan:
    needs: integration-test
    runs-on: ubuntu-latest
    steps:
      - run: npm run audit

  dependency-check:
    needs: security-scan
    runs-on: ubuntu-latest
    steps:
      - run: npm audit --audit-level=high
flowchart TD A[Пуш кода] --> B[Юнит-тесты] B -->|Успех| C[Линтинг] B -->|Провал| X[Блокировка пайплайна] C -->|Успех| D[Интеграционные тесты] C -->|Провал| X D -->|Успех| E[Сканирование безопасности] D -->|Провал| X E -->|Успех| F[Проверка зависимостей] E -->|Предупреждение| F E -->|Провал| X F -->|Успех| G[Сборка артефакта] F -->|Предупреждение| G F -->|Провал| X X --> H[Уведомление разработчика]

Не каждая проверка должна блокировать пайплайн

Некоторые команды запускают все проверки как блокирующие: если любая проверка падает, пайплайн останавливается, и артефакт не собирается. Другие команды позволяют некоторым проверкам проходить с предупреждениями, особенно сканированию безопасности, которое может давать ложные срабатывания.

Важна последовательность. Каждое изменение проходит одни и те же проверки. Качество не зависит от того, вспомнил ли разработчик запустить тесты локально. Оно зависит от пайплайна.

Практический чеклист для вашего CI-пайплайна

Если вы настраиваете или ревьюите CI-пайплайн для бэкенд-сервиса, вот краткий чеклист:

  • Юнит-тесты запускаются на каждый коммит и выполняются менее чем за минуту
  • Линтинг запускается до или одновременно с юнит-тестами
  • Интеграционные тесты запускаются с реальными зависимостями в контролируемом окружении
  • Сканирование безопасности проверяет ваш собственный код на распространенные уязвимости
  • Проверка зависимостей сверяет ваши библиотеки с известными базами данных уязвимостей
  • Пайплайн падает быстро: быстрые проверки идут первыми, медленные — позже
  • Каждое изменение проходит одни и те же проверки, независимо от того, кто его автор

Что дальше

Как только все эти проверки пройдены, артефакт собран и готов. Но сборка артефакта — только половина истории. Следующий вопрос: как разместить новую версию на серверах, не нарушив работу пользователей. Здесь в игру вступают стратегии развертывания, и это мы рассмотрим в следующей главе.

А пока главный вывод: качество вашей продакшен-системы определяется задолго до того, как она попадет в продакшен. Оно определяется в минуты после каждого пуша, когда автоматические проверки решают, безопасен ли ваш код для дальнейшего продвижения. Сделайте эти проверки быстрыми, последовательными и запускайте их каждый раз.