Перейти до вмісту

CI/CD і якість коду

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

Перед цією темою: Тестування.

Junior+

Що таке CI і CD та з яких кроків складається конвеєр .NET-проєкту?

Section titled “Що таке 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+

Як автоматично стежити за якістю коду?

Section titled “Як автоматично стежити за якістю коду?”
Відповідь

Що це

Автоматичні перевірки якості — правила, які конвеєр застосовує до кожного 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+

Як керувати пакетами й версіями?

Section titled “Як керувати пакетами й версіями?”
Відповідь

Що це

  • 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+

Які бувають стратегії розгортання і як відкочуватися?

Section titled “Які бувають стратегії розгортання і як відкочуватися?”
Відповідь

Що це

Стратегія розгортання — порядок, у якому нова версія потрапляє на сервери й отримує трафік.

Стратегія Як працює
Зупинити й замінити зупинили, замінили файли, запустили — є простій
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 або пул балансувальника, другий читає метрики з моніторингу. Наступного релізу кольори міняються місцями.