CI/CD і якість коду
Як код потрапляє з pull request у продакшен: з яких кроків складається конвеєр, які перевірки якості в нього вбудовують, як керувати пакетами й версіями та як розгортати, щоб відкат був звичайною операцією. На співбесіді це перевіряють запитаннями про конвеєр, з яким кандидат працював сам.
Перед цією темою: Тестування.
Що таке CI і CD та з яких кроків складається конвеєр .NET-проєкту?
Section titled “Що таке CI і CD та з яких кроків складається конвеєр .NET-проєкту?”Що це
- CI — зміни часто зливаються в спільну гілку, і кожна автоматично збирається й тестується.
- Continuous delivery — перевірена збірка готова до розгортання будь-коли, у прод — за кнопкою.
- Continuous deployment — у прод без кнопки йде кожна зміна, що пройшла перевірки.
Кроки конвеєра .NET
dotnet restore— завантажити NuGet-пакети.dotnet build— зібрати рішення.dotnet test— прогнати тести.- Аналіз коду: аналізатори, SonarQube.
dotnet publish— підготувати файли застосунку.- Зберегти результат як артефакт.
- Розгорнути його на тестовий стенд, потім у прод.
На 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 видно в лозі старту або в службовому ендпоінті — зрозуміло, що зараз працює.
Підсумок: цінність конвеєра — відтворюваність: один незмінний артефакт, конфігурація ззовні й швидкий зворотний зв’язок.
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// Декларативний конвеєр на 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/**' } } }}- Чим continuous delivery відрізняється від continuous deployment? — Ручним підтвердженням розгортання в прод проти повністю автоматичного.
- Чому артефакт збирають один раз? — Інакше в прод іде не та збірка, яку тестували.
- Що запускати на PR, а що на
main? — На PR — швидкі перевірки для злиття, наmain— ще публікацію артефакту й розгортання. - Як пришвидшити конвеєр? — Кеш пакетів,
--no-restoreі--no-build, паралельні завдання, важкі тести — за розкладом. - Де зберігати секрети конвеєра? — У секретах GitHub або Credentials Jenkins, з мінімальними правами токена.
Як автоматично стежити за якістю коду?
Section titled “Як автоматично стежити за якістю коду?”Що це
Автоматичні перевірки якості — правила, які конвеєр застосовує до кожного PR.
Які бувають
- Компілятор і аналізатори. Попередження
CS8602про можливийnull, правилаCAxxxxпро надійність і продуктивність. Вбудовані в .NET SDK. - Стиль.
.editorconfigзадає відступи, іменування й важливість правил,dotnet formatвирівнює код за ним. - Покриття. Частка коду, яку виконали тести.
- SonarQube. Збирає все разом і виносить вердикт за quality gate.
- Рев’ю. Людина перевіряє те, чого не бачать інструменти: задум і дизайн.
Як користуватися
Увімкніть TreatWarningsAsErrors і зробіть зелений конвеєр обов’язковою умовою злиття в main.
Типова помилка
Гнатися за 100% покриття. Покриття показує, який код виконувався, а не що його результат перевірили.
Як влаштовано
- Збирання. Аналізатори Roslyn увімкнені за замовчуванням із .NET 5.
AnalysisLevelобирає набір правил:latest-recommendedсуворіший за замовчування. Правила стилюIDExxxxпід час збирання з командного рядка працюють лише зEnforceCodeStyleInBuild. - Строгість.
TreatWarningsAsErrorsробить помилкою будь-яке попередження. М’якше —<WarningsAsErrors>Nullable</WarningsAsErrors>: помилками стають лише nullable-попередження. - Формат.
dotnet format --verify-no-changesпадає, якщо код розходиться з.editorconfig. - SonarQube.
dotnet sonarscanner beginдо збирання,dotnet testзі звітом покриття,endпісля. Сервер рахує quality gate. - Гілка. Правила гілки вимагають зелених перевірок і схвалення рев’юера.
Пастки
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>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// Потрібні глобальний інструмент 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 — червоне збирання } }}- Чим
TreatWarningsAsErrorsвідрізняється відWarningsAsErrors? — Перший робить помилками всі попередження, другий — лише перелічені, наприкладNullable. - Що перевіряє quality gate «Sonar way»? — Новий код: немає нових зауважень, hotspots перевірені, покриття від 80%, дублювання до 3%.
- Чому gate дивиться на новий код, а не на весь? — Інакше легасі-проєкт вічно червоний, а планку треба тримати на змінах.
- Чому SonarQube показує нульове покриття? — Звіт не створено в потрібному форматі або шлях у
sonar.cs.opencover.reportsPathsне збігся. - Як оновити SDK і не зламати збирання новими правилами? — Зафіксувати
AnalysisLevelверсією й піднімати її окремим PR.
Як керувати пакетами й версіями?
Section titled “Як керувати пакетами й версіями?”Що це
- SemVer — версія
MAJOR.MINOR.PATCH, наприклад2.4.1. Несумісна зміна дає3.0.0, нова функція —2.5.0, виправлення —2.4.2. Суфікс-beta.1позначає попередню версію. - Приватний фід — NuGet-сервер компанії для внутрішніх пакетів: спільної бібліотеки логування, клієнтів до внутрішніх API.
Як користуватися
- Конвеєр бібліотеки виконує
dotnet packі отримує.nupkg. dotnet nuget pushпублікує пакет у фід.- Фід прописаний у
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 дописує вInformationalVersioncommit 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>- Коли змінювати major-версію? — За будь-якої несумісної зміни публічного API, навіть маленької.
- Навіщо lock-файл, якщо версії й так указані? — Указані прямі залежності; транзитивні й floating-версії без нього можуть змінитися.
- Як захиститися від dependency confusion? — Package Source Mapping або один фід, який проксує nuget.org.
- Що робити з вразливістю в транзитивному пакеті? — Оновити найближчий прямий пакет, інакше підняти транзитивний явним посиланням або transitive pinning.
- Навіщо Source Link? — Налагоджувач завантажує вихідний код пакета рівно тієї версії, з якої його зібрано.
Які бувають стратегії розгортання і як відкочуватися?
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 blueswitch-traffic.ps1 і watch-metrics.ps1 — скрипти проєкту: перший змінює правило ARR або пул балансувальника, другий читає метрики з моніторингу. Наступного релізу кольори міняються місцями.
- Чим blue-green відрізняється від canary? — Blue-green перемикає весь трафік одразу, canary — сходинками за частками.
- Як відкотити реліз, якщо міграція видалила колонку? — Швидко ніяк, тому видалення — окремий пізній реліз за expand/contract.
- Чи дає
app_offline.htmрозгортання без простою? — Ні, сайт на цей час недоступний; без простою — кілька серверів або два сайти за балансувальником. - Чим readiness відрізняється від liveness? — Readiness вирішує, чи давати екземпляру трафік, liveness — чи перезапускати процес.
- Що таке bake time? — Період спостереження за метриками після розгортання, перш ніж іти далі.