Посібник з CI/CD у GitLab для (майже) абсолютного новачка

Або як придбати гарні бейджики для свого проекту за один вечір ненапружного кодингу

Напевно, у кожного розробника, який має хоча б один пет-проект, у певний момент виникає свербіж на тему красивих бейджиків зі статусами, покриттям коду, версіями пакетів у nuget… І мене це свербіння призвело до написання цієї статті. У процесі підготовки до її написання я отримав ось таку красу в одному зі своїх проектів:

Посібник з CI/CD у GitLab для (майже) абсолютного новачка

У статті буде розглянуто базове налаштування безперервної інтеграції та постачання для проекту бібліотеки класів на .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

Приступимо!

Збираємо конфігурацію

Готуємо акаунти

  1. Створюємо акаунт у Microsoft Azure

  2. Переходимо в Azure DevOps

  3. Створюємо новий проект

    1. Ім'я - будь-яке
    2. Видимість - будь-яка
      Посібник з CI/CD у GitLab для (майже) абсолютного новачка

  4. При натисканні на кнопку Create проект буде створено, і буде здійснено перехід на сторінку. На цій сторінці можна вимкнути непотрібні можливості, перейшовши в налаштування проекти (нижнє посилання у списку зліва -> Overview -> блок Azure DevOps Services)
    Посібник з CI/CD у GitLab для (майже) абсолютного новачка

  5. Переходимо в Atrifacts, тиснемо Create feed

    1. Вводимо ім'я джерела
    2. Вибираємо видимість
    3. Знімаємо галочку Include packages from common public sourcesщоб джерело не перетворилося на смітник клон nuget
      Посібник з CI/CD у GitLab для (майже) абсолютного новачка

  6. Тиснемо Connect to feed, вибираємо Visual Studio, з блоку Machine Setup копіюємо Source
    Посібник з CI/CD у GitLab для (майже) абсолютного новачка

  7. Йдемо в налаштування облікового запису, вибираємо Personal Access Token
    Посібник з CI/CD у GitLab для (майже) абсолютного новачка

  8. Створюємо новий токен доступу

    1. Ім'я – довільне
    2. Організація - поточна
    3. Термін дії - максимум 1 рік
    4. Область дії (scope) – Packaging/Read & Write
      Посібник з CI/CD у GitLab для (майже) абсолютного новачка

  9. Копіюємо створений токен - після закриття модального вікна значення буде недоступним

  10. Заходимо в налаштування репозиторію в GitLab, вибираємо налаштування CI/CD
    Посібник з CI/CD у GitLab для (майже) абсолютного новачка

  11. Розкриваємо блок Variables, додаємо нову

    1. Ім'я - будь-яке без пробілів (буде доступне в командній оболонці)
    2. Значення – токен доступу з п. 9
    3. Вибираємо Mask variable
      Посібник з CI/CD у GitLab для (майже) абсолютного новачка

На цьому попереднє налаштування завершено.

Готуємо каркас конфігурації

За замовчуванням, для налаштування 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 та додавання коммітів у майстер:

pack and deploy job:
  # snip
  only:
    - merge_request
    - master

Як видно, все просто та прямолінійно.

Також можна налаштувати завдання на спрацювання лише якщо створено merge request з певною цільовою або вихідною гілкою:

  rules:
    - if: $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "master"

В умовах можна використовувати перелічені тут змінні; правила rules не сумісні з правилами only/except.

Налаштування збереження артефактів

Під час виконання завдання build job у нас будуть створені артефакти складання, які можна перевикористовувати у наступних завданнях. Для цього потрібно в конфігурацію завдання додати шляхи, файли якими потрібно буде зберегти і перевикористовувати в наступних задачах, в ключ artifacts:

