Посібник з CI/CD у GitLab для (майже) абсолютного новачка
Або як придбати гарні бейджики для свого проекту за один вечір ненапружного кодингу
Напевно, у кожного розробника, який має хоча б один пет-проект, у певний момент виникає свербіж на тему красивих бейджиків зі статусами, покриттям коду, версіями пакетів у nuget… І мене це свербіння призвело до написання цієї статті. У процесі підготовки до її написання я отримав ось таку красу в одному зі своїх проектів:
У статті буде розглянуто базове налаштування безперервної інтеграції та постачання для проекту бібліотеки класів на .Net Core у GitLab, з публікацією документації у GitLab Pages та відправкою зібраних пакетів до приватного фіду в Azure DevOps.
Як середовище розробки використовувалася VS Code з розширенням GitLab Workflow (Для валідації файлу налаштувань прямо з середовища розробки).
Короткий вступ
CD - це коли ти тільки пушнув, а у клієнта вже все впало?
Що таке CI/CD і навіщо потрібно можна легко нагуглити. Повноцінну документацію з налаштування пайплайнів у GitLab знайти також нескладно. Тут я коротко і наскільки можна без огріхів опишу процес роботи системи з висоти пташиного польоту:
розробник відправляє коміт у репозиторій, створює merge request через сайт, або ще якимось чином явно чи неявно запускає пайплайн,
з конфігурації вибираються всі завдання, умови яких дозволяють їх запустити у цьому контексті,
завдання організовуються відповідно до своїх етапів,
етапи по черзі виконуються - тобто. паралельно виконуються всі завдання цього етапу,
якщо етап завершується невдачею (тобто завершується невдачею хоча б одне із завдань етапу) - пайплайн зупиняється (майже завжди),
якщо всі етапи завершено успішно, пайплайн вважається успішним.
Таким чином, маємо:
пайплайн - набір завдань, організованих в етапи, в якому можна зібрати, протестувати, упакувати код, розгорнути готове складання в хмарний сервіс, тощо,
етап (етап) - одиниця організації пайплайну, містить 1+ завдання,
завдання (робота) - Одиниця роботи в пайплайні. Складається зі скрипту (обов'язково), умов запуску, налаштувань публікації/кешування артефактів та багато іншого.
Відповідно, завдання при налаштуванні CI/CD зводиться до того, щоб створити набір завдань, що реалізують всі необхідні дії для збирання, тестування та публікації коду та артефактів.
Перед початком: чому?
Чому GitLab?
Тому що коли виникла потреба створити приватні репозиторії під пет-проекти, на GitHub'e вони були платними, а я жадібним. Репозиторії стали безкоштовними, але поки що це не є для мене приводом достатнім переїздити на GitHub.
Чому не Azure DevOps Pipelines?
Бо там налаштування елементарне — навіть не потрібні знання командного рядка. Інтеграція із зовнішніми провайдерами git — у пару кліків, імпорт SSH-ключів для відправки коммітів у репозиторій — також пайплайн легко налаштовується навіть не з шаблону.
Вихідна позиція: що є і чого хочеться
маємо:
репозиторій у GitLab.
Хочемо:
автоматичне складання та тестування для кожного merge request,
складання пакетів для кожного merge request і пуша в майстер за умови наявності в повідомленні комміту певного рядка,
надсилання зібраних пакетів до приватного фіду в Azure DevOps,
складання документації та публікацію в GitLab Pages,
бейджики!11
Описані вимоги органічно лягають на таку модель пайплайну:
Етап 1 - складання
Збираємо код, вихідні файли публікуємо як артефакти
Етап 2 - тестування
Отримуємо артефакти з етапу складання, ганяємо тести, зібраємо дані покриття коду
Етап 3 - відправка
Завдання 1 - збираємо nuget-пакет і відправляємо до Azure DevOps
Завдання 2 — збираємо сайт з xmldoc у вихідному коді та публікуємо у GitLab Pages
При натисканні на кнопку Create проект буде створено, і буде здійснено перехід на сторінку. На цій сторінці можна вимкнути непотрібні можливості, перейшовши в налаштування проекти (нижнє посилання у списку зліва -> Overview -> блок Azure DevOps Services)
Переходимо в Atrifacts, тиснемо Create feed
Вводимо ім'я джерела
Вибираємо видимість
Знімаємо галочку Include packages from common public sourcesщоб джерело не перетворилося на смітник клон nuget
Тиснемо Connect to feed, вибираємо Visual Studio, з блоку Machine Setup копіюємо Source
Йдемо в налаштування облікового запису, вибираємо Personal Access Token
Створюємо новий токен доступу
Ім'я – довільне
Організація - поточна
Термін дії - максимум 1 рік
Область дії (scope) – Packaging/Read & Write
Копіюємо створений токен - після закриття модального вікна значення буде недоступним
Заходимо в налаштування репозиторію в GitLab, вибираємо налаштування CI/CD
Розкриваємо блок Variables, додаємо нову
Ім'я - будь-яке без пробілів (буде доступне в командній оболонці)
Значення – токен доступу з п. 9
Вибираємо Mask variable
На цьому попереднє налаштування завершено.
Готуємо каркас конфігурації
За замовчуванням, для налаштування CI/CD у GitLab використовується файл .gitlab-ci.yml з кореня репозиторію. Можна налаштувати довільний шлях до цього файлу в налаштуваннях репозиторію, але це не потрібно.
Як видно з розширення файл містить конфігурацію у форматі YAML. У документації докладно описано, які ключі можуть бути на верхньому рівні конфігурації, і кожному з вкладених рівнів.
Спочатку додамо у файл конфігурації посилання на docker-образ, у якому відбуватиметься виконання завдань. Для цього знаходимо сторінку образів .Net Core у Docker Hub. У GitHub є докладний посібник, який вибрати образ для різних завдань. Нам для складання підійде образ із .Net Core 3.1, тому сміливо додаємо першим рядком у конфігурацію
image: mcr.microsoft.com/dotnet/core/sdk:3.1
Тепер при запуску пайплайну зі сховища образів Microsoft буде завантажено зазначений образ, в якому будуть виконуватися всі завдання з конфігурації.
Наступний етап - додати етапТи. За промовчанням GitLab визначає 5 етапів:
.pre - Виконується до всіх етапів,
.post - Виконується після всіх етапів,
build - Перший після .pre етап,
test - другий етап,
deploy - Третій етап.
Нічого не заважає оголосити їх явно, втім. Порядок, у якому зазначені етапи, впливає порядок, у якому виконуються. Для повноти викладу додамо в конфігурацію:
stages:
- build
- test
- deploy
Для налагодження має сенс отримати інформацію про оточення, в якому виконуються завдання. Додамо глобальний набір команд, який виконуватиметься перед кожним завданням, за допомогою before_script:
before_script:
- $PSVersionTable.PSVersion
- dotnet --version
- nuget help | select-string Version
Залишилося додати хоча б одне завдання, щоб під час відправлення коммітів пайплайн запустився. Поки що додамо порожнє завдання для демонстрації:
dummy job:
script:
- echo ok
Запускаємо валідацію, отримуємо повідомлення, що все добре, комітім, пушимо, дивимося на сайті на результати ... І отримуємо помилку скрипту bash: .PSVersion: command not found. WTF?
Все логічно - за замовчуванням runner'и (що відповідають за виконання скриптів завдань і надаються GitLab'ом) використовують bash для виконання команд. Можна виправити цю справу, явно вказавши в описі завдання, які теги повинні бути у раннера, що виконує пайплайн:
dummy job on windows:
script:
- echo ok
tags:
- windows
Чудово! Тепер пайплайн виконується.
Уважний читач, повторивши вказані кроки, зауважить, що завдання виконалося в етапі testхоча ми не вказували етап. Як можна здогадатися, test є етапом за умовчанням.
Продовжимо створення кістяка конфігурації, додавши всі завдання, описані вище:
build job:
script:
- echo "building..."
tags:
- windows
stage: build
test and cover job:
script:
- echo "running tests and coverage analysis..."
tags:
- windows
stage: test
pack and deploy job:
script:
- echo "packing and pushing to nuget..."
tags:
- windows
stage: deploy
pages:
script:
- echo "creating docs..."
tags:
- windows
stage: deploy
Отримали не особливо функціональний, але коректний пайплайн.
Налаштування тригерів
Через те, що для жодного із завдань не вказано фільтри спрацьовування, пайплайн буде повністю виконуватися при кожному відправленні коммітів у репозиторій. Так як це не є бажаною поведінкою у загальному випадку, ми налаштуємо фільтри спрацьовування для завдань.
Фільтри можуть налаштовуватися у двох форматах: only/except и Правила. Коротко, only/except дозволяє налаштовувати фільтри за тригерами (merge_requestнаприклад, — налаштовує завдання на виконання при кожному створенні запиту на злиття та при кожній відправці коммітів у гілку, що є вихідною у запиті на злиття) та імен гілок (в т.ч. з використанням регулярних виразів); rules дозволяє налаштовувати набір умов і, опціонально, змінювати умову виконання завдання залежно від успіху попередніх завдань (when у GitLab CI/CD).
Згадаймо набір вимог - складання та тестування тільки для merge request, упаковка та відправка в Azure DevOps - для merge request і гармат у майстер, генерація документації - для гармат у майстер.
Для початку налаштуємо завдання складання коду, додавши правило спрацьовування тільки при merge request:
build job:
# snip
only:
- merge_request
Тепер налаштуємо завдання упаковки на спрацювання на merge request та додавання коммітів у майстер:
В умовах можна використовувати перелічені тут змінні; правила rules не сумісні з правилами only/except.
Налаштування збереження артефактів
Під час виконання завдання build job у нас будуть створені артефакти складання, які можна перевикористовувати у наступних завданнях. Для цього потрібно в конфігурацію завдання додати шляхи, файли якими потрібно буде зберегти і перевикористовувати в наступних задачах, в ключ artifacts:
Шляхи підтримують wildcards, що спрощує їх завдання.
Якщо завдання створює артефакти, то кожна наступна задача зможе їх отримати до них доступ - вони будуть розташовуватися по тих же шляхах щодо кореня репозиторію, якими були зібрані з вихідного завдання. Також артефакти доступні для скачування на сайті.
Тепер, коли у нас готовий (і перевірений) каркас конфігурації, можна переходити до написання скриптів для завдань.
Пишемо скрипти
Можливо, колись давно, в далекій галактиці, збирати проекти (у тому числі і на .net) з командного рядка було болем. Зараз же зібрати, протестувати та опублікувати проект можна у 3 команди:
dotnet build
dotnet test
dotnet pack
Звичайно, є деякі нюанси, через які ми дещо ускладнимо команди.
Ми хочемо релізне, а не налагоджувальне складання, тому до кожної команди додаємо -c Release
При тестуванні ми хочемо збирати дані про покриття коду, тому потрібно підключити аналізатор покриття до тестових бібліотек:
До всіх тестових бібліотек слід додати пакет coverlet.msbuild: dotnet add package coverlet.msbuild з папки проекту
До команди запуску тестів додамо /p:CollectCoverage=true
До конфігурації завдання тестування додамо ключ для отримання результатів покриття (див. нижче)
При упаковці коду в nuget-пакети задамо вихідну директорію для пакетів: -o .
Збираємо дані покриття коду
Coverlet після запуску тестів виводить у консоль статистику із запуску:
GitLab дозволяє вказати регулярний вираз для отримання статистики, яку потім можна отримати у вигляді бейджу. Регулярний вираз вказується в налаштуваннях завдання з ключем coverage; у виразі має бути присутнім capture-група, значення якої і буде передано в бейдж:
test and cover job:
# snip
coverage: /|s*Totals*|s*(d+[,.]d+%)/
Тут ми отримуємо статистику з рядка із загальним покриттям по лініях.
Публікуємо пакети та документацію
Обидві дії у нас призначені на останній етап пайплайну — якщо вже складання та тести пройшли, можна й поділитися зі світом напрацюваннями.
Для початку розглянемо публікацію у джерело пакетів:
Якщо в проекті немає файлу конфігурації nuget (nuget.config), створимо новий: dotnet new nugetconfig
Навіщо: в образі може бути заборонено доступ на запис до глобальних (користувацької та машинної) конфігурацій. Щоб не ловити помилки, просто створимо нову локальну конфігурацію і працюватимемо з нею.
Додамо до локальної конфігурації нове джерело пакетів: nuget sources add -name <name> -source <url> -username <organization> -password <gitlab variable> -configfile nuget.config -StorePasswordInClearText
name - локальне ім'я джерела, не принципово
url - URL джерела з етапу "Готуємо акаунти", п. 6
organization - Назва організації в Azure DevOps
gitlab variable - Ім'я змінної з токеном доступу, доданої в GitLab ("Готуємо акаунти", п. 11). Звичайно, у форматі $variableName
Надсилаємо всі пакети з поточної директорії, тому *.nupkg.
name - З кроку вище.
key - Будь-який рядок. В Azure DevOps у вікні Connect to feed завжди як приклад наводять рядок az.
-skipduplicate — при спробі відправити існуючий пакет без цього ключа джерело поверне помилку 409 Conflict; з ключем відправлення буде пропущено.
Тепер налаштуємо створення документації:
Для початку, у репозиторії, у гілці master, ініціалізуємо проект docfx. Для цього з кореня треба виконати команду docfx init та в інтерактивному режимі задамо ключові параметри для збирання документації. Детальний опис мінімального налаштування проекту тут.
При налаштуванні важливо вказати вихідну директорію ..public — GitLab за промовчанням бере вміст папки public у корені репозиторію як джерело для Pages. Т.к. проект розташовуватиметься у вкладеній в репозиторій папці - додаємо в дорогу вихід на рівень вгору.
Відправимо зміни до GitLab.
У конфігурацію пайплайну додамо завдання pages (зарезервоване слово для завдань публікації сайтів у GitLab Pages):
Скрипт:
nuget install docfx.console -version 2.51.0 - Встановить docfx; версія вказана для гарантії правильності шляхів інсталяції пакета.
Раніше при налаштуванні проекту я вказував джерело коду документації як файл рішення. Основний мінус – документація створюється і для тестових проектів. Якщо це не потрібно, можна задати таке значення вузлу metadata.src:
metadata.src.src: "../" - Виходимо на рівень вгору щодо розташування docfx.json, т.к. у патернах не працює пошук нагору по дереву директорій.
metadata.src.files: ["**/*.csproj"] - Глобальний патерн, збираємо всі проекти C# з усіх директорій.
metadata.src.exclude: ["*.tests*/**"] - глобальний патерн, виключаємо все з папок з .tests в назві
Проміжний підсумок
Ось таку просту конфігурацію можна скласти буквально за півгодини та пару чашок кави, яка дозволить при кожному запиті злиття та відправлення в майстер перевіряти, що код збирається та тести проходять, збирати новий пакет, оновлювати документацію та радувати око красивими бейджиками у README проекті.
Підсумковий .gitlab-ci.yml
image: mcr.microsoft.com/dotnet/core/sdk:3.1
before_script:
- $PSVersionTable.PSVersion
- dotnet --version
- nuget help | select-string Version
stages:
- build
- test
- deploy
build job:
stage: build
script:
- dotnet build -c Release
tags:
- windows
only:
- merge_requests
- master
artifacts:
paths:
- your/path/to/binaries
test and cover job:
stage: test
tags:
- windows
script:
- dotnet test -c Release /p:CollectCoverage=true
coverage: /|s*Totals*|s*(d+[,.]d+%)/
only:
- merge_requests
- master
pack and deploy job:
stage: deploy
tags:
- windows
script:
- dotnet pack -c Release -o .
- dotnet new nugetconfig
- nuget sources add -name feedName -source https://pkgs.dev.azure.com/your-organization/_packaging/your-feed/nuget/v3/index.json -username your-organization -password $nugetFeedToken -configfile nuget.config -StorePasswordInClearText
- nuget push -source feedName -skipduplicate -apikey az *.nupkg
only:
- master
pages:
tags:
- windows
stage: deploy
script:
- nuget install docfx.console -version 2.51.0
- $env:path = "$env:path;$($(get-location).Path)"
- .docfx.console.2.51.0toolsdocfx.exe .docfxdocfx.json
artifacts:
paths:
- public
only:
- master
До речі про бейджиків
Адже заради них все й починалося!
Бейджі зі статусами пайплайну та покриттям коду доступні в GitLab в налаштуваннях CI/CD у блоці Gtntral pipelines:
Бейдж із посиланням на документацію я створював на платформі Shields.io - там все досить прямолінійно, можна створити свій бейдж та отримувати його за допомогою запиту.
![Пример с Shields.io](https://img.shields.io/badge/custom-badge-blue)
Azure DevOps Artifacts також дозволяє створювати бейджі для пакетів із зазначенням актуальної версії. Для цього у джерелі на сайті Azure DevOps потрібно натиснути на Create badge у вибраного пакета та скопіювати markdown-розмітку:
Додаємо краси
Виділяємо загальні фрагменти конфігурації
Під час написання конфігурації та пошуків документації, я натрапив на цікаву можливість YAML — перевикористання фрагментів.
Як видно з налаштувань завдань, усі вони вимагають наявності тега windows у раннера, і спрацьовують при надсиланні в майстер/створенні запиту на злиття (крім документації). Додамо це у фрагмент, який перевикористовуватимемо:
І тепер в описі завдання можемо вставити раніше оголошений фрагмент:
build job:
<<: *common_tags
<<: *common_only
Назви фрагментів повинні починатися з точки, щоб не бути інтерпретованим як завдання.
Версіонування пакетів
При створенні пакета компілятор перевіряє ключі командного рядка, і за їх відсутності - файли проектів; знайшовши вузол Version, він бере його значення як версію пакета, що збирається. Виходить, щоб зібрати пакет із новою версією, потрібно або оновити її у файлі проекту, або передати як аргумент командного рядка.
Додамо ще одну хотівку - нехай молодші два номери у версії будуть роком і датою складання пакета, і додамо пререлізні версії. Додавати ці дані у файл проекту і перевіряти перед кожною відправкою можна, звичайно — але ж можна це робити і в пайплайні, збираючи версію пакета з контексту і передаючи через аргумент командного рядка.
Умовимося, якщо у повідомленні комміта є рядок виду release (v./ver./version) <version number> (rev./revision <revision>)?, то ми будемо з цього рядка брати версію пакета, доповнювати її поточною датою та передавати як аргумент команді dotnet pack. Без рядка — просто не збиратимемо пакет.
Це завдання вирішує наступний скрипт:
# регулярное выражение для поиска строки с версией
$rx = "releases+(v.?|ver.?|version)s*(?<maj>d+)(?<min>.d+)?(?<rel>.d+)?s*((rev.?|revision)?s+(?<rev>[a-zA-Z0-9-_]+))?"
# ищем строку в сообщении коммита, передаваемом в одной из предопределяемых GitLab'ом переменных
$found = $env:CI_COMMIT_MESSAGE -match $rx
# совпадений нет - выходим
if (!$found) { Write-Output "no release info found, aborting"; exit }
# извлекаем мажорную и минорную версии
$maj = $matches['maj']
$min = $matches['min']
# если строка содержит номер релиза - используем его, иначе - текущий год
if ($matches.ContainsKey('rel')) { $rel = $matches['rel'] } else { $rel = ".$(get-date -format "yyyy")" }
# в качестве номера сборки - текущие месяц и день
$bld = $(get-date -format "MMdd")
# если есть данные по пререлизной версии - включаем их в версию
if ($matches.ContainsKey('rev')) { $rev = "-$($matches['rev'])" } else { $rev = '' }
# собираем единую строку версии
$version = "$maj$min$rel.$bld$rev"
# собираем пакеты
dotnet pack -c Release -o . /p:Version=$version
Додаємо скрипт у завдання pack and deploy job і спостерігаємо складання пакетів суворо за наявності заданого рядка у повідомленні комміту.
Разом
Витративши приблизно півгодини-годину часу на написання конфігурації, налагодження в локальному PowerShell і, можливо, пару невдалих запусків, ми отримали нескладну конфігурацію для автоматизації рутинних завдань.
Звичайно, GitLab CI/CD набагато ширший і багатогранніший, ніж може здатися після прочитання цього керівництва. це зовсім не так. Там навіть Auto DevOps є, що дозволяє
автоматично виявляти, будувати, test, deploy, і monitor ваші applications
Тепер у планах — налаштувати пайплайн для розгортання додатків в Azure з використанням Pulumi та автоматичним визначенням цільового оточення, що буде освітлено в наступній статті.