Перейти к содержимому

CI/CD и качество кода

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

Перед этой темой: Тестирование.

Junior+

Что такое CI и CD и из каких шагов состоит конвейер .NET-проекта?

Заголовок раздела «Что такое CI и CD и из каких шагов состоит конвейер .NET-проекта?»
Ответ

Что это

  • CI — изменения часто сливаются в общую ветку, и каждое автоматически собирается и тестируется.
  • Continuous delivery — проверенная сборка готова к выкладке в любой момент, в прод — по кнопке.
  • Continuous deployment — в прод без кнопки уходит каждое изменение, прошедшее проверки.

Шаги конвейера .NET

  1. dotnet restore — скачать NuGet-пакеты.
  2. dotnet build — собрать решение.
  3. dotnet test — прогнать тесты.
  4. Анализ кода: анализаторы, SonarQube.
  5. dotnet publish — подготовить файлы приложения.
  6. Сохранить результат как артефакт.
  7. Выложить его на тестовый стенд, потом в прод.

На pull request обычно идут шаги 1–4, на push в main — все.

Частая ошибка

Пересобирать проект для каждого окружения. Тогда в прод уходит не та сборка, которую проверяли.

Как устроено

  • Конвейер описан кодом в репозитории: .github/workflows/*.yml в GitHub Actions, Jenkinsfile в Jenkins. Его версионируют и ревьюят, как остальной код.
  • Триггеры: pull_request проверяет изменение до слияния, push в main собирает релизного кандидата, ручной запуск — для выкладки и повторов.
  • Задачи (jobs) выполняются на агентах: облачных или своих (self-hosted), например на Windows Server рядом с IIS. Разные задачи обмениваются файлами через артефакты.

Когда что выбирать

Признак Continuous delivery Continuous deployment
Выкладка в прод после ручного подтверждения автоматически
Что нужно надёжный CI ещё автотесты, мониторинг, автооткат

Ловушки

  • Долгий конвейер. Проверка PR на 40 минут — изменения копятся и сливаются пачками. Помогают кеш пакетов, --no-restore и --no-build в следующих шагах, параллельные задачи.
  • Нестабильные тесты. «Перезапусти, иногда падает» приучает игнорировать красный конвейер.
  • Секреты в YAML. Токены хранят в секретах GitHub или в Credentials Jenkins, а не в репозитории.

Правило

На PR — быстрые проверки, без зелёного статуса слияние запрещено. На main — артефакт собирается один раз и продвигается по окружениям.

Под капотом

  • Артефакт неизменяем и помечен версией и commit SHA. По версии из прода находят точный коммит.
  • Конфигурация окружения не вшивается в сборку, а приходит при выкладке: переменные окружения, appsettings.{Environment}.json, хранилище секретов. Так один артефакт проходит dev, stage и prod.
  • Воспроизводимость: global.json фиксирует SDK, lock-файлы — пакеты, агенты собираются из одного образа.

Цена

  • Время обратной связи — главная метрика CI: медленный конвейер тормозит каждое изменение.
  • Self-hosted-агенты видят внутреннюю сеть и дешевле на объёме, но их обновляют и защищают сами. Код из PR исполняется на них, поэтому GitHub не советует такие агенты для публичных репозиториев.

Компромиссы

  • GitHub Actions — YAML рядом с кодом и готовые actions. Jenkins — свой сервер, плагины и Groovy, полный контроль ценой постоянного обновления плагинов.
  • Continuous deployment требует отката за минуты и метрик, по которым его запускать. Без них ручное подтверждение честнее.
  • Быстрые проверки на PR и тяжёлые по расписанию — компромисс между скоростью и полнотой.

Как проверить в проде

  • DORA-метрики: частота выкладок, время от коммита до прода, доля неудачных изменений, время восстановления.
  • Версия и commit SHA видны в логе старта или в служебном эндпоинте — понятно, что сейчас работает.

Итог: ценность конвейера — воспроизводимость: один неизменяемый артефакт, конфигурация снаружи и быстрая обратная связь.

Пример
.github/workflows/ci.yml
name: ci
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-dotnet@v6
with:
global-json-file: global.json # версия SDK из репозитория
- run: dotnet restore
- run: dotnet build --no-restore -c Release
- run: dotnet test --no-build -c Release
- run: dotnet publish src/Shop.Api --no-build -c Release -o out
# Артефакт сохраняем только из main: его и выкладываем
- if: github.ref == 'refs/heads/main'
uses: actions/upload-artifact@v7
with:
name: shop-api-${{ github.sha }}
path: out
То же в Jenkinsfile
// Декларативный конвейер на Windows-агенте (multibranch pipeline)
pipeline {
agent { label 'windows' }
stages {
stage('Build') {
steps {
bat 'dotnet restore'
bat 'dotnet build --no-restore -c Release'
}
}
stage('Test') {
steps { bat 'dotnet test --no-build -c Release' }
}
stage('Publish') {
when { branch 'main' } // артефакт — только из main
steps {
bat 'dotnet publish src/Shop.Api --no-build -c Release -o out'
archiveArtifacts artifacts: 'out/**'
}
}
}
}
Middle+

Как автоматически следить за качеством кода?

Заголовок раздела «Как автоматически следить за качеством кода?»
Ответ

Что это

Автоматические проверки качества — правила, которые конвейер применяет к каждому PR.

Какие бывают

  • Компилятор и анализаторы. Предупреждение CS8602 о возможном null, правила CAxxxx о надёжности и производительности. Встроены в .NET SDK.
  • Стиль. .editorconfig задаёт отступы, именование и важность правил, dotnet format выравнивает код по нему.
  • Покрытие. Доля кода, которую выполнили тесты.
  • SonarQube. Собирает всё вместе и выносит вердикт по quality gate.
  • Ревью. Человек проверяет то, что не видят инструменты: замысел и дизайн.

Как пользоваться

Включите TreatWarningsAsErrors и сделайте зелёный конвейер обязательным условием слияния в main.

Частая ошибка

Гнаться за 100% покрытия. Покрытие показывает, какой код выполнялся, а не что его результат проверили.

Как устроено

  1. Сборка. Анализаторы Roslyn включены по умолчанию с .NET 5. AnalysisLevel выбирает набор правил: latest-recommended строже умолчания. Правила стиля IDExxxx при сборке из командной строки работают только с EnforceCodeStyleInBuild.
  2. Строгость. TreatWarningsAsErrors делает ошибкой любое предупреждение. Мягче — <WarningsAsErrors>Nullable</WarningsAsErrors>: ошибками становятся только nullable-предупреждения.
  3. Формат. dotnet format --verify-no-changes падает, если код расходится с .editorconfig.
  4. SonarQube. dotnet sonarscanner begin до сборки, dotnet test с отчётом покрытия, end после. Сервер считает quality gate.
  5. Ветка. Правила ветки требуют зелёных проверок и одобрения ревьюера.

Ловушки

  • TreatWarningsAsErrors на старом проекте даёт сотни ошибок разом. Его включают по проектам или начинают с WarningsAsErrors.
  • Анализ веток и PR в SonarQube есть с Developer Edition. Бесплатная Community Build анализирует только главную ветку.
  • Покрытие SonarQube не считает сам: отчёт делает coverlet, а путь передают свойством sonar.cs.opencover.reportsPaths.

Правило

Что можно проверить при сборке — проверяйте при сборке. SonarQube держит пороги на новый код, правила ветки делают проверки обязательными.

Под капотом

  • Анализатор Roslyn подписывается на синтаксические и семантические события компилятора. Он видит типы и символы, а результат одинаков в IDE и в CI.
  • Важность правила задаётся слоями: AnalysisLevel — база, .editorconfig или .globalconfig — конкретное правило (dotnet_diagnostic.CA2016.severity), #pragma и [SuppressMessage] — конкретное место.
  • SonarQube считает метрики относительно периода нового кода: с прошлой версии, за N дней или от ветки. «Sonar way» проверяет только его, а покрытие и дублирование игнорирует, пока новых строк меньше 20.

Цена

  • Анализаторы удлиняют сборку, latest-all на большом решении заметен.
  • Подавление без ревью — технический долг без владельца.

Компромиссы

  • 80% покрытия нового кода — умолчание Sonar, а не закон. Тривиальные DTO исключают через sonar.coverage.exclusions, сложную логику — нет.
  • Новый SDK приносит новые правила. AnalysisLevel с номером (10.0-recommended) фиксирует набор, и обновление становится отдельным PR.
  • Gate по всему коду на легаси вечно красный, и его перестают замечать. Gate на новый код держит планку для изменений, а старый код чинят, когда трогают.

Как проверить, что это работает

  • Тренд подавлений (#pragma warning disable, NoWarn) за квартал.
  • Доля PR, слитых в обход правил ветки или с красным gate.

Итог: строгость — в сборке и правилах ветки, SonarQube держит планку на новом коде, а подавления ревьюят как код.

Пример
<!-- Directory.Build.props в корне репозитория: действует на все проекты -->
<Project>
<PropertyGroup>
<Nullable>enable</Nullable>
<!-- Набор правил зафиксирован: новый SDK не включит новые правила сам -->
<AnalysisLevel>10.0-recommended</AnalysisLevel>
<!-- Правила стиля IDExxxx проверяются и в dotnet build -->
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<!-- Любое предупреждение ломает сборку -->
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
</Project>
.editorconfig
root = true
[*.cs]
# CA2016: передавать CancellationToken дальше — ошибка сборки
dotnet_diagnostic.CA2016.severity = error
# IDE0161: file-scoped namespace — предупреждение (формат option:severity, .NET 9+)
csharp_style_namespace_declarations = file_scoped:warning
SonarQube в Jenkinsfile
// Нужны глобальный инструмент dotnet-sonarscanner на агенте
// и webhook SonarQube на <jenkins>/sonarqube-webhook/
stage('Build and analyze') {
steps {
// withSonarQubeEnv подставляет SONAR_HOST_URL и SONAR_AUTH_TOKEN
withSonarQubeEnv('SonarQube') {
bat '''
dotnet sonarscanner begin /k:"shop-api" ^
/d:sonar.host.url=%SONAR_HOST_URL% ^
/d:sonar.token=%SONAR_AUTH_TOKEN% ^
/d:sonar.cs.opencover.reportsPaths=**/coverage.opencover.xml
dotnet build -c Release --no-incremental
dotnet test -c Release --no-build --collect:"XPlat Code Coverage;Format=opencover"
dotnet sonarscanner end /d:sonar.token=%SONAR_AUTH_TOKEN%
'''
}
}
}
stage('Quality gate') {
steps {
timeout(time: 10, unit: 'MINUTES') {
waitForQualityGate abortPipeline: true // красный gate — красная сборка
}
}
}
Middle+
Ответ

Что это

  • SemVer — версия MAJOR.MINOR.PATCH, например 2.4.1. Ломающее изменение даёт 3.0.0, новая функция — 2.5.0, исправление — 2.4.2. Суффикс -beta.1 помечает предварительную версию.
  • Приватный фид — NuGet-сервер компании для внутренних пакетов: общей библиотеки логирования, клиентов к внутренним API.

Как пользоваться

  1. Конвейер библиотеки выполняет dotnet pack и получает .nupkg.
  2. dotnet nuget push публикует пакет в фид.
  3. Фид прописан в nuget.config в корне репозитория, и пакет ставится как обычный: dotnet add package Company.Logging.

Частая ошибка

Перезалить изменённый пакет с той же версией. У всех в кеше ~/.nuget/packages уже лежит старый, и сборки начинают расходиться.

Как устроено

  • Фид. Azure Artifacts и GitHub Packages — облачные. BaGetter (продолжение BaGet), Nexus и ProGet — свои серверы. NuGet.Server — старый вариант на .NET Framework. Хороший фид проксирует nuget.org, и все пакеты идут через одну точку.
  • Версии в одном месте. Central Package Management и Directory.Packages.props — см. Платформу .NET.
  • Lock-файл. RestorePackagesWithLockFile создаёт packages.lock.json с полным графом. dotnet restore --locked-mode в CI падает, если граф изменился, а файл нет.
  • Аудит. NuGetAudit сверяет граф с базой уязвимостей при restore и выдаёт NU1901–NU1904 — от low до critical.
  • Версия из git. MinVer берёт версию из последнего тега, GitVersion — из тегов и схемы веток. Ручной <Version> в проекте не нужен.

Когда что выбирать

Инструмент Когда
MinVer версия — последний тег, простой процесс
GitVersion релизные ветки, GitFlow, сложные правила
ручной <Version> один пакет, редкие релизы

Ловушки

  • Lock-файл библиотеки не помогает её потребителям: граф считает приложение. Его коммитят прежде всего для приложений.
  • Floating-версии (2.*) без lock-файла делают сборку невоспроизводимой.
  • Свежий advisory ломает сборку с TreatWarningsAsErrors. Заранее решите, какие коды блокируют.

Правило

Один фид, версии в Directory.Packages.props, lock-файл и --locked-mode для приложений, версия — из тега.

Под капотом

  • NuGet поддерживает SemVer 2.0: предварительная версия ниже релиза (2.0.0-rc.1 меньше 2.0.0), метаданные после + в сравнении не участвуют.
  • Версий у сборки несколько: версия пакета — для NuGet, AssemblyVersion — для загрузчика, FileVersion — для Windows, InformationalVersion — для людей. С .NET 8 SDK дописывает в InformationalVersion commit SHA: 2.4.0+a1b2c3d….
  • Source Link с .NET 8 встроен в SDK для GitHub, Azure Repos, GitLab и Bitbucket. Он записывает в PDB адрес репозитория и коммит, и отладчик скачивает исходник точной версии. ContinuousIntegrationBuild=true в CI нормализует пути для детерминированной сборки.

Цена

  • .snupkg не раздувает пакет, но поддерживается не всеми фидами: Azure Artifacts его не принимает. Альтернатива — DebugType=embedded, PDB внутри сборки ценой размера.
  • Аудит транзитивных пакетов находит больше уязвимостей — и больше шума.

Компромиссы

  • Dependency confusion: пакет Company.Logging с высокой версией на nuget.org может победить внутренний. Защита — Package Source Mapping или один фид с проксированием.
  • Блокирующий аудит в отдельном конвейере (рецепт Microsoft с -p:AuditPipeline=true) не останавливает срочный фикс из-за свежего advisory.

Как проверить в проде

  • Версия и SHA из InformationalVersion в логе старта ведут к коммиту релиза.
  • dotnet package list --vulnerable --include-transitive (до .NET 10 — dotnet list package) на коммите релиза показывает уязвимости в том, что сейчас работает.
  • NuGet Package Explorer показывает, есть ли в пакете Source Link и символы.

Итог: пакеты — цепочка поставок: один фид, зафиксированный граф, аудит и версия, по которой находится коммит.

Пример
<!-- Directory.Build.props: правила для пакетов во всех проектах -->
<Project>
<PropertyGroup>
<!-- packages.lock.json с полным графом; в CI: dotnet restore --locked-mode -->
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
<!-- Аудит и транзитивных пакетов (для net10.0 это уже умолчание) -->
<NuGetAuditMode>all</NuGetAuditMode>
<!-- Уязвимости high и critical ломают сборку, low и moderate — предупреждения -->
<WarningsAsErrors>$(WarningsAsErrors);NU1903;NU1904</WarningsAsErrors>
</PropertyGroup>
</Project>
<!-- nuget.config в корне репозитория -->
<configuration>
<packageSources>
<clear />
<add key="company" value="https://nuget.example.com/v3/index.json" />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
</packageSources>
<!-- Package Source Mapping: Company.* — только из своего фида -->
<packageSourceMapping>
<packageSource key="company">
<package pattern="Company.*" />
</packageSource>
<packageSource key="nuget.org">
<package pattern="*" />
</packageSource>
</packageSourceMapping>
</configuration>
Senior+

Какие бывают стратегии выкладки и как откатываться?

Заголовок раздела «Какие бывают стратегии выкладки и как откатываться?»
Ответ

Что это

Стратегия выкладки — порядок, в котором новая версия попадает на серверы и получает трафик.

Стратегия Как работает
Остановить и заменить остановили, заменили файлы, запустили — есть простой
Rolling серверы за балансировщиком обновляются по одному
Blue-green новая копия рядом со старой, трафик переключается разом
Canary новая версия сначала получает 1–5% трафика

Как откатываться

  • Хранить предыдущий артефакт и выкладывать его одной командой.
  • В blue-green — вернуть трафик на старую копию.
  • Функцию под feature flag — выключить флагом, без выкладки.

Частая ошибка

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

Как устроено

  • Rolling. Балансировщик выводит сервер из ротации, сервер обновляется, проходит health check и возвращается.
  • Blue-green. Две полные копии. Новая разворачивается и прогревается без трафика, проходит smoke-тесты, затем балансировщик переключается. Старая остаётся для отката.
  • Canary. Новая версия получает малую долю трафика, её ошибки и задержки сравнивают со старой, долю увеличивают ступенями.
  • Feature flags. Код выкладывается выключенным и включается для части пользователей, например через Microsoft.FeatureManagement.

Когда что выбирать

Стратегия Плюс Минус
Rolling без лишних серверов откат — такой же долгий rolling
Blue-green мгновенный откат двойные ресурсы
Canary мало пострадавших нужны метрики и маршрутизация

Ловушки

  • Схема БД. Миграцию применяют до выкладки, и она обязана работать со старым кодом: добавлять, а не удалять и переименовывать. Подробно — в изменении схемы без простоя.
  • Состояние в памяти. После переключения пользователь попадает на другой экземпляр, поэтому сессии и кеши живут вне процесса.
  • Две версии сразу. Сообщения в очередях и ответы API должны понимать обе версии.

Правило

Каждый релиз совместим с предыдущим по схеме, API и сообщениям. Тогда подходит любая стратегия, и откат кода не требует отката базы.

Под капотом: IIS и контейнеры

  • app_offline.htm. Файл в корне сайта заставляет ASP.NET Core Module корректно остановить приложение и отдавать содержимое файла. DLL освобождаются, файлы заменяют, файл удаляют — следующий запрос запускает новую версию. Это простой: годится для окна обслуживания или как шаг rolling за балансировщиком.
  • Два сайта. Синий и зелёный сайты со своими пулами и папками за ARR или внешним балансировщиком. Зелёный разворачивают и прогревают, проверяют /health/ready, переключают балансировщик, синий держат до конца наблюдения.
  • Контейнеры. Оркестратор пускает трафик на новый экземпляр только после успешной readiness-проверки. Readiness и liveness — разные проверки: недоступная БД не повод перезапускать процесс.

Цена

Любая стратегия без простоя требует совместимости версий N и N−1: expand/contract в базе, необязательные поля в API, версионированные сообщения.

Компромиссы

  • Откат быстрее, если артефакт и схема совместимы. Исправление вперёд (roll forward) — если миграция данных уже необратима.
  • Feature flags отделяют выкладку от релиза, но копят мёртвые ветки кода. У флага должен быть срок жизни.

Как проверить в проде

  • После переключения конвейер выжидает период наблюдения (bake time), сравнивает долю 5xx, p99 и бизнес-метрики с порогами и при нарушении сам возвращает трафик.
  • Выкладка помечена в мониторинге версией, поэтому всплеск ошибок сразу связывается с релизом.
  • Откат репетируют: откат, который ни разу не выполняли, не работает.

Итог: безопасная выкладка — малые шаги, совместимость N и N−1, решение по метрикам и уже проверенный откат.

Пример
# Задача выкладки в ci.yml: blue-green на IIS (для простоты новая копия — зелёная)
jobs:
deploy:
needs: build
if: github.ref == 'refs/heads/main'
runs-on: [self-hosted, windows, iis] # свой агент в сети серверов
environment: production # ручное подтверждение в настройках окружения
defaults:
run:
shell: powershell
steps:
- uses: actions/checkout@v7 # скрипты выкладки лежат в репозитории
- uses: actions/download-artifact@v8
with:
name: shop-api-${{ github.sha }}
path: drop
# 1. Выкладка в копию без трафика
- run: |
Import-Module WebAdministration
Stop-WebAppPool -Name shop-green
Copy-Item drop\* D:\sites\shop-green -Recurse -Force
Start-WebAppPool -Name shop-green
# 2. Прогрев и проверка готовности до переключения
- run: Invoke-WebRequest http://localhost:8081/health/ready -UseBasicParsing
# 3. Переключение балансировщика и наблюдение за метриками
- run: ./deploy/switch-traffic.ps1 -To green
- run: ./deploy/watch-metrics.ps1 -Minutes 15 -Max5xxPercent 1
# 4. Сбой на любом шаге — трафик обратно на прежнюю копию
- if: failure()
run: ./deploy/switch-traffic.ps1 -To blue

switch-traffic.ps1 и watch-metrics.ps1 — скрипты проекта: первый меняет правило ARR или пул балансировщика, второй читает метрики из мониторинга. В следующий релиз цвета меняются местами.