build job:
  # snip
  artifacts:
    paths:
      - path/to/build/artifacts
      - another/path
      - MyCoolLib.*/bin/Release/*

Шляхи підтримують wildcards, що спрощує їх завдання.

Якщо завдання створює артефакти, то кожна наступна задача зможе їх отримати до них доступ - вони будуть розташовуватися по тих же шляхах щодо кореня репозиторію, якими були зібрані з вихідного завдання. Також артефакти доступні для скачування на сайті.

Тепер, коли у нас готовий (і перевірений) каркас конфігурації, можна переходити до написання скриптів для завдань.

Пишемо скрипти

Можливо, колись давно, в далекій галактиці, збирати проекти (у тому числі і на .net) з командного рядка було болем. Зараз же зібрати, протестувати та опублікувати проект можна у 3 команди:

dotnet build
dotnet test
dotnet pack

Звичайно, є деякі нюанси, через які ми дещо ускладнимо команди.

  1. Ми хочемо релізне, а не налагоджувальне складання, тому до кожної команди додаємо -c Release
  2. При тестуванні ми хочемо збирати дані про покриття коду, тому потрібно підключити аналізатор покриття до тестових бібліотек:
    1. До всіх тестових бібліотек слід додати пакет coverlet.msbuild: dotnet add package coverlet.msbuild з папки проекту
    2. До команди запуску тестів додамо /p:CollectCoverage=true
    3. До конфігурації завдання тестування додамо ключ для отримання результатів покриття (див. нижче)
  3. При упаковці коду в nuget-пакети задамо вихідну директорію для пакетів: -o .

Збираємо дані покриття коду

Coverlet після запуску тестів виводить у консоль статистику із запуску:

Calculating coverage result...
  Generating report 'C:Usersxxxsourcereposmy-projectmyProject.testscoverage.json'

+-------------+--------+--------+--------+
| Module      | Line   | Branch | Method |
+-------------+--------+--------+--------+
| project 1   | 83,24% | 66,66% | 92,1%  |
+-------------+--------+--------+--------+
| project 2   | 87,5%  | 50%    | 100%   |
+-------------+--------+--------+--------+
| project 3   | 100%   | 83,33% | 100%   |
+-------------+--------+--------+--------+

+---------+--------+--------+--------+
|         | Line   | Branch | Method |
+---------+--------+--------+--------+
| Total   | 84,27% | 65,76% | 92,94% |
+---------+--------+--------+--------+
| Average | 90,24% | 66,66% | 97,36% |
+---------+--------+--------+--------+

GitLab дозволяє вказати регулярний вираз для отримання статистики, яку потім можна отримати у вигляді бейджу. Регулярний вираз вказується в налаштуваннях завдання з ключем coverage; у виразі має бути присутнім capture-група, значення якої і буде передано в бейдж:

test and cover job:
  # snip
  coverage: /|s*Totals*|s*(d+[,.]d+%)/

Тут ми отримуємо статистику з рядка із загальним покриттям по лініях.

Публікуємо пакети та документацію

Обидві дії у нас призначені на останній етап пайплайну — якщо вже складання та тести пройшли, можна й поділитися зі світом напрацюваннями.

Для початку розглянемо публікацію у джерело пакетів:

  1. Якщо в проекті немає файлу конфігурації nuget (nuget.config), створимо новий: dotnet new nugetconfig

    Навіщо: в образі може бути заборонено доступ на запис до глобальних (користувацької та машинної) конфігурацій. Щоб не ловити помилки, просто створимо нову локальну конфігурацію і працюватимемо з нею.

  2. Додамо до локальної конфігурації нове джерело пакетів: nuget sources add -name <name> -source <url> -username <organization> -password <gitlab variable> -configfile nuget.config -StorePasswordInClearText
    1. name - локальне ім'я джерела, не принципово
    2. url - URL джерела з етапу "Готуємо акаунти", п. 6
    3. organization - Назва організації в Azure DevOps
    4. gitlab variable - Ім'я змінної з токеном доступу, доданої в GitLab ("Готуємо акаунти", п. 11). Звичайно, у форматі $variableName
    5. -StorePasswordInClearText - хак для обходу помилки відмови у доступі (не я перший на ці граблі настав)
    6. На випадок помилок може бути корисним додати -verbosity detailed
  3. Відправляємо пакет у джерело: nuget push -source <name> -skipduplicate -apikey <key> *.nupkg
    1. Надсилаємо всі пакети з поточної директорії, тому *.nupkg.
    2. name - З кроку вище.
    3. key - Будь-який рядок. В Azure DevOps у вікні Connect to feed завжди як приклад наводять рядок az.
    4. -skipduplicate — при спробі відправити існуючий пакет без цього ключа джерело поверне помилку 409 Conflict; з ключем відправлення буде пропущено.

Тепер налаштуємо створення документації:

  1. Для початку, у репозиторії, у гілці master, ініціалізуємо проект docfx. Для цього з кореня треба виконати команду docfx init та в інтерактивному режимі задамо ключові параметри для збирання документації. Детальний опис мінімального налаштування проекту тут.
    1. При налаштуванні важливо вказати вихідну директорію ..public — GitLab за промовчанням бере вміст папки public у корені репозиторію як джерело для Pages. Т.к. проект розташовуватиметься у вкладеній в репозиторій папці - додаємо в дорогу вихід на рівень вгору.
  2. Відправимо зміни до GitLab.
  3. У конфігурацію пайплайну додамо завдання pages (зарезервоване слово для завдань публікації сайтів у GitLab Pages):
    1. Скрипт:
      1. nuget install docfx.console -version 2.51.0 - Встановить docfx; версія вказана для гарантії правильності шляхів інсталяції пакета.
      2. .docfx.console.2.51.0toolsdocfx.exe .docfx_projectdocfx.json - Збираємо документацію
    2. Вузол artifacts:

pages:
  # snip
  artifacts:
    paths:
      - public

Ліричний відступ про docfx

Раніше при налаштуванні проекту я вказував джерело коду документації як файл рішення. Основний мінус – документація створюється і для тестових проектів. Якщо це не потрібно, можна задати таке значення вузлу metadata.src:

{
  "metadata": [
    {
      "src": [
        {
          "src": "../",
          "files": [
            "**/*.csproj"
          ],
          "exclude":[
            "*.tests*/**"
          ]
        }
      ],
      // --- snip ---
    },
    // --- snip ---
  ],
  // --- snip ---
}

  1. metadata.src.src: "../" - Виходимо на рівень вгору щодо розташування docfx.json, т.к. у патернах не працює пошук нагору по дереву директорій.
  2. metadata.src.files: ["**/*.csproj"] - Глобальний патерн, збираємо всі проекти C# з усіх директорій.
  3. 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:

