Sau cum să obții etichete frumoase pentru proiectul tău într-o seară de codare relaxată
Probabil că fiecare dezvoltator care are măcar un proiect personal, la un moment dat, simte nevoia de etichete frumoase cu stări, acoperiri de cod, versiuni de pachete în nuget... Și această nevoie m-a dus la scrierea acestui articol. În procesul de pregătire a acestuia, am obținut o astfel de frumusețe într-unul dintre proiectele mele:

Articolul va discuta configurarea de bază a integrării continue și a livrării pentru un proiect de bibliotecă de clase pe .Net Core în GitLab, cu publicarea documentației în GitLab Pages și trimiterea pachetelor compilate într-un feed privat în Azure DevOps.
Ca mediu de dezvoltare a fost folosit VS Code cu extensia (pentru validarea fișierului de configurare chiar din mediu).
Introducere scurtă
CD - este atunci când abia ai făcut push și clientul deja a căzut tot?
Ce este CI/CD și de ce este necesar - se poate căuta ușor. Documentația completă pentru configurarea pipeline-urilor în GitLab poate fi găsită . Aici voi descrie pe scurt și, pe cât posibil, fără greșeli, procesul de funcționare al sistemului dintr-o perspectivă de ansamblu:
- dezvoltatorul trimite un commit în repository, creează un merge request prin site, sau într-un alt mod, explicit sau implicit, pornește un pipeline,
- din configurație sunt selectate toate sarcinile ale căror condiții permit să fie pornite în acest context,
- sarcinile sunt organizate conform etapelor lor,
- etapele sunt executate pe rând — adică, în paralel sunt executate toate sarcinile din această etapă,
- dacă etapa se încheie cu eșec (adică dacă cel puțin una dintre sarcinile etapei eșuează) — pipeline-ul se oprește (aproape întotdeauna),
- dacă toate etapele se finalizează cu succes, pipeline-ul este considerat că a trecut cu succes.
Astfel, avem:
- pipeline - un set de sarcini, organizate în etape, în care se poate construi, testa, împacheta codul, desfășura compilarea finală în serviciul cloud, și altele.
- etapă (stage) - unitatea de organizare a pipeline-ului, conține 1+ sarcină,
- sarcină (job) - unitatea de lucru în pipeline. Constă dintr-un script (obligatoriu), condiții de lansare, setări de publicare/cache pentru artefacte și multe altele.
Așadar, sarcina în configurarea CI/CD se reduce la crearea unui set de sarcini care să implementeze toate acțiunile necesare pentru construirea, testarea și publicarea codului și artefactelor.
Înainte de a începe: de ce?
- De ce GitLab?
Pentru că, atunci când a apărut necesitatea de a crea depozite private pentru proiecte personale, acestea erau plătite pe GitHub, iar eu eram zgârcit. Depozitele au devenit gratuite, dar pentru mine asta nu este un motiv suficient pentru a mă muta pe GitHub.
- De ce nu Azure DevOps Pipelines?
Pentru că acolo setarea este simplă — nici măcar nu ai nevoie de cunoștințe de linie de comandă. Integrarea cu furnizorii externi de git se face în câteva clicuri, importul cheilor SSH pentru a trimite commit-uri în depozit — de asemenea, pipeline-ul se configurează ușor chiar și fără un șablon.
Poziția de start: ce avem și ce ne dorim
Avem:
- un depozit în GitLab.
Ne dorim:
- construirea și testarea automată pentru fiecare merge request,
- construirea pachetelor pentru fiecare merge request și pus în master cu condiția ca mesajul commit-ului să conțină un anumit șir,
- trimiterea pachetelor construite într-un feed privat în Azure DevOps,
- construirea documentației și publicarea în GitLab Pages,
- badge-uri!11
Cerințele descrise se potrivesc perfect în următorul model de pipeline:
- Etapa 1 — construcția
- Construim codul, publicăm fișierele generate ca artefacte
- Etapa 2 — testare
- Obținem artefactele de la etapa de construcție, rulăm teste, colectăm datele de acoperire a codului
- Etapa 3 — trimitere
- Sarcina 1 — construim un pachet nuget și îl trimitem în Azure DevOps
- Sarcina 2 — construim site-ul din xmldoc în codul sursă și publicăm în GitLab Pages
Să începem!
Construim configurația
Pregătim conturile
Creăm un cont în
Trecem la
Creăm un proiect nou
- Numele — oricum
- Vizibilitate — oricum

