АШШумилкин А. О.ОАиП · ПиТПМ
ПиТПМ · Лекция 09

Лекция №9. Практики непрерывной поддержки интеграции (CI/CD). Автоматический запуск тестов в конвейерах сборки (Build Pipelines).

6 564 слов8 разделов1 иллюстраций
↓ Скачать Markdown

§9.1. Что такое CI/CD и зачем это нужно?

Представьте себе команду разработчиков в 2000-х годах. Они пишут код на своих компьютерах. Каждый работает над своей задачей. Через несколько недель или месяцев они решают, что пора «собрать релиз». Как это выглядело:

1. Разработчики отправляют свой код в общую папку.

2. Начинается ручная сборка проекта (занимает часы).

3. Кто-то запускает тесты вручную (если они вообще есть).

4. Оказывается, что код Ивана не совместим с кодом Петра.

5. Все собираются и два дня разбираются, кто что сломал.

6. Исправляют ошибки, снова собирают, снова тестируют.

7. Через неделю выкатывают релиз.

Проблемы этого подхода:

ПроблемаЧто это значит
Интеграционный адРазработчики пишут код в изоляции, а потом мучительно склеивают его в одно целое
Долгий цикл обратной связиОшибку, допущенную в первый день, находили через месяц
Страх перед релизомКаждый релиз был стрессом — «а вдруг что-то сломается?»
Ручная работаСборка, тестирование, деплой — всё делали руками

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

На смену такому подходу пришел подход CI / CD. Давайте для начала раскроем первую часть данного понятия.

CI расшифровывается как «Continuous Integration»

— непрерывная интеграция. Оно отвечает на вопрос главный: «А не сломал ли я что – нибудь?». Вместо того чтобы накапливать код неделями и мучительно объединять его, разработчики интегрируют (объединяют) свой код в общую ветку каждый день, а часто — несколько раз в день.

Как это работает:

1. Разработчик пишет код для своей задачи.

2. Он делает git push (отправляет код в репозиторий).

3. Автоматически запускается CI-сервер.

4. CI-сервер:

  • Собирает проект (проверяет, что код компилируется).
  • Запускает все тесты (проверяет, что ничего не сломалось).
  • Проверяет стиль кода (не нарушены ли правила оформления).

5. Результат приходит разработчику:

  • ✅ Зеленый — всё хорошо, можно отправлять pull request.
  • ❌ Красный — код сломал сборку или тесты, нужно исправлять.

CI отвечает на два главных вопроса:

  1. «Собирается ли проект?» — не допустили ли мы синтаксических ошибок?
  2. «Работают ли тесты?» — не сломали ли мы существующую функциональность?

Теперь разберем вторую часть понятия CI / CD.

CD расшифровывается как Continuous Delivery (непрерывная доставка) или Continuous Deployment (непрерывное развертывание).

Continuous Delivery (Непрерывная доставка)

  • после того как код успешно прошел CI (собрался и прошел тесты), он автоматически готовится к развертыванию. Но решение о том, когда именно выкатывать на продакшен, принимает человек.

Continuous Deployment (Непрерывное развертывание)

  • код не только готовится к развертыванию, но и автоматически развертывается на продакшен-сервере сразу после успешного прохождения тестов. Никто не нажимает кнопку «Деплой».

Какая разница между CD Delivery и CD Deployment?

РежимContinuous DeliveryContinuous Deployment
Решение о релизеПринимает человекПринимает система
Когда выкатываетсяПо команде (руками)Сразу после успешных тестов
Подходит дляБанковские системы, медицинское ПОСайты, интернет-магазины, мобильные приложения
РискНиже (человек контролирует)Выше (всё автоматически)

Для понимания зачем же вообще нужен CI / CD, есть простая аналогия. Представьте себе автомобильный завод. Есть конвейер, по которому движется машина.

Этап на заводеЭтап в CI/CD
Детали подаются на конвейерРазработчик отправляет код (git push)
Автоматическая сборка кузоваСборка проекта (dotnet build)
Проверка качества сваркиЗапуск модульных тестов
Проверка тормозов и двигателяЗапуск интеграционных тестов
Проверка, как машина выглядитСтатический анализ кода (Linter)
Готовая машина выезжает с конвейераГотовое приложение выкатывается на сервер

Если на этапе проверки качества обнаружен дефект — машина снимается с конвейера. Её не отправляют клиенту.

То же самое в CI/CD: если тесты упали — код не попадает в продакшен.

Что дает CI/CD разработчику:

Что было раньшеЧто стало с CI/CD
«У меня работает» (а у всех остальных — нет)»«Все тесты зеленые» (значит, у всех работает)
Релиз раз в месяц (и каждый раз страшно)Релиз несколько раз в день (и это безопасно)
Исправлять ошибки неделямиОшибка находится в первые минуты после отправки кода
«А кто сломал сборку?» (иголка в стоге сена)«Иван сломал сборку в коммите abc123» (видно сразу)
Ручной деплой на серверАвтоматический деплой по нажатию кнопки

Когда разработчик делает git push, CI/CD делает примерно вот что (упрощенно):

yaml

# Это упрощенный пример, код не обязателен для понимания
steps:
  - name: Скачать код
    run: git clone ...
  
  - name: Восстановить зависимости
    run: dotnet restore
  
  - name: Собрать проект
    run: dotnet build
  
  - name: Запустить тесты
    run: dotnet test

Если любой из этих шагов падает — разработчик видит красный значок в интерфейсе GitHub/GitLab и бежит исправлять свою ошибку.

§9.2. CI/CD Pipeline: что это и из чего состоит?

Pipeline (конвейер)

— это последовательность шагов, которые автоматически выполняются при каждом изменении кода.

Мы уже говорили про аналогию с заводским конвейером. Теперь давайте разберем этот конвейер детально — как он устроен, из каких этапов состоит и что на каждом этапе происходит.

Представьте, что вы отправляете посылку. Она проходит через несколько этапов:

1. Приемка (проверка упаковки). 2. Сортировка (определение пункта назначения). 3. Транспортировка (поездка на самолете). 4. Доставка (курьер везет до двери).

Если на любом этапе возникает проблема — посылка возвращается отправителю или задерживается.

Pipeline в CI/CD работает точно так же. Код проходит через этапы, и если на каком-то этапе происходит сбой — конвейер останавливается и сообщает об ошибке.

Обычно pipeline состоит из четырех основных этапов. Не все проекты используют все этапы, но в большинстве случаев они выглядят так:

