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

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

## §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 Delivery | Continuous Deployment |
| --- | --- | --- |
| **Решение о релизе** | Принимает человек | Принимает система |
| **Когда выкатывается** | По команде (руками) | Сразу после успешных тестов |
| **Подходит для** | Банковские системы, медицинское ПО | Сайты, интернет-магазины, мобильные приложения |
| **Риск** | Ниже (человек контролирует) | Выше (всё автоматически) |

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

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

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

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

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

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

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

yaml

```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

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

Для JavaScript:

bash

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

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

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

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

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

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

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

- Для **C#**: **`StyleCop`, `SonarQube`**.
- Для **JavaScript**: **`ESLint`, `Prettier`**.

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

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

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

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

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

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

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

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

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

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

**Триггер**

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

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

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

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

yaml

```yaml
on:
  push:
    branches:
      - main
      - develop
```

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

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

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

yaml

```yaml
on:
  pull_request:
    branches:
      - main
```

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

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

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

yaml

```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-тесты** | Проверяет UI | `npx playwright test` |
| **Развертывание** | Отправляет на сервер | `scp` или `kubectl apply` |

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

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

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

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

wireframe

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

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

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

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

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

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

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

wireframe

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

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

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

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

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

![](/images/lectures/pitpm/09/image-01.webp)

## §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

```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 Actions** | GitHub предоставляет бесплатные Runner в облаке |
| **GitLab CI** | GitLab предоставляет бесплатные Runner в облаке |

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

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

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

| Платформа | Бесплатные минуты в месяц |
| --- | --- |
| **GitHub Actions** | 2000 минут (около 33 часов) для бесплатных аккаунтов |
| **GitLab CI** | 400 минут (около 6.5 часов) для бесплатных аккаунтов |

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

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

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

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

bash

```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

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

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

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

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

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

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

yaml

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

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

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

**Словарь**

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

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

yaml

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

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

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

yaml

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

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

| JSON | YAML |
| --- | --- |
| `{ "имя": "Иван", "возраст": 25 }` | `имя: Иван` |
|  | `возраст: 25` |
| `{ "пользователь": { "имя": "Иван" } }` | `пользователь:` |
|  | `␣␣имя: Иван` |

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

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

**Список**

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

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

yaml

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

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

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

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

yaml

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

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

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

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

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

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

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

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

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

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

yaml

```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

```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 Actions | GitLab CI |
| --- | --- |
| `jobs:` — список заданий | `stages:` — этапы |
| `runs-on:` — операционная система | `image:` — Docker-образ |
| `steps:` — шаги внутри задания | `script:` — команды внутри задания |

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

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

```text
.github/workflows/build.yml
```

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

yaml

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

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

yaml

```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

```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-dotnet` | `actions/setup-node` |
| **Версия** | `dotnet-version: '8.0'` | `node-version: '18'` |
| **Восстановление зависимостей** | `dotnet restore` | `npm ci` |
| **Сборка** | `dotnet build` | `Может отсутствовать (или tsc)` |
| **Тесты** | `dotnet test` | `npm test` |

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

yaml

```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

```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

```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

```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

```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

```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

```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

```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/prod | Dev: test password, Prod: real password |
| **Временные секреты** | Сгенерировать и использовать одноразовый токен | Использовать и забыть |

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

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

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

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

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

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

yaml

```yaml
- name: Restore dependencies
  run: dotnet restore
```

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

Для **JavaScript (Node.js)**:

yaml

```yaml
- name: Install dependencies
  run: npm ci
```

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

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

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

Для **C# (.NET)**:

yaml

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

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

yaml

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

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

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

yaml

```yaml
- name: Build project
  run: npm run build
```

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

**Linter**

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

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

**Linter**

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

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

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

yaml

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

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

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

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

yaml

```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

```yaml
- name: Run linter
  run: npm run lint
```

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

json

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

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

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

**Для C# (.NET)**:

yaml

```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

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

Или с **Jest**:

yaml

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

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

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

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

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

yaml

```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

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

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

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

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

yaml

```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

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

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

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

Для **C# (.NET)**:

yaml

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

Для **JavaScript (Node.js)**:

yaml

```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

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

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

| Этап | C# (.NET) | JavaScript (Node.js) |
| --- | --- | --- |
| **Установка окружения** | `actions/setup-dotnet` | `actions/setup-node` |
| **Версия** | `dotnet-version: '8.0'` | `node-version: '18'` |
| **Восстановление зависимостей** | `dotnet restore` | `npm ci` |
| **Сборка** | `dotnet build` | `npx tsc --noEmit` или `npm run build` |
| **Статический анализ** | `dotnet format --verify-no-changes` | `npm 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-deps` | `npx playwright install --with-deps` |
| **Docker** | Нужен для Testcontainers | Нужен для Testcontainers |

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

**CI/CD**

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

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

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

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

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

Пример:

yaml

```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

```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

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

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

yaml

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

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

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

yaml

```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

```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

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

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

yaml

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

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

yaml

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

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

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

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

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

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

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

csharp

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

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

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

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

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

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

yaml

```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). Сначала пишете тест (который падает), потом пишете код (который делает тест зеленым).**