Посібник з CI/CD у GitLab для (майже) абсолютного новачка

Бейдж із посиланням на документацію я створював на платформі Shields.io - там все досить прямолінійно, можна створити свій бейдж та отримувати його за допомогою запиту.

![Пример с Shields.io](https://img.shields.io/badge/custom-badge-blue)

Посібник з CI/CD у GitLab для (майже) абсолютного новачка

Azure DevOps Artifacts також дозволяє створювати бейджі для пакетів із зазначенням актуальної версії. Для цього у джерелі на сайті Azure DevOps потрібно натиснути на Create badge у вибраного пакета та скопіювати markdown-розмітку:

Посібник з CI/CD у GitLab для (майже) абсолютного новачка

Посібник з CI/CD у GitLab для (майже) абсолютного новачка

Додаємо краси

Виділяємо загальні фрагменти конфігурації

Під час написання конфігурації та пошуків документації, я натрапив на цікаву можливість YAML — перевикористання фрагментів.

Як видно з налаштувань завдань, усі вони вимагають наявності тега windows у раннера, і спрацьовують при надсиланні в майстер/створенні запиту на злиття (крім документації). Додамо це у фрагмент, який перевикористовуватимемо:

.common_tags: &common_tags
  tags:
    - windows
.common_only: &common_only
  only:
    - merge_requests
    - master

І тепер в описі завдання можемо вставити раніше оголошений фрагмент:

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 та автоматичним визначенням цільового оточення, що буде освітлено в наступній статті.

Джерело: habr.com

Додати коментар або відгук