Этап №1. Сборка (Build)

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

Для C#:

bash

dotnet restore  # скачиваем библиотеки
dotnet build    # компилируем

Для JavaScript:

bash

npm install     # скачиваем библиотеки
npm run build   # собираем (если нужна компиляция)

Если данный этап упал, то код содержит синтаксические ошибки. Разработчик должен их исправить.

Этап №2. Статический анализ (Lint / Code Quality)

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

  • Неиспользуемые переменные.
  • Слишком сложные методы.
  • Нарушение правил именования.
  • Потенциальные уязвимости безопасности.

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

Примеры инструментов:

  • Для C#: StyleCop, SonarQube.
  • Для JavaScript: ESLint, Prettier.

Если данный этап упал, то код написан не по правилам. Нужно исправить стиль или переписать слишком сложные участки.

Этап №3. Тестирование (Test)

На данном этапе запускаются все виды автоматических тестов. Этот этап часто разбивают на подэтапы:

ПодэтапЧто проверяетИнструменты
Модульные тестыОтдельные методы и классыxUnit, NUnit (C#), Jest (JS)
Интеграционные тестыВзаимодействие с БД, APITestcontainers, WireMock
E2E-тестыПолные сценарии пользователяPlaywright, Selenium

Если данный этап упал, то код сломал существующую функциональность. Нужно найти, какой тест упал, и исправить код.

Этап №4. Развертывание (Deploy)

На данном этапе собранное приложение отправляется на сервер и запускается. Это нужно, чтобы код был доступен реальным пользователям (или тестировщикам). Варианты развертывания следующие:

ВариантЧто происходитКогда используется
Dev-средаРазвертывание на тестовый сервер для разработчиковПосле каждого push
Staging-средаРазвертывание на среду, максимально близкую к боевойПеред релизом
ProductionРазвертывание на боевой серверТолько после одобрения релиза

Если данный этап упал, то приложение не установилось на сервер. Нужно проверить настройки сервера или конфигурацию.

А что запускает конвейер?

Триггер

— это событие, которое запускает конвейер. Без триггера конвейер не начнет работу.

Основные типы триггеров:

1. Push (отправка кода)

Конвейер запускается, когда разработчик отправляет код в репозиторий.

yaml

on:
  push:
    branches:
      - main
      - develop

Данный триггер необходимо использовать для всех изменений в основных ветках.

2. Pull Request (создание запроса на вливание)

Конвейер запускается, когда разработчик создает pull request или обновляет его.

yaml

on:
  pull_request:
    branches:
      - main

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

3. По расписанию (Schedule)

Конвейер запускается по расписанию (например, каждый день в 3:00 ночи).

yaml

on:
  schedule:
    - cron: '0 3 * * *'  # каждый день в 3:00

Это нужно для регулярных проверок (например, запуск нагрузочных тестов по ночам).

4. Вручную (Manual)

Конвейер запускается вручную через интерфейс GitHub/GitLab. Это нужно для развертывания на продакшен (чтобы случайно не выкатить нестабильный код).

Для того, чтобы узнать, как выглядит pipeline, представьте себе ленту конвейера:

wireframe

[Push] → [Сборка] → [Анализ] → [Тесты] → [Развертывание] → [Готово]

   ↑          ↑           ↑          ↑           ↑

  Триггер    Этап 1     Этап 2     Этап 3      Этап 4

Если любой этап красный (упал) — конвейер останавливается:

wireframe

[Push] → [Сборка] → [Анализ] → [Тесты] → ✗ ОШИБКА!

                                    ↑

                               Тесты упали

Разработчик смотрит, какой тест упал, исправляет ошибку и отправляет код заново.

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

ЭтапЧто делаетКак выглядит в коде (упрощенно)
СборкаКомпилирует кодdotnet build
Статический анализПроверяет стильdotnet format --verify
Модульные тестыПроверяет логикуdotnet test --filter "Category=Unit"
Интеграционные тестыПроверяет БДdotnet test --filter "Category=Integration"
E2E-тестыПроверяет UInpx playwright test
РазвертываниеОтправляет на серверscp или kubectl apply

Порядок этапов не случаен. Он построен по принципу «дешевые проверки сначала, дорогие — потом»:

ПорядокПочему
1. СборкаСамая rápida и дешевая проверка. Если код не компилируется — нет смысла запускать тесты.
2. Статический анализТоже быстрый. Проверяет стиль без выполнения кода.
3. ТестыДороже (требуют окружение, БД). Но они находят реальные баги.
4. РазвертываниеСамое дорогое. Делаем только после того, как все проверки прошли.

И это звучит логично, ведь в аэропорту сначала проверяют документы (быстро), потом багаж (медленнее), потом сажают в самолет (самое долгое). Если документы не в порядке — не тратят время на проверку багажа и посадку.

Когда разработчик отправляет код, он видит в GitHub/GitLab что-то похожее:

wireframe

✅ Сборка          (прошла за 12 секунд)

✅ Стат. анализ    (прошла за 5 секунд)

✅ Модульные тесты (прошла за 30 секунд)

✅ Интеграционные  (прошла за 2 минуты)

⏳ E2E-тесты       (выполняется... 3 минуты)

⬜ Развертывание   (ожидает)

Или, если что-то упало:

wireframe

✅ Сборка          (прошла)

✅ Стат. анализ    (прошла)

❌ Модульные тесты (упали!)

Разработчик кликает на красный этап и видит, какой именно тест упал и какая ошибка произошла.

Более подробный интерфейс представлен ниже на изображении.

§9.3. Инструменты CI/CD: GitHub Actions и GitLab CI

GitHub Actions и GitLab CI

— это встроенные системы непрерывной интеграции, которые работают прямо внутри вашего репозитория.

Если у вас есть проект на GitHub — вы можете использовать GitHub Actions. Если на GitLab — GitLab CI. Обе системы делают одно и то же: автоматически собирают, тестируют и развертывают ваш код.

Это как встроенный конвейер прямо на вашем складе. Вам не нужно никуда ехать, ничего подключать — всё уже есть.

Раньше для CI/CD использовали отдельные серверы. Самым популярным был Jenkins.

Как это выглядело с Jenkins:

1. Вы поднимаете отдельный сервер (или виртуальную машину).

2. Устанавливаете на него Jenkins.

3. Настраиваете плагины (для Git, для .NET, для Docker и т.п.).

4. Настраиваете соединение с репозиторием.

5. Пишете скрипты сборки.

6. Поддерживаете этот сервер (обновления, бэкапы, перезапуски).

Проблемы этого подхода:

ПроблемаЧто это значит
Сложно настраиватьJenkins требует много ручной работы
ДорогоНужен отдельный сервер (платить за хостинг)
Нужно поддерживатьСервер нужно обновлять, следить за его работой
Плагины ломаютсяПлагины не всегда совместимы с новыми версиями

Что изменилось с GitHub Actions и GitLab CI:

Что было раньше (Jenkins)Что стало (GitHub/GitLab CI)
Отдельный серверВстроено в репозиторий
Сложная настройкаПростой YAML-файл
Нужно обслуживать серверВсё обслуживает GitHub/GitLab
Плагины могут конфликтоватьВсё работает «из коробки»

Простая аналогия:

  • Jenkins — это как купить машину, собрать ее из деталей, обслуживать, заправлять, страховать.
  • GitHub Actions / GitLab CI — это как каршеринг: машина уже есть, заправлена, застрахована. Вы просто садитесь и едете.

И GitHub Actions, и GitLab CI работают по одному принципу: вы создаете конфигурационный файл, в котором описываете, что должен делать конвейер. Где лежит файл:

ПлатформаПуть к файлу
GitHub Actions.github/workflows/название.yml
GitLab CI.gitlab-ci.yml

Внутри каждого файла вы описываете:

  • Когда запускать конвейер (при push, pull request, по расписанию).
  • Что делать (собрать, протестировать, развернуть).
  • Где делать (на какой операционной системе).

Это как инструкция для робота. Вы пишете: «Когда увидишь новый код — собери его, запусти тесты, если всё хорошо — отправь на сервер».

Пример YAML-файла (упрощенно):

yaml

# Это НЕ обязательно запоминать, просто чтобы увидеть, как выглядит
name: CI
on: [push]  # Триггер: при каждом push

jobs:
  build:
    runs-on: ubuntu-latest  # Где выполнять (Ubuntu)
    steps:
      - name: Собрать проект
        run: dotnet build
      - name: Запустить тесты
        run: dotnet test

А кто выполняет все наши команды?

Runner

— это виртуальная машина (или контейнер), на которой выполняются ваши команды.

Как это работает:

1. Вы отправляете код в репозиторий.

2. GitHub/GitLab находит свободный Runner.

3. Runner запускает виртуальную машину (например, Ubuntu Linux).

4. Runner выполняет все команды из вашего YAML-файла.

5. После завершения Runner уничтожает виртуальную машину.

Кто предоставляет Runner:

ПлатформаОткуда берется Runner
GitHub ActionsGitHub предоставляет бесплатные Runner в облаке
GitLab CIGitLab предоставляет бесплатные Runner в облаке

Какую операционную систему можно использовать:

Операционная системаДля чего подходит
Ubuntu LinuxУниверсальная, быстрая, бесплатная
WindowsДля C# проектов, где нужен Windows
macOSДля iOS-приложений

Бесплатные лимиты:

ПлатформаБесплатные минуты в месяц
GitHub Actions2000 минут (около 33 часов) для бесплатных аккаунтов
GitLab CI400 минут (около 6.5 часов) для бесплатных аккаунтов

Этого достаточно для учебного проекта и большинства небольших проектов. Теперь давайте сравним GitHub Actions и GitLab CI:

ХарактеристикаGitHub ActionsGitLab CI
Где используетсяВ проектах на GitHubВ проектах на GitLab
Файл конфигурации.github/workflows/*.yml.gitlab-ci.yml
Бесплатные минуты2000 мин/месяц400 мин/месяц
ЭкосистемаОгромное количество готовых actionsМного готовых шаблонов
СложностьПрощеСложнее (больше возможностей)

Давайте рассмотрим процесс работы от начала до конца:

Шаг №1. Разработчик пишет код

bash

git add .
git commit -m "Добавил новую функцию"
git push

Шаг №2. GitHub/GitLab видит push и запускает конвейер

Система находит свободный Runner и передает ему ваш YAML-файл.

Шаг №3. Runner выполняет команды

wireframe

✅ Скачал код из репозитория

✅ Восстановил зависимости (dotnet restore)

✅ Собрал проект (dotnet build)

✅ Запустил тесты (dotnet test)

✅ Все тесты зеленые!

Шаг №4. Результат виден в интерфейсе

Разработчик видит зеленый значок ✅ «All checks have passed». Если что-то упало: ❌ Тесты упали (1 тест провалился)

Разработчик видит красный значок ❌ и кликает на него, чтобы увидеть, какой тест упал.

Начинающие разработчики могут задать важный вопрос:

Дополнительные меры безопасности, которые принимаются:

1. Секреты (Secrets). Пароли и токены хранятся в зашифрованном виде.

2. Изоляция. Каждый Runner запускается отдельно.

3. Логи. Все логи доступны только владельцу репозитория.

§9.4. Знакомство с YAML: язык, на котором говорят конвейеры

YAML

— это простой язык для создания конфигурационных файлов.

Главное, что нужно запомнить

«YAML — это не код, а данные».

В YAML вы не пишете программу. Вы описываете структуру данных: «У меня есть такой-то параметр с таким-то значением».

YAML

— это как бланк анкеты. Вы заполняете поля: «Имя: Роман», «Фамилия: Максимов», «Возраст: 23», «Город: Казань», «Место работы – КИТ – КАИ». Вы не пишете программу, а просто заполняете форму.

YAML используется в следующих спецификациях:

Инструмент / ТехнологияДля чего используется
CI/CD (GitHub Actions, GitLab CI)Описание конвейера сборки
Docker ComposeОписание контейнеров
KubernetesОписание развертывания приложений
AnsibleОписание конфигурации серверов
OpenAPI / SwaggerОписание API

Если вы работаете в IT или когда вы будете работать в IT — вы будете встречать YAML постоянно. Это стандарт индустрии.

YAML очень простой. Нужно запомнить всего три правила.

Правило №1. Всё решают отступы (пробелы)

В YAML структура данных определяется отступами. Это самый важный и самый странный для новичков момент. Как это выглядит:

yaml

# Так правильно (пробелы показывают вложенность)
имя: Иван
адрес:
  город: Москва
  улица: Тверская
  дом: 10

Что здесь происходит:

  • имя — параметр верхнего уровня.
  • адрес — параметр верхнего уровня, который содержит внутри себя три других параметра.
  • город, улица, дом — параметры, которые находятся внутри адрес. Они сдвинуты на 2 пробела вправо.

Важные правила про отступы:

ПравилоПример
Используйте пробелы, НЕ табуляцию␣␣город: Москва (пробелы)
Количество пробелов должно быть одинаковымВсегда 2 пробела (или 4) — одинаково
Отступ показывает вложенностьЧем больше отступ — тем глубже уровень

Давайте рассмотрим простейшую аналогию. Представьте, что вы пишете список покупок. Вы пишете:

yaml

Продукты:
  Молочные:
    Молоко
    Сыр
  Мясные:
    Курица
    Говядина

Отступы показывают, что «Молоко» и «Сыр» относятся к «Молочным», а «Курица» и «Говядина» — к «Мясным».

Правило №2. Словари (ключ: значение)

Словарь

— это набор пар «ключ: значение».

Это основа YAML. Как выглядит словарь:

yaml

# Словарь с несколькими парами
имя: Иван
возраст: 25
город: Москва

Что здесь происходит:

  • имя — ключ.
  • Иван — значение.
  • Двоеточие и пробел разделяют ключ и значение.

yaml

Словарь внутри словаря (вложенность):
пользователь:
  имя: Иван
  возраст: 25
  адрес:
    город: Москва
    улица: Тверская

Сравнение с JSON (для понимания):

JSONYAML
{ "имя": "Иван", "возраст": 25 }имя: Иван
возраст: 25
{ "пользователь": { "имя": "Иван" } }пользователь:
␣␣имя: Иван

YAML читается как обычный текст, без лишних скобок и кавычек.

Правило №3. Списки (массивы)

Список

— это набор элементов в определенном порядке.

Как выглядит список:

yaml

# Список продуктов
продукты:
  - Молоко
  - Сыр
  - Хлеб

Что здесь происходит:

  • продукты: — ключ.
  • - Молоко — элемент списка.
  • Дефис и пробел показывают элемент списка.

Список внутри словаря:

yaml

пользователь:
  имя: Иван
  заказы:
    - номер: 1
      сумма: 1000
    - номер: 2
      сумма: 2000

Сравнение с JSON:

JSONYAML
["Молоко", "Сыр", "Хлеб"]- Молоко
- Сыр
- Хлеб
[{"номер": 1}, {"номер": 2}]- номер: 1
- номер: 2

А что же проще, JSON или YAML? Давайте рассмотрим их характеристики:

ХарактеристикаJSONYAML
ЧитаемостьСложно (много скобок и запятых)Просто (как обычный текст)
РазмерБольше (лишние символы)Меньше (нет лишних скобок)
Для людейТрудно читатьЛегко читать
Для машинЛегко парситьТоже легко
Где используетсяAPI, передача данныхКонфигурационные файлы

Пример одного и того же в JSON и YAML. :

{
  "имя": "Иван",
  "возраст": 25,
  "адрес": {
    "город": "Москва",
    "улица": "Тверская"
  },
  "хобби": ["футбол", "музыка"]
}

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

Теперь давайте посмотрим, как выглядит реальный файл для GitHub Actions.

Файл .github/workflows/build.yml:

yaml

# Имя конвейера (отображается в интерфейсе)
name: Build and Test

# Триггер: когда запускать
on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]
# Jobs: что делать
jobs:
  # Job с именем build
  build:
    # На какой ОС запускать
    runs-on: ubuntu-latest
    # Steps: шаги внутри job
    steps:
      # Шаг 1: скачать код
      - name: Checkout code
        uses: actions/checkout@v3
      # Шаг 2: установить .NET SDK
      - name: Setup .NET
        uses: actions/setup-dotnet@v3
        with:
          dotnet-version: '8.0'      
      # Шаг 3: восстановить зависимости
      - name: Restore dependencies
        run: dotnet restore      
      # Шаг 4: собрать проект
      - name: Build
        run: dotnet build --no-restore      
      # Шаг 5: запустить тесты
      - name: Run tests
        run: dotnet test --no-build --verbosity normal

Теперь давайте разберем данный код по частям:

ЧастьЧто делаетСинтаксис
name:Имя конвейераСтрока
on:ТриггерыСловарь
jobs:Список заданийСловарь со списками
runs-on:Операционная системаСтрока
steps:Шаги внутри заданияСписок
- name:Имя шагаСтрока
uses:Использовать готовое действиеСтрока
run:Выполнить командуСтрока

На русском это читается следующим образом: «Конвейер с именем Build and Test. Запускайся при push в ветку main или при создании pull request в main. В задании build используй Ubuntu. Выполни следующие шаги: скачай код, установи .NET SDK, восстанови зависимости, собери проект, запусти тесты.».

Теперь рассмотрим пример для GitLab CI. В GitLab синтаксис похожий, но чуть другой:

yaml

# stages: этапы конвейера
stages:
  - build
  - test

# Job: сборка
build:
  stage: build
  image: mcr.microsoft.com/dotnet/sdk:8.0
  script:
    - dotnet restore
    - dotnet build

# Job: тесты
test:
  stage: test
  image: mcr.microsoft.com/dotnet/sdk:8.0
  script:
    - dotnet test

Рассмотрим разницу между GitHub Actions и GitLab CI:

GitHub ActionsGitLab CI
jobs: — список заданийstages: — этапы
runs-on: — операционная системаimage: — Docker-образ
steps: — шаги внутри заданияscript: — команды внутри задания

Как уже было сказано ранее, самая частая ошибка новичков в YAML — неправильные отступы.

yaml

# ❌ НЕПРАВИЛЬНО (разные отступы)
steps:
  - name: Restore
    run: dotnet restore
      - name: Build
    run: dotnet build

# ✅ ПРАВИЛЬНО (одинаковые отступы)
steps:
  - name: Restore
    run: dotnet restore
  - name: Build
    run: dotnet build

Как проверить, что отступы правильные:

1. Все элементы одного уровня должны иметь одинаковый отступ.

2. Вложенные элементы должны иметь больший отступ.

3. Используйте только пробелы (не табуляцию).

Большинство редакторов кода (VS Code, Visual Studio) подсвечивают ошибки в YAML. Это помогает заметить проблему до того, как вы отправите код.

§9.5. Структура CI-конвейера: собираем и тестируем

Теперь, когда мы знаем, что такое YAML и как он работает, давайте напишем наш первый конвейер.

Что должно происходить при git push:

1. Скачать код из репозитория.

2. Восстановить зависимости (NuGet/npm).

3. Собрать проект.

4. Запустить тесты.

5. Показать результат (зеленый/красный).

Это минимальный набор действий для любого CI-конвейера. Если тесты проходят — всё хорошо. Если падают — разработчик должен исправить ошибку.

Давайте напишем конвейер для проекта на C#. Он будет запускаться при каждом git push в ветку main. В корне репозитория создайте папку и файл:

text

.github/workflows/build.yml

Структура любого YAML-файла для CI/CD:

yaml

# 1. Имя конвейера
name: Название
# 2. Триггер: когда запускать
on: [push]
# 3. Задания: что делать
jobs:
  имя_задания:
    runs-on: ubuntu-latest
    steps:
      - name: Описание шага
        run: команда

Теперь заполним каждую часть конкретными значениями. Вот полный файл для GitHub Actions, который собирает и тестирует C# проект:

yaml

# Имя конвейера (показывается в интерфейсе GitHub)
name: Build and Test
# Триггер: запускаем при каждом push в main и develop
on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]
# Задания: что делаем
jobs:
  # Задание с именем "build"
  build:
    # На какой ОС запускать (Ubuntu — самая популярная)
    runs-on: ubuntu-latest
    # Шаги внутри задания
    steps:
      # Шаг 1: скачать код из репозитория
      - name: Checkout code
        uses: actions/checkout@v4      
      # Шаг 2: установить .NET SDK (версия 8.0)
      - name: Setup .NET
        uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '8.0'      
      # Шаг 3: восстановить NuGet-пакеты
      - name: Restore dependencies
        run: dotnet restore      
      # Шаг 4: собрать проект в режиме Release
      - name: Build project
        run: dotnet build --configuration Release --no-restore      
      # Шаг 5: запустить все тесты
      - name: Run tests
        run: dotnet test --configuration Release --no-build --verbosity normal

Давайте разберем этот код по частям:

ЧастьЧто делает
name: Build and TestИмя конвейера. Отображается в интерфейсе GitHub.
on: push: branches: [ main, develop ]Запускается при push в main или develop.
on: pull_request: branches: [ main ]Запускается при создании pull request в main.
runs-on: ubuntu-latestВыполняется на свежей версии Ubuntu.
uses: actions/checkout@v4Скачивает код из репозитория (готовое действие от GitHub).
uses: actions/setup-dotnet@v4Устанавливает .NET SDK (готовое действие от GitHub).
run: dotnet restoreВосстанавливает пакеты.
run: dotnet buildСобирает проект.
run: dotnet testЗапускает тесты.

Что означают флаги в dotnet-командах:

КомандаЧто делает
dotnet restoreСкачивает все NuGet-пакеты.
dotnet build --configuration ReleaseСобирает проект в режиме Release.
dotnet build --no-restoreНе восстанавливает пакеты перед сборкой (мы уже сделали это ранее).
dotnet test --configuration Release --no-buildЗапускает тесты без пересборки (использует уже собранные файлы).

Для JavaScript проект на GitHub Actions будет выглядеть так:

yaml

name: Build and Test

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4     
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '18'
          cache: 'npm'   
      - name: Install dependencies
        run: npm ci
      - name: Run linter
        run: npm run lint      
      - name: Run tests
        run: npm test

Давайте рассмотрим разницу между двумя YAML – кодами:

ЧтоC#JavaScript
Установка языкаactions/setup-dotnetactions/setup-node
Версияdotnet-version: '8.0'node-version: '18'
Восстановление зависимостейdotnet restorenpm ci
Сборкаdotnet buildМожет отсутствовать (или tsc)
Тестыdotnet testnpm test

GitLab использует похожий синтаксис, но с другими ключевыми словами:

yaml

# stages: определяем порядок этапов
stages:
  - build
  - test

# Job для сборки
build:
  stage: build
  image: mcr.microsoft.com/dotnet/sdk:8.0
  script:
    - dotnet restore
    - dotnet build --configuration Release

# Job для тестов
test:
  stage: test
  image: mcr.microsoft.com/dotnet/sdk:8.0
  script:
    - dotnet test --configuration Release --no-build

После отправки кода разработчик видит в интерфейсе GitHub/GitLab процесс выполнения. В GitHub Actions процесс выполнения выглядит примерно следующим образом:

wireframe

✅ Build and Test / build (pull_request)

   ✅ Checkout code (1s)

   ✅ Setup .NET (5s)

   ✅ Restore dependencies (3s)

   ✅ Build project (10s)

   ✅ Run tests (15s)

   

   ✅ All checks have passed

GitLab CI:

wireframe

text

✅ build (passed)

✅ test (passed)

   

   Pipeline passed

Если тесты упали:

wireframe

❌ Build and Test / build (pull_request)

   ✅ Checkout code (1s)

   ✅ Setup .NET (5s)

   ✅ Restore dependencies (3s)

   ✅ Build project (10s)

   ❌ Run tests (15s) ← здесь ошибка

      1 test failed: UserServiceTests.Login_InvalidPassword_ReturnsError

Разработчик кликает на упавший тест и видит ошибку.

Что нужно запомнить при работе с CI в GitHub Actions или GitLab CI:

1. Конвейер запускается автоматически при каждом push или pull request.

2. Любой конвейер состоит из:

  • Триггер (on: или stages:).
  • Задания (jobs: или stages:).
  • Шаги (steps: или script:).

3. Главное правило: если тесты упали — код не попадает в основную ветку.

4. Для C# проекта нужны: .NET SDK, dotnet restore, dotnet build, dotnet test.

5. Для JavaScript проекта нужны: Node.js, npm ci, npm test.

§9.6. Дополнительные возможности CI/CD

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

Какие задачи возникают:

ЗадачаПроблема
Пароли и токеныКак использовать секретные данные, не показывая их в коде?
Результаты тестовКак посмотреть отчеты, если тесты упали?
УведомленияКак узнать, что сборка сломалась, не заходя в репозиторий?

Рассмотрим каждую задачу по порядку.

1. Secrets (Секреты) – как хранить пароли и токены

В вашем коде могут быть секретные данные:

  • Пароли к базе данных.
  • API-ключи (например, для отправки email).
  • Токены доступа к внешним сервисам.
  • Строки подключения к БД.

Что будет, если захардкодить их в коде:

yaml

# ❌ НИКОГДА ТАК НЕ ДЕЛАЙТЕ!
steps:
  - name: Deploy to server
    run: |
      ssh user@server.com
      password: my_secret_password_123  # Пароль в открытом виде!

Чем это опасно:

  • Любой, у кого есть доступ к репозиторию, увидит пароль.
  • Пароль будет в истории коммитов навсегда.
  • Злоумышленники могут использовать эти данные.

Для решения используется Secrets (Секреты).

Secrets

— это зашифрованные переменные, которые хранятся в настройках репозитория. Они доступны только во время выполнения конвейера.

Как это работает:

1. Вы добавляете секрет в настройках GitHub/GitLab.

2. В YAML-файле вы обращаетесь к нему через специальный синтаксис.

3. Конвейер использует секрет, но не показывает его в логах.

Как добавить секрет в GitHub:

1. Зайдите в репозиторий на GitHub.

2. Нажмите Settings → Secrets and variables → Actions.

3. Нажмите New repository secret.

4. Введите имя (например, DATABASE_PASSWORD) и значение (пароль).

5. Нажмите Add secret.

Как использовать секрет в YAML:

yaml

steps:
  - name: Run tests with database
    env:
      DB_PASSWORD: ${{ secrets.DATABASE_PASSWORD }}  # Подставляем секрет
    run: dotnet test --environment DB_PASSWORD=$DB_PASSWORD

Как добавить секрет в GitLab:

1. Зайдите в проект на GitLab.

2. Нажмите Settings → CI/CD.

3. Разверните раздел Variables.

4. Нажмите Add variable.

5. Введите ключ (например, DATABASE_PASSWORD) и значение.

6. Выберите Protect variable (защитить) и Mask variable (скрыть в логах).

7. Нажмите Add variable.

Как использовать секрет в YAML (GitLab):

yaml

variables:
  DB_PASSWORD: $DATABASE_PASSWORD  # Подставляем секрет

job:
  script:
    - echo "Running tests with DB_PASSWORD"
    - dotnet test

2. Artifacts (Артефакты): как сохранять результаты тестов

Тесты упали, но вы не знаете почему. В логах только сообщение «Test failed». Нужно посмотреть детальный отчет, но после завершения конвейера все временные файлы удаляются.

Для решения используются Artifacts (Артефакты).

Артефакты

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

Что можно сохранять как артефакты:

Что сохранятьЗачем
Отчеты о тестах (JUnit XML, HTML)Посмотреть, какие тесты упали
Логи сборкиПонять, что пошло не так
Скриншоты (E2E-тесты)Увидеть, как выглядела страница в момент ошибки
Собранное приложениеСкачать и запустить локально

Как сохранить артефакты в GitHub Actions:

yaml

steps:
  # ... сборка и тесты ...
  # Сохраняем результаты тестов как артефакт
  - name: Upload test results
    uses: actions/upload-artifact@v4
    with:
      name: test-results
      path: TestResults/  # Папка с результатами тестов
      retention-days: 7   # Хранить 7 дней

Как же это выглядит для пользователя? После завершения конвейера в интерфейсе GitHub появляется кнопка Artifacts. Вы кликаете на нее и скачиваете файлы.

Пример для .NET (xUnit):

yaml

# Настройка вывода тестов в файл
- name: Run tests with output
  run: dotnet test --logger "trx;LogFileName=test_results.trx"

# Сохраняем результат
- name: Upload test results
  uses: actions/upload-artifact@v4
  with:
    name: test-results
    path: TestResults/*.trx
Как сохранить артефакты в GitLab CI:
job:
  script:
    - dotnet test --logger "trx;LogFileName=test_results.trx"
  artifacts:
    name: "$CI_JOB_NAME-$CI_COMMIT_REF_NAME"
    paths:
      - TestResults/*.trx
    expire_in: 7 days

3. Уведомления: как понять, что сборка сломалась

Вы отправили код и ушли пить кофе. Через 10 минут сборка упала, но вы не знаете об этом. Через час вы возвращаетесь и видите красный значок. Время потеряно.

Для решения используются Уведомления. CI/CD может отправлять уведомления о результатах сборки:

  • Email.
  • Telegram.
  • Slack.
  • Встроенные уведомления GitHub/GitLab.

GitHub сам отправляет уведомления на email, если вы подписаны на изменения в репозитории.

1. Зайдите в репозиторий.

2. Нажмите Watch → Custom.

3. Выберите Actions.

4. Теперь вы будете получать уведомления о всех запусках конвейеров.

Уведомления в Telegram (GitHub Actions):

yaml

steps:
  # ... сборка и тесты ...
  # Отправка уведомления в Telegram
  - name: Send Telegram notification
    if: always()  # Отправить всегда (успех или неудача)
    uses: appleboy/telegram-action@master
    with:
      to: ${{ secrets.TELEGRAM_CHAT_ID }}
      token: ${{ secrets.TELEGRAM_TOKEN }}
      message: |
        ${{ github.repository }}
        Status: ${{ job.status }}
        Commit: ${{ github.sha }}

Уведомления в Slack (GitHub Actions):

yaml

- name: Send Slack notification
  if: always()
  uses: 8398a7/action-slack@v3
  with:
    status: ${{ job.status }}
    fields: repo,commit,author,message
  env:
    SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

В GitLab уведомления настраиваются через интеграции:

1. Зайдите в проект → Settings → Integrations.

2. Выберите нужный сервис (Slack, Telegram, Mattermost).

3. Настройте webhook-URL.

4. Включите события для уведомлений (Push, Tag push, Pipeline).

Давайте разберем, что можно делать с артефактами и секретами:

ВозможностьЧто делаетПример использования
Артефакты между джобамиПередать файлы из одной джобы в другуюСобрать → передать на тестирование
Секреты для разных средРазные пароли для dev/staging/prodDev: test password, Prod: real password
Временные секретыСгенерировать и использовать одноразовый токенИспользовать и забыть

§9.7. Что должно быть в CI-конвейере для проекта

В §9.5 мы написали простой конвейер, который собирает проект и запускает тесты. Но в реальном проекте этого недостаточно. Вам нужно не просто запустить тесты, а запустить все виды тестов, которые вы писали.

Что должно быть в полноценном конвейере:

ЭтапЧто проверяет
1. Восстановление зависимостейСкачать все библиотеки
2. Сборка проектаПроверить, что код компилируется
3. Статический анализПроверить стиль и качество кода
4. Модульные тестыПроверить отдельные методы
5. Интеграционные тестыПроверить работу с БД
6. E2E-тестыПроверить полные сценарии

Этап №1. Восстановление зависимостей

Если ваш проект использует сторонние библиотеки. Перед сборкой их нужно скачать и установить. Для C# (.NET):

yaml

- name: Restore dependencies
  run: dotnet restore

dotnet restore скачивает все NuGet-пакеты, указанные в файле .csproj.

Для JavaScript (Node.js):

yaml

- name: Install dependencies
  run: npm ci

npm ci (clean install) устанавливает все пакеты из package-lock.json. Это быстрее и надежнее, чем npm install.

Этап №2. Сборка проекта (Build)

Это нужно, чтобы проверить, что код компилируется без ошибок. Если проект не собирается — нет смысла запускать тесты.

Для C# (.NET):

yaml

- name: Build project
  run: dotnet build --configuration Release --no-restore

Для JavaScript (TypeScript): eсли вы используете TypeScript, нужно проверить, что типы корректны:

yaml

yaml
- name: Type check
  run: npx tsc --noEmit

--noEmit означает «не создавать файлы, только проверить типы».

Если вы используете сборщик (Webpack, Vite):

yaml

- name: Build project
  run: npm run build

Этап №3. Статический анализ кода (Linter)

Linter

— это инструмент, который проверяет код на:

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

Linter

— это как проверка орфографии в тексте. Текст можно прочитать и без нее, но ошибки делают его некрасивым и могут запутать читателя.

Для C# (.NET) есть несколько вариантов:

1. dotnet format (встроенный):

yaml

- name: Check code style
  run: dotnet format --verify-no-changes

--verify-no-changes проверяет, что код уже отформатирован правильно. Если есть нарушения — конвейер упадет.

2. StyleCop (анализатор стиля): добавьте в проект пакет StyleCop.Analyzers. Ошибки будут отображаться как предупреждения при сборке.

3. SonarQube (полный анализ):

yaml

- name: Run SonarQube analysis
  run: |
    dotnet tool install --global dotnet-sonarscanner
    dotnet sonarscanner begin /k:"project-key"
    dotnet build
    dotnet sonarscanner end

SonarQube проверяет не только стиль, но и безопасность, сложность кода, дублирование. Для JavaScript (Node.js): ESLint — стандартный линтер для JavaScript/TypeScript:

yaml

- name: Run linter
  run: npm run lint

В package.json должен быть скрипт:

json

"scripts": {
  "lint": "eslint src/**/*.ts"
}

Этап №4. Запуск модульных тестов

Это нужно, чтобы проверить, что отдельные методы и классы работают правильно. Модульные тесты — это основа пирамиды тестирования. Их должно быть много, и они должны быть быстрыми.

Для C# (.NET):

yaml

- name: Run unit tests
  run: dotnet test --configuration Release --no-build --filter "Category=Unit"

--filter "Category=Unit" запускает только тесты с атрибутом [Trait("Category", "Unit")].

Для JavaScript (Node.js):

yaml

- name: Run unit tests
  run: npm test

Или с Jest:

yaml

- name: Run unit tests
  run: npm run test:unit

Этап №5. Запуск интеграционных тестов

Это нужно, чтобы проверить взаимодействие с реальной базой данных, внешними API и другими системами.

Интеграционные тесты требуют наличия базы данных. В CI окружении её нужно поднять.

Для C# (.NET) это происходит с помощью Testcontainers. Testcontainers поднимает Docker-контейнер с БД прямо во время тестов.

yaml

- name: Run integration tests
  run: dotnet test --configuration Release --no-build --filter "Category=Integration"
  env:
    # Testcontainers автоматически использует Docker
    TESTCONTAINERS_RYUK_DISABLED: true

Для JavaScript (Node.js) с Testcontainers, аналогично, поднимаем Docker-контейнер с БД:

yaml

- name: Run integration tests
  run: npm run test:integration

Этап №6. Запуск E2E-тестов

Это нужно, чтобы проверить полные пользовательские сценарии через реальный браузер.

Для C# (.NET) с Playwright:

yaml

- name: Install Playwright browsers
  run: pwsh ./playwright.ps1 install --with-deps

- name: Run E2E tests
  run: dotnet test --configuration Release --no-build --filter "Category=E2E"

Для JavaScript (Node.js) с Playwright:

yaml

- name: Install Playwright browsers
  run: npx playwright install --with-deps

- name: Run E2E tests
  run: npm run test:e2e

Чтобы все этапы работали, нужно правильно настроить окружение на CI-раннере.

Для C# (.NET):

yaml

- name: Setup .NET
  uses: actions/setup-dotnet@v4
  with:
    dotnet-version: '8.0'

Для JavaScript (Node.js):

yaml

- name: Setup Node.js
  uses: actions/setup-node@v4
  with:
    node-version: '18'

Для Docker (нужен для Testcontainers). В GitHub Actions Docker уже установлен на Ubuntu-раннерах по умолчанию. Ничего делать не нужно. В GitLab CI Docker тоже доступен.

Для Playwright (браузеры):

yaml

- name: Install Playwright dependencies
  run: |
    npx playwright install --with-deps

В конце стоит сравнить настройку CI для C# и JavaScript:

ЭтапC# (.NET)JavaScript (Node.js)
Установка окруженияactions/setup-dotnetactions/setup-node
Версияdotnet-version: '8.0'node-version: '18'
Восстановление зависимостейdotnet restorenpm ci
Сборкаdotnet buildnpx tsc --noEmit или npm run build
Статический анализdotnet format --verify-no-changesnpm run lint (ESLint)
Модульные тестыdotnet test --filter "Category=Unit"npm run test:unit
Интеграционные тестыdotnet test --filter "Category=Integration"npm run test:integration
E2E-тестыdotnet test --filter "Category=E2E"npm run test:e2e
Playwright установкаpwsh ./playwright.ps1 install --with-depsnpx playwright install --with-deps
DockerНужен для TestcontainersНужен для Testcontainers

§9.8. Антипаттерны в CI/CD: чего делать НЕ надо

CI/CD

— это мощный инструмент, но только если вы используете его правильно.

Есть несколько типичных ошибок, которые сводят на нет всю пользу от автоматизации.

Главная мысль этого параграфа заключается в том, что CI/CD — это не просто «написал и забыл». Это инструмент, который требует внимания и правильной настройки.

Антипаттерн №1. «Написал конвейер и забыл»