Când apăsați butonul Create, proiectul va fi creat și se va face trecerea la pagina sa. Pe această pagină se pot dezactiva opțiunile inutile, mergând în setările proiectului (link-ul de jos din lista din stânga -> Overview -> blocul Azure DevOps Services)

Trecem la Atrifacts, facem clic pe Create feed
- Introducem numele sursei
- Selectăm vizibilitatea
- Debifăm caseta Include packages from common public sources, pentru a evita ca sursa să devină o gunoaie de clone nuget

Facem clic pe Connect to feed, alegem Visual Studio, din blocul Machine Setup copiem Source

Mergem în setările contului, alegem Personal Access Token

Creăm un nou token de acces
- Numele — la alegere
- Organizație — curentă
- Perioada de valabilitate — maxim 1 an
- Domeniul de aplicare (scope) — Packaging/Read & Write

Copiem tokenul creat — după închiderea feronței, valoarea va fi indisponibilă
Accesăm setările repository-ului în GitLab, alegem setările CI/CD

Deschidem blocul Variables, adăugăm un nou element
- Nume — orice, fără spații (va fi disponibil în shell-ul de comandă)
- Valoare — tokenul de acces din pct. 9
- Alegem Mask variable

Acesta este finalul configurării preliminare.
Pregătim cadrul de configurare
Implicit, pentru configurarea CI/CD în GitLab se folosește fișierul .gitlab-ci.yml din rădăcina repository-ului. Se poate seta o cale personalizată pentru acest fișier în setările repository-ului, dar în acest caz nu este necesar.
După cum se vede din extensie, fișierul conține configurația în format YAML. Documentația detaliază ce chei pot fi incluse la nivelul superior al configurației și în fiecare nivel încorporat.
Începem prin a adăuga în fișierul de configurație un link către imaginea docker în care se vor executa sarcinile. Pentru asta, găsim . În există un ghid detaliat despre ce imagine să alegi pentru diferite sarcini. Pentru construcție, imaginea cu .Net Core 3.1 se potrivește, așa că o adăugăm fără ezitare în prima linie a configurației
image: mcr.microsoft.com/dotnet/core/sdk:3.1Acum, atunci când se lansează pipeline-ul din repository-ul imaginilor Microsoft, imaginea specificată va fi descărcată și toate sarcinile din configurație vor fi executate în aceasta.
Următorul pas — adăugarea stageetapelor. În mod implicit, GitLab definește 5 etape:
.pre— se execută înainte de toate etapele,.post— se execută după toate etapele,build— prima după.preetap,test— a doua etapă,deploy— a treia etapă.
Nu este nicio problemă să le declarăm explicit. Ordinea în care sunt specificate etapele influențează ordinea în care sunt executate. Pentru a fi complet, adăugăm în configurație:
stages:
- build
- test
- deployPentru depanare, are sens să obținem informații despre mediu în care se execută sarcinile. Vom adăuga un set global de comenzi care vor fi executate înainte de fiecare sarcină, folosind before_script:
before_script:
- $PSVersionTable.PSVersion
- dotnet --version
- nuget help | select-string VersionRămâne doar să adăugăm cel puțin o sarcină pentru ca, la trimiterea commit-urilor, pipeline-ul să pornească. Deocamdată, vom adăuga o sarcină goală pentru demonstrație:
dummy job:
script:
- echo okActivăm validarea, primim un mesaj că totul este bine, facem commit, împingem, ne uităm pe site la rezultate… Și primim o eroare de script — bash: .PSVersion: comanda nu a fost găsită. WTF?
Totul are sens — în mod implicit, runnerii (responsabili pentru executarea scripturilor de sarcini și furnizați de GitLab) folosesc bash pentru executarea comenzilor. Acest lucru poate fi corectat prin specificarea explicită în descrierea sarcinii a tag-urilor care trebuie să fie asociate cu runner-ul pipeline-ului executant:
job dummy pe windows:
script:
- echo ok
tags:
- windowsPerfect! Acum pipeline-ul se execută.
Cititorul atent, repetând pașii indicați, va observa că sarcina a fost finalizată în etapa test, deși nu am specificat etapa. Cum putem ghici, test este etapa implicită.
Să continuăm crearea scheletului configurației, adăugând toate sarcinile menționate mai sus:
job build:
script:
- echo "building..."
tags:
- windows
stage: build
test și job de acoperire:
script:
- echo "running tests and coverage analysis..."
tags:
- windows
stage: test
job de pachetare și implementare:
script:
- echo "packing and pushing to nuget..."
tags:
- windows
stage: deploy
pagini:
script:
- echo "creating docs..."
tags:
- windows
stage: deployAm obținut un pipeline corect, deși nu foarte funcțional.
Configurarea declanșatoarelor
Deoarece niciuna dintre sarcini nu are specificate filtre de declanșare, pipeline-ul va complet fi executat la fiecare trimitere a commit-urilor în repository. Deoarece aceasta nu este un comportament dorit în general, vom configura filtrele de declanșare pentru sarcini.
Filtrele pot fi configurate în două formate: și . Pe scurt, only/except permete configurarea filtrelor pe declanșatoare (merge_request, de exemplu — configurează sarcina să fie executată la fiecare creare a unei cereri de fuziune și la fiecare trimitere a commit-urilor în ramura care este sursa cererii de fuziune) și numele ramurilor (inclusiv utilizând expresii regulate); rules permete configurarea unui set de condiții și, opțional, modificarea condiției de executare a sarcinii în funcție de succesul sarcinilor anterioare ().
Să ne amintim setul de cerințe — compilare și testare doar pentru merge request, pachetare și trimitere în Azure DevOps — pentru merge request-uri și push-uri în master, generarea documentației — pentru push-uri în master.
Pentru început, să configurăm sarcina de compilare a codului, adăugând o regulă de declanșare doar pentru merge request:
job build:
# snip
only:
- merge_requestAcum să configurăm sarcina de pachetare pentru declanșările pe merge request și adăugarea commit-urilor în master:
pachet și desfășurare sarcină:
# snip
doar:
- cerere_fuzionare
- masterDupă cum se poate observa, totul este simplu și direct.
De asemenea, se poate configura sarcina să declanșeze doar dacă este creată o cerere de fuzionare cu o ramură țintă sau de bază specifică:
reguli:
- dacă: $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "master"În condiții se pot utiliza ; regulile rules nu sunt compatibile cu regulile only/except.
Configurarea păstrării artefactelor
În timpul executării sarcinii sarcină de construcție vor fi create artefacte de construcție, care pot fi reutilizate în sarcini ulterioare. Pentru aceasta, trebuie adăugate în configurația sarcinii căile, fișierele pe care trebuie să le salvăm și să le reutilizăm în sarcinile următoare, în cheia :
sarcină de construcție:
# snip
artefacte:
căi:
- path/to/build/artefacts
- another/path
- MyCoolLib.*/bin/Release/*Cărțile acceptă wildcard-uri, ceea ce simplifică cu siguranță definirea acestora.
Dacă sarcina creează artefacte, fiecare sarcină ulterioară le va putea accesa — acestea se vor afla pe aceleași căi în raport cu rădăcina repository-ului, pe care au fost generate din sarcina de bază. De asemenea, artefactele sunt disponibile pentru descărcare pe site.
Acum, când avem cadrul configurat (și verificat), putem trece la scrierea scripturilor pentru sarcini.
Scriem scripturi
Poate că, cândva, în urmă cu mult timp, în galaxii îndepărtate, construirea proiectelor (inclusiv .net) din linia de comandă era o durere. Acum, se poate construi, testa și publica un proiect cu 3 comenzi:
dotnet build
dotnet test
dotnet packDesigur, există unele nuanțe care vor complica puțin comenzile.
- Vrem o construcție de tip release, nu debug, așa că adăugăm la fiecare comandă
-c Release - Când testăm, dorim să colectăm date despre acoperirea codului, așa că va trebui să adăugăm un analizator de acoperire în bibliotecile de testare:
- În toate bibliotecile de testare trebuie adăugat pachetul
coverlet.msbuild:dotnet add package coverlet.msbuilddin folderul proiectului - În comanda de rulare a testelor, vom adăuga
/p:CollectCoverage=true - În configurația sarcinii de testare, vom adăuga o cheie pentru a obține rezultatele acoperirii (vezi mai jos)
- În toate bibliotecile de testare trebuie adăugat pachetul
- Când împachetăm codul în pachete nuget, vom defini directorul de ieșire pentru pachete:
-o .
Colectăm date despre acoperirea codului
Coverlet emite în consolă statistici despre rulare după execuția testelor:
Calcularea rezultatului acoperirii...
Generarea raportului 'C:Usersxxxsourcereposmy-projectmyProject.testscoverage.json'
+-------------+--------+--------+--------+
| Modul | Linie | Ramură | Metodă |
+-------------+--------+--------+--------+
| proiect 1 | 83,24% | 66,66% | 92,1% |
+-------------+--------+--------+--------+
| proiect 2 | 87,5% | 50% | 100% |
+-------------+--------+--------+--------+
| proiect 3 | 100% | 83,33% | 100% |
+-------------+--------+--------+--------+
+---------+--------+--------+--------+
| | Linie | Ramură | Metodă |
+---------+--------+--------+--------+
| Total | 84,27% | 65,76% | 92,94% |
+---------+--------+--------+--------+
| Medie | 90,24% | 66,66% | 97,36% |
+---------+--------+--------+--------+GitLab permite specificarea unei expresii regulate pentru obținerea statisticilor, care pot fi apoi obținute sub formă de badge. Expresia regulată se specifică în setările sarcinii cu cheia coverage; în expresie trebuie să existe un grup de captare, al cărui valoare va fi transmisă badge-ului:
job de testare și acoperire:
# snip
coverage: \/|s*Totals*|s*(d+[,.]d+%)\/Aici obținem statisticile dintr-o linie cu acoperirea totală pe linii.
Publicăm pachete și documentație
Ambele acțiuni sunt programate pentru ultima etapă a pipeline-ului — odată ce compilarea și testele au fost finalizate, putem împărtăși realizările noastre cu lumea.
Pentru început, să examinăm publicarea într-o sursă de pachete:
Dacă în proiect nu există un fișier de configurare nuget (
nuget.config), să creăm unul nou:dotnet new nugetconfigDe ce: în imagine poate fi interzis accesul la configurațiile globale (de utilizator și de sistem). Pentru a evita erorile, pur și simplu vom crea o nouă configurație locală și vom lucra cu ea.
- Să adăugăm în configurația locală o nouă sursă de pachete:
nuget sources add -name <name> -source <url> -username <organization> -password <gitlab variable> -configfile nuget.config -StorePasswordInClearTextname— nume local al sursei, nu este esențialurl— URL-ul sursei din etapa "Pregătim conturile", p. 6organization— numele organizației în Azure DevOpsgitlab variable— numele variabilei cu token-ul de acces, adăugate în GitLab ("Pregătim conturile", p. 11). Desigur, în formatul$variableName-StorePasswordInClearText— hack pentru a ocoli eroarea de acces interzis ()- În cazul erorilor, poate fi util să adăugăm
-verbosity detailed
- Trimitem pachetul în sursă:
nuget push -source <name> -skipduplicate -apikey <key> *.nupkg- Trimitem toate pachetele din directorul curent, așa că
*.nupkg. name— din pasul anterior.key— orice linie. În Azure DevOps, fereastra Conectează-te la feed întotdeauna prezintă ca exemplu liniaaz.-skipduplicate— la încercarea de a trimite un pachet deja existent fără această cheie, serverul va returna o eroare409 Conflict; cu cheia, trimiterea va fi omisă.
- Trimitem toate pachetele din directorul curent, așa că
Acum să configurăm generarea documentației:
- Pentru început, în repository, în ramura master, inițializăm proiectul docfx. Pentru aceasta, din rădăcină trebuie să executăm comanda
docfx initși în modul interactiv vom specifica parametrii cheie pentru generarea documentației. O descriere detaliată a configurării minime a proiectului .- La configurare este important să indicăm directorul de ieșire
..public— GitLab din default ia conținutul folderului public din rădăcina repository-ului ca sursă pentru Pages. Deoarece proiectul va fi situat într-un folder în interiorul repository-ului, adăugăm în cale ieșirea la un nivel superior.
- La configurare este important să indicăm directorul de ieșire
- Să trimitem modificările în GitLab.
- În configurația pipeline-ului vom adăuga sarcina
pages(cuvânt rezervat pentru sarcini de publicare site-uri în GitLab Pages):- Script:
nuget install docfx.console -version 2.51.0— va instala docfx; versiunea specificată pentru a garanta corectitudinea căilor de instalare a pachetului..docfx.console.2.51.0toolsdocfx.exe .docfx_projectdocfx.json— generăm documentația
- Nod artifacts:
- Script:
pages:
# snip
artifacts:
paths:
- publicO mică digresiune despre docfx
Anterior, când configuram proiectul, specificam sursa codului pentru documentație ca fișier de soluție. Principalul dezavantaj a fost că documentația era generată și pentru proiectele de testare. În cazul în care acest lucru nu este necesar, se poate specifica o astfel de valoare nodului metadata.src:
{
"metadata": [
{
"src": [
{
"src": "..\/",
"files": [
"**\/*.csproj"
],
"exclude":[
"*.tests*\/**"
]
}
],
\/\/ --- snip ---
},
\/\/ --- snip ---
],
\/\/ --- snip ---
}metadata.src.src: "..\/"— ieșim la un nivel superior în raport cu locațiadocfx.json, deoarece în modele nu funcționează căutarea în sus pe arborele de directoare.metadata.src.files: ["**\/*.csproj"]— model global, avem toate proiectele C# din toate directoarele.metadata.src.exclude: ["*.tests*\/**"]— model global, excludem totul din folderele cu.testsîn nume
Concluzie intermediară
Acest tip de configurație simplă poate fi realizat literalmente în jumătate de oră și câteva cești de cafea, care va permite la fiecare solicitare de fuziune și trimitere în master să verifice dacă codul se compilează și testele trec, să genereze un nou pachet, să actualizeze documentația și să încânte privirea cu frumoase badge-uri în README-ul proiectului.
Final .gitlab-ci.yml
imagine: 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:
- masterApropo de insigne
Pentru ele s-a început totul!
Insignele cu statusurile pipeline-ului și acoperirea codului sunt disponibile în GitLab în setările CI/CD la secțiunea Gtntral pipelines:

Am creat insigna cu linkul către documentație pe platforma — acolo totul este destul de simplu, poți crea propria insignă și o poți obține printr-o solicitare.

Azure DevOps Artifacts permite de asemenea crearea de insigne pentru pachete, indicând versiunea actuală. Pentru asta, pe site-ul Azure DevOps, trebuie să dai click pe Create badge pentru pachetul dorit și să copiezi markup-ul markdown:


Adăugăm frumusețe
Subliniem fragmentele comune de configurare
În timpul scrierii configurației și căutărilor prin documentație, am dat peste o oportunitate interesantă YAML — reutilizarea fragmentelor.
După cum se vede din setările sarcinilor, toate necesită un tag windows al runner-ului, și se activează la trimiterea în master / crearea unei cereri de fuziune (cu excepția documentației). Să adăugăm asta în fragmentul pe care îl vom reutiliza:
.common_tags: &common_tags
tags:
- windows
.common_only: &common_only
only:
- merge_requests
- masterȘi acum în descrierea sarcinii putem insera fragmentul declarat anterior:
build job:
<<: *common_tags
<<: *common_onlyNumele fragmentelor trebuie să înceapă cu un punct, pentru a nu fi interpretate ca sarcină.
Versionarea pachetelor
At the creation of the package, the compiler checks the command line keys, and in their absence — the project files; finding the node Version, it takes its value as the version of the package being built. Therefore, to build a package with a new version, either update it in the project file or pass it as a command line argument.
Let’s add another feature — let the last two digits in the version be the year and date of the package build, and add pre-release versions. One can, of course, add this data to the project file and check it before each submission — but this can also be done in the pipeline, gathering the package version from the context and passing it via the command line argument.
Let’s agree that if the commit message contains a line in the form of release (v.\/ver.\/version) <version number> (rev.\/revision <revision>)?, then we will take the package version from this string, append the current date to it, and pass it as an argument to the command dotnet pack. In the absence of the string — we will simply not build the package.
This task is solved by the following script:
# регулярное выражение для поиска строки с версией
$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=$versionAdd the script to the task pack and deploy job and monitor the package builds strictly when the specified string is present in the commit message.
În concluzie
Having spent about half an hour to an hour writing the configuration, debugging in local PowerShell, and possibly a couple of failed runs, we got a simple configuration for automating routine tasks.
Of course, GitLab CI\/CD is much broader and more multifaceted than it may seem after reading this guide — . It even has , care permite
automatically detect, build, test, deploy, and monitor your applications
Now the plans are to configure a pipeline for deploying applications in Azure, using Pulumi and automatic target environment detection, which will be covered in the next article.
Sursa: habr.com