Разработчик настроил CI/CD, увидел зеленый значок один раз и больше никогда не заглядывает в результаты. Конвейер работает, тесты проходят — и все довольны.

Пример:

yaml

# Конвейер работал 6 месяцев назад
- name: Setup .NET
  uses: actions/setup-dotnet@v2  # Старая версия!
  with:
    dotnet-version: '6.0'       # Устаревшая версия

.NET 6.0 больше не поддерживается, setup-dotnet@v2 устарел. Конвейер падает при каждом запуске, но никто не обращает внимания.

Что нужно делать:

1. Смотреть на результаты после каждого push. Если конвейер красный — разбираться сразу, а не откладывать.

2. Регулярно обновлять конфигурацию. Версии инструментов, плагинов и языков обновляются — нужно следить за этим.

3. Настроить уведомления. Если конвейер упал — вы должны узнать об этом сразу (см. §9.6).

Антипаттерн №2. «Долгий конвейер»

Конвейер работает 2 – 3 часа. Разработчики отправляют код и идут пить чай, смотреть сериалы или заниматься другими делами. Обратная связь приходит слишком поздно. Почему это плохо:

ПроблемаПоследствия
Долгая обратная связьРазработчик переключается на другую задачу и забывает про код
Снижение продуктивностиЗа день можно сделать только 2 – 3 итерации вместо 20
Очереди в CIЕсли 10 разработчиков отправляют код, конвейеры стоят в очереди

Почему конвейер становится долгим:

ПричинаЧто делать
Слишком много тестов (все E2E)Вынести E2E в отдельный конвейер (запускать только в main)
Тесты на разных языках последовательноЗапускать параллельно (Matrix strategy)
Подготовка данных через UIИспользовать API для подготовки данных
Долгая сборкаИспользовать кеширование зависимостей

А как ускорить конвейер? Например, использовать параллельный запуск тестов (Matrix).

yaml

jobs:
  test:
    strategy:
      matrix:
        test_group: [Unit, Integration, E2E]
    runs-on: ubuntu-latest
    steps:
      - name: Run ${{ matrix.test_group }} tests
        run: dotnet test --filter "Category=${{ matrix.test_group }}"

Следующим шагом, можно использовать кеширование зависимостей.

yaml

- name: Cache NuGet packages
  uses: actions/cache@v3
  with:
    path: ~/.nuget/packages
    key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }}

Так же можно каждый конвейер разделять на несколько этапов.

yaml

# Быстрый конвейер (3 минуты)
name: Quick Check
on: push

jobs:
  quick:
    steps:
      - dotnet restore
      - dotnet build
      - dotnet test --filter "Category=Unit"

где, полный конвейер (запускается только в main)

yaml

name: Full Check
on: push to main

jobs:
  full:
    steps:
      - dotnet restore
      - dotnet build
      - dotnet test --filter "Category=Unit"
      - dotnet test --filter "Category=Integration"
      - dotnet test --filter "Category=E2E"

Вот представьте, что Вы ждете пиццу. Если курьер едет 2 часа — вы заказываете в другом месте. Чем быстрее обратная связь — тем быстрее вы исправляете ошибки.

Антипаттерн №3. «Секреты в коде»

Разработчик хранит пароли, токены и строки подключения прямо в YAML-файле. Пример как делать НЕ нужно:

yaml

# ❌ СТРОГО ЗАПРЕЩЕНО!
steps:
  - name: Deploy to database
    run: |
      connection_string="Server=prod-server;Database=Shop;User Id=admin;Password=SuperSecret123!"
      dotnet run --connection-string "$connection_string"

Почему это катастрофа:

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

Для того, чтобы делать все правильно, нужно следовать следующим шагам:

Шаг №1. Использовать Secrets (GitHub Actions):

yaml

# ✅ ПРАВИЛЬНО
steps:
  - name: Deploy to database
    run: dotnet run --connection-string "${{ secrets.DB_CONNECTION_STRING }}"

Шаг №2. Использовать Variables (GitLab CI):

yaml

# ✅ ПРАВИЛЬНО
variables:
  DB_CONNECTION_STRING: $DB_CONNECTION_STRING  # Из настроек проекта

Шаг №3. Использовать переменные окружения:

yaml

# ✅ ПРАВИЛЬНО
env:
  DB_PASSWORD: ${{ secrets.DB_PASSWORD }}

Зачем это нужно? Представьте, что вы пишете пароль от банковской карты на стикере и приклеиваете его на монитор. Все, кто проходит мимо, видят его. Тот, кто получает доступ к вашему компьютеру — получает доступ к деньгам.

Антипаттерн №4. «Зеленый конвейер, но всё сломалось»

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

Почему это возможно:

ПричинаЧто проверяет тестыЧто НЕ проверяет тесты
Нет тестов на новый кодТолько старую функциональностьНовые методы не покрыты тестами
Нет тестов на интеграциюОтдельные модулиВзаимодействие между модулями
Нет тестов на регрессиюТолько позитивные сценарииКрайние случаи и ошибки
Нет E2E-тестовAPI и сервисыИнтерфейс пользователя

Давайте рассмотрим следующим пример:

csharp

// Разработчик добавил новый метод
public async Task<User> GetUserAsync(int id)
{
    // Новый метод, но тестов для него нет!
    return await _context.Users.FindAsync(id);
}

Конвейер зеленый (тесты проходят), потому что:

  • Старые тесты не проверяют новый метод.
  • Интеграционных тестов нет.
  • E2E-тестов нет.

В продакшене метод GetUserAsync вызывается, падает с ошибкой, пользователи не могут войти в систему.

Для того, чтобы это исправить, нужно следовать следующим шагам:

Шаг №1. Проверять покрытие тестами (Code Coverage): нужно добавить в конвейер проверку, что тесты покрывают хотя бы 80% кода.

yaml

- name: Run tests with coverage
  run: dotnet test --collect:"XPlat Code Coverage"
  
- name: Check coverage threshold
  run: |
    reportgenerator -reports:TestResults/*.cobertura.xml -targetdir:coveragereport
    coverage=$(cat coveragereport/Summary.xml | grep -oP 'lineRate="\K[0-9.]+')
    if (( $(echo "$coverage < 0.8" | bc -l) )); then
      echo "❌ Coverage is $coverage, below 80%!"
      exit 1
    fi

Шаг №2. Добавить мутационное тестирование. Инструменты типа Stryker проверяют, найдут ли тесты ошибку, если изменить код.

Шаг №3. Писать тесты до кода (TDD). Сначала пишете тест (который падает), потом пишете код (который делает тест зеленым).