O cómo conseguir bonitos badges para tu proyecto en una tarde de codificación sin estrés
Seguramente, a cada desarrollador que tiene al menos un proyecto personal en algún momento le pica la curiosidad sobre bonitos badges con estados, cobertura de código, versiones de paquetes en nuget... Y esa curiosidad me llevó a escribir este artículo. En el proceso de preparación para su redacción, conseguí esta belleza en uno de mis proyectos:

En este artículo se revisará la configuración básica de integración y entrega continua para un proyecto de biblioteca de clases en .Net Core en GitLab, incluyendo la publicación de documentación en GitLab Pages y el envío de paquetes construidos a un feed privado en Azure DevOps.
Como entorno de desarrollo se utilizó VS Code con la extensión (para validar el archivo de configuración directamente desde el entorno de desarrollo).
Introducción breve
¿CD es cuando acabas de hacer push y al cliente ya se le ha caído todo?
Qué es CI/CD y para qué sirve se puede encontrar fácilmente en Google. También es fácil encontrar documentación completa sobre cómo configurar pipelines en GitLab . Aquí describiré brevemente y, en la medida de lo posible, sin errores el proceso de funcionamiento del sistema desde una perspectiva general:
- el desarrollador envía un commit al repositorio, crea un merge request a través del sitio web, o de alguna otra manera, explícita o implícitamente, inicia el pipeline,
- de la configuración se seleccionan todas las tareas cuyas condiciones permiten ejecutarlas en este contexto,
- las tareas se organizan según sus etapas,
- las etapas se ejecutan una tras otra, es decir, en paralelo se ejecutan todas las tareas de esa etapa,
- si una etapa falla (es decir, si al menos una de las tareas de la etapa falla) — el pipeline se detiene (casi siempre),
- si todas las etapas se completan exitosamente, el pipeline se considera que ha pasado correctamente.
Así, tenemos:
- un pipeline es un conjunto de tareas, organizadas en etapas, donde se puede compilar, probar, empaquetar el código, desplegar la construcción lista en un servicio en la nube, etc.
- etapa (stage) — unidad de organización del pipeline, contiene 1+ tarea,
- tarea (job) — unidad de trabajo en el pipeline. Consiste en un script (obligatorio), condiciones de ejecución, configuraciones de publicación/caché de artefactos y mucho más.
Por lo tanto, la tarea al configurar CI/CD se reduce a crear un conjunto de tareas que implementen todas las acciones necesarias para compilar, probar y publicar el código y los artefactos.
Antes de empezar: ¿por qué?
- ¿Por qué GitLab?
Porque cuando surgió la necesidad de crear repositorios privados para proyectos personales, en GitHub eran de pago, y yo soy tacaño. Los repositorios se volvieron gratuitos, pero hasta ahora eso no es suficiente motivo para mudarme a GitHub.
- ¿Por qué no Azure DevOps Pipelines?
Porque ahí la configuración es elemental: ni siquiera se requieren conocimientos de línea de comandos. La integración con proveedores externos de git se hace en un par de clics, importar claves SSH para enviar commits al repositorio también es así, el pipeline se configura fácilmente incluso sin plantilla.
Posición inicial: qué hay y qué se desea
Tenemos:
- un repositorio en GitLab.
Queremos:
- compilación automática y pruebas para cada merge request,
- compilación de paquetes para cada merge request y push a master, siempre que haya una línea específica en el mensaje de commit,
- envío de los paquetes compilados a un feed privado en Azure DevOps,
- compilación de la documentación y publicación en GitLab Pages,
- ¡badges!11
Los requisitos descritos se alinean perfectamente con el siguiente modelo de pipeline:
- Etapa 1 — Compilación
- Compilamos el código, los archivos de salida se publican como artefactos
- Etapa 2 — Pruebas
- Obtenemos los artefactos de la etapa de compilación, ejecutamos las pruebas y recolectamos los datos de cobertura del código
- Etapa 3 — Envío
- Tarea 1 — compilamos el paquete nuget y lo enviamos a Azure DevOps
- Tarea 2 — compilamos el sitio desde xmldoc en el código fuente y lo publicamos en GitLab Pages
¡Empecemos!
Compilamos la configuración
Preparamos las cuentas
Creamos una cuenta en
Vamos a
Creamos un nuevo proyecto
- Nombre — cualquier cosa
- Visibilidad — cualquier

Al hacer clic en el botón Crear, se creará el proyecto y se redirigirá a su página. En esta página se pueden deshabilitar las funcionalidades innecesarias yendo a la configuración del proyecto (enlace inferior en la lista de la izquierda -> Overview -> bloque Azure DevOps Services)

Vamos a Atrifacts, hacemos clic en Crear feed
- Ingresamos el nombre de la fuente
- Seleccionamos la visibilidad
- Desmarcamos la casilla Incluir paquetes de fuentes públicas comunes, para que la fuente no se convierta en un basurero clon de nuget

Hacemos clic en Conectar al feed, seleccionamos Visual Studio, del bloque Configuración de máquina copiamos la Fuente

Vamos a la configuración de la cuenta, seleccionamos Token de acceso personal

Creamos un nuevo token de acceso
- Nombre — arbitrario
- Organización — actual
- La duración es de un máximo de 1 año
- Ámbito (scope) — Packaging/Read & Write

Copiamos el token creado — después de cerrar la ventana modal, el valor no estará disponible
Accedemos a la configuración del repositorio en GitLab, seleccionamos la configuración de CI/CD

Desplegamos el bloque Variables, añadimos uno nuevo
- Nombre — cualquier nombre sin espacios (disponible en la línea de comandos)
- Valor — el token de acceso del punto 9
- Seleccionamos Mask variable

Con esto, la configuración preliminar está completa.
Preparamos el esqueleto de configuración
De forma predeterminada, para configurar CI/CD en GitLab se utiliza un archivo .gitlab-ci.yml en la raíz del repositorio. Se puede establecer una ruta arbitraria a este archivo en la configuración del repositorio, pero en este caso no es necesario.
Como se puede ver por la extensión, el archivo contiene configuración en formato YAML. La documentación describe detalladamente qué claves pueden contenerse en el nivel superior de configuración y en cada uno de los niveles anidados.
Primero añadiremos al archivo de configuración un enlace a la imagen de docker, en la que se ejecutarán las tareas. Para esto, encontramos . Hay hay una guía detallada sobre qué imagen elegir para diferentes tareas. Para nuestra compilación, elegiremos la imagen con .Net Core 3.1, así que simplemente añadimos la siguiente línea en la configuración
image: mcr.microsoft.com/dotnet/core/sdk:3.1A partir de ahora, al ejecutar el pipeline almacenado en imágenes de Microsoft, se descargará la imagen especificada, en la que se ejecutarán todas las tareas de la configuración.
El siguiente paso es añadir stageetapas. Por defecto, GitLab define 5 etapas:
.pre— se ejecuta antes de todas las etapas,.post— se ejecuta después de todas las etapas,build— la primera después de.prela etapa,test— segunda etapa,deploy— tercera etapa.
No hay nada que impida declararlas explícitamente, sin embargo. El orden en el que se indican las etapas influye en el orden en que se ejecutan. Para ser completos, añadamos a la configuración:
stages:
- build
- test
- deployPara depurar, tiene sentido obtener información sobre el entorno en el que se ejecutan las tareas. Añadamos un conjunto global de comandos que se ejecutará antes de cada tarea, usando before_script:
before_script:
- $PSVersionTable.PSVersion
- dotnet --version
- nuget help | select-string VersionSolo queda añadir al menos una tarea para que al enviar commits se inicie el pipeline. Por ahora, añadiremos una tarea vacía para demostración:
dummy job:
script:
- echo okIniciamos la validación, recibimos un mensaje de que todo está bien, hacemos commit, pusheamos, miramos los resultados en el sitio... Y obtenemos un error de script — bash: .PSVersion: comando no encontrado. ¿Qué demonios?
Todo tiene sentido: por defecto, los runners (responsables de ejecutar los scripts de tareas y proporcionados por GitLab) utilizan bash para ejecutar comandos. Podemos solucionar esto, indicando explícitamente en la descripción de la tarea, qué etiquetas deben tener el runner del pipeline ejecutor:
tarea dummy en windows:
script:
- echo ok
tags:
- windows¡Excelente! Ahora el pipeline se ejecuta.
El lector atento, al repetir los pasos indicados, notará que la tarea se ejecutó en la etapa test, aunque no especificamos una etapa. Como se puede deducir, test es la etapa por defecto.
Continuemos creando la estructura de configuración, añadiendo todas las tareas descritas anteriormente:
tarea de build:
script:
- echo "building..."
tags:
- windows
stage: build
tarea de test y cobertura:
script:
- echo "running tests and coverage analysis..."
tags:
- windows
stage: test
tarea de empaquetado y despliegue:
script:
- echo "packing and pushing to nuget..."
tags:
- windows
stage: deploy
páginas:
script:
- echo "creating docs..."
tags:
- windows
stage: deployObtenemos un pipeline que, aunque no es especialmente funcional, es correcto.
Configuración de triggers
Dado que no se han especificado filtros de activación para ninguna de las tareas, el pipeline se completamente ejecutará con cada envío de commits al repositorio. Dado que este no es el comportamiento deseado en general, configuraremos filtros de activación para las tareas.
Los filtros se pueden configurar en dos formatos: y . En resumen, only/except permite configurar filtros por triggers (merge_request, por ejemplo, configura la tarea para ejecutarse en cada creación de un merge request y en cada envío de commits a la rama que es origen del merge request) y nombres de ramas (incluyendo el uso de expresiones regulares); rules permite establecer un conjunto de condiciones y, opcionalmente, cambiar la condición de ejecución de la tarea según el éxito de tareas anteriores ().
Recordemos el conjunto de requisitos: compilación y pruebas solo para merge request, empaquetado y envío a Azure DevOps — para merge requests y pushes a master, generación de documentación — para pushes a master.
Para comenzar, configuraremos una tarea de compilación de código, añadiendo una regla de activación solo para merge request:
tarea de build:
# snip
only:
- merge_requestAhora configuraremos la tarea de empaquetado para activarse en merge request y en la adición de commits a master:
paquetar y desplegar trabajo:
# snip
solo:
- merge_request
- masterComo se puede ver, es bastante simple y directo.
También se puede configurar la tarea para que se active solo si se crea un merge request con una rama objetivo o de origen específica:
reglas:
- si: $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "master"En las condiciones se pueden utilizar ; las reglas rules no son compatibles con las reglas only/except.
Configuración de la conservación de artefactos
Durante la ejecución de la tarea trabajo de construcción se crearán artefactos de construcción que se pueden reutilizar en tareas posteriores. Para ello, se deben añadir en la configuración de la tarea las rutas y archivos que se deben conservar y reutilizar en las siguientes tareas, en la clave :
trabajo de construcción:
# snip
artefactos:
rutas:
- path/to/build/artifacts
- another/path
- MyCoolLib.*/bin/Release/*Las rutas admiten comodines, lo que sin duda facilita su designación.
Si la tarea crea artefactos, entonces cada tarea posterior podrá acceder a ellos; estarán ubicados en las mismas rutas relativas a la raíz del repositorio, desde donde se recopilaron de la tarea de origen. Además, los artefactos están disponibles para descargar en el sitio.
Ahora que tenemos el esqueleto de la configuración listo (y verificado), podemos pasar a escribir los scripts para las tareas.
Escribimos scripts
Quizás, hace mucho tiempo, en una galáxia muy, muy lejana, compilar proyectos (incluidos los de .net) desde la línea de comandos era un dolor. Sin embargo, ahora se puede compilar, probar y publicar un proyecto en 3 comandos:
dotnet build
dotnet test
dotnet packPor supuesto, hay algunos detalles que complican un poco los comandos.
- Queremos una compilación de lanzamiento, no de depuración, por lo que añadimos
-c Release - Al probar, queremos recopilar datos sobre la cobertura del código, por lo que será necesario habilitar el analizador de cobertura en las bibliotecas de prueba:
- Se debe agregar el paquete
coverlet.msbuild:dotnet add package coverlet.msbuilddesde la carpeta del proyecto - En el comando de ejecución de pruebas añadiremos
/p:CollectCoverage=true - En la configuración de la tarea de pruebas agregaremos una clave para obtener los resultados de cobertura (ver más abajo)
- Se debe agregar el paquete
- Al empaquetar el código en paquetes nuget, estableceremos el directorio de salida para los paquetes:
-o .
Recopilando datos de cobertura del código
Coverlet muestra en la consola las estadísticas de ejecución después de ejecutar las pruebas:
Calculando el resultado de la cobertura...
Generando el informe 'C:Usersxxxsourcereposmy-projectmyProject.testscoverage.json'
+-------------+--------+--------+--------+
| Módulo | Línea | Rama | Método |
+-------------+--------+--------+--------+
| proyecto 1 | 83,24% | 66,66% | 92,1% |
+-------------+--------+--------+--------+
| proyecto 2 | 87,5% | 50% | 100% |
+-------------+--------+--------+--------+
| proyecto 3 | 100% | 83,33% | 100% |
+-------------+--------+--------+--------+
+---------+--------+--------+--------+
| | Línea | Rama | Método |
+---------+--------+--------+--------+
| Total | 84,27% | 65,76% | 92,94% |
+---------+--------+--------+--------+
| Promedio| 90,24% | 66,66% | 97,36% |
+---------+--------+--------+--------+GitLab permite especificar una expresión regular para obtener estadísticas, que luego se pueden obtener en forma de insignia. La expresión regular se indica en la configuración de la tarea con la clave coverage; en la expresión debe haber un grupo de captura, cuyo valor será transmitido a la insignia:
trabajo de pruebas y cobertura:
# snip
coverage: \/|s*Totales*s*(d+[,.]d+%)\/Aquí obtenemos estadísticas de la línea total de cobertura.
Publicamos paquetes y documentación
Ambas acciones están programadas para la última etapa del pipeline; dado que la construcción y las pruebas se han completado, podemos compartir nuestro trabajo con el mundo.
Primero, veamos la publicación en la fuente de paquetes:
Si en el proyecto no hay un archivo de configuración de nuget (
nuget.config), crearemos uno nuevo:dotnet new nugetconfig¿Por qué? en la imagen puede estar prohibido el acceso de escritura a las configuraciones globales (de usuario y de máquina). Para evitar errores, simplemente crearemos una nueva configuración local y trabajaremos con ella.
- Agreguemos una nueva fuente de paquetes a la configuración local:
nuget sources add -name <name> -source <url> -username <organization> -password <gitlab variable> -configfile nuget.config -StorePasswordInClearTextname— nombre local de la fuente, no es críticourl— URL de la fuente de la etapa "Preparando cuentas", p. 6organization— nombre de la organización en Azure DevOpsgitlab variable— nombre de la variable con el token de acceso, añadida en GitLab ("Preparando cuentas", p. 11). Por supuesto, en el formato$variableName-StorePasswordInClearText— hack para evitar el error de acceso denegado ()- En caso de errores, puede ser útil añadir
-verbosity detailed
- Enviamos el paquete a la fuente:
nuget push -source <name> -skipduplicate -apikey <key> *.nupkg- Enviamos todos los paquetes del directorio actual, por lo que
*.nupkg. name— de la etapa anterior.key— cualquier cadena. En Azure DevOps, en la ventana Conectar a feed siempre se menciona como ejemplo la cadenaaz.-skipduplicate— al intentar enviar un paquete ya existente sin esta clave, el origen devolverá un error409 Conflicto; con la clave, el envío será omitido.
- Enviamos todos los paquetes del directorio actual, por lo que
Ahora configuraremos la creación de la documentación:
- Para comenzar, en el repositorio, en la rama master, inicializamos el proyecto docfx. Para ello desde la raíz debemos ejecutar el comando
docfx inity en modo interactivo estableceremos los parámetros clave para la construcción de la documentación. Una descripción detallada de la configuración mínima del proyecto .- Al configurar, es importante especificar el directorio de salida
..public— GitLab por defecto toma el contenido de la carpeta public en la raíz del repositorio como fuente para Pages. Dado que el proyecto se ubicará en una carpeta anidada dentro del repositorio, añadimos a la ruta la salida un nivel hacia arriba.
- Al configurar, es importante especificar el directorio de salida
- Enviar cambios a GitLab.
- En la configuración del pipeline añadimos la tarea
pages(palabra reservada para las tareas de publicación de sitios en GitLab Pages):- Script:
nuget install docfx.console -version 2.51.0— instalará docfx; la versión se indica para garantizar la corrección de las rutas de instalación del paquete..docfx.console.2.51.0toolsdocfx.exe .docfx_projectdocfx.json— recopilamos la documentación
- Nodo artifacts:
- Script:
pages:
# snip
artifacts:
paths:
- publicUna digresión lírica sobre docfx
Anteriormente, al configurar el proyecto, indicaba la fuente de código para la documentación como archivo de solución. La principal desventaja es que la documentación se crea también para los proyectos de prueba. En caso de que esto no sea necesario, se puede establecer tal valor en el nodo metadata.src:
{
"metadata": [
{
"src": [
{
"src": "..\/",
"files": [
"**/*.csproj"
],
"exclude":[
"*.tests*\/**"
]
}
],
\/\/ --- snip ---
},
\/\/ --- snip ---
],
\/\/ --- snip ---
}metadata.src.src: "..\/"— subimos un nivel en relación a la ubicacióndocfx.json, dado que en los patrones no funciona la búsqueda hacia arriba en el árbol de directorios.metadata.src.files: ["**/*.csproj"]— patrón global, recopilamos todos los proyectos C# de todos los directorios.metadata.src.exclude: ["*.tests*\/**"]— patrón global, excluimos todo de las carpetas con.testsen el nombre
Resultado intermedio
Esta configuración simple se puede elaborar en literalmente media hora y un par de tazas de café, que permitirá comprobar en cada solicitud de fusión y envío a master que el código se compila y las pruebas pasan, recopilar un nuevo paquete, actualizar la documentación y alegrar la vista con bonitos distintivos en el README del proyecto.
El archivo final .gitlab-ci.yml
imagen: 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:
- masterPor cierto, sobre las insignias
¡Todo esto fue por ellas!
Las insignias con los estados del pipeline y la cobertura del código están disponibles en GitLab en la configuración de CI/CD en el bloque de Gtntral pipelines:

La insignia con enlace a la documentación la creé en la plataforma — aquí todo es bastante sencillo, puedes crear tu propia insignia y obtenerla a través de una solicitud.

Azure DevOps Artifacts también permite crear insignias para paquetes indicando la versión actual. Para esto, en la fuente del sitio de Azure DevOps, debes hacer clic en Crear insignia en el paquete seleccionado y copiar el markdown:


Añadiendo un toque estético
Destacamos los fragmentos comunes de la configuración
Mientras escribía la configuración y buscaba en la documentación, me encontré con una interesante característica de YAML: la reutilización de fragmentos.
Como se puede ver en la configuración de las tareas, todas requieren la presencia de la etiqueta windows en el runner, y se activan al enviar a master/creando una solicitud de fusión (excepto la documentación). Agreguemos esto al fragmento que reutilizaremos:
.common_tags: &common_tags
tags:
- windows
.common_only: &common_only
only:
- merge_requests
- masterY ahora en la descripción de la tarea podemos insertar el fragmento previamente declarado:
build job:
<<: *common_tags
<<: *common_onlyLos nombres de los fragmentos deben comenzar con un punto para no ser interpretados como tareas.
Versionado de paquetes
Al crear un paquete, el compilador verifica las claves de la línea de comandos y, si no están presentes, los archivos de proyecto; al encontrar el nodo Version, toma su valor como la versión del paquete que se está construyendo. Por lo tanto, para construir un paquete con una nueva versión, es necesario actualizarla en el archivo del proyecto o pasarla como argumento en la línea de comandos.
Agreguemos otro deseo: que los dos últimos números en la versión sean el año y la fecha de la construcción del paquete, y añadamos versiones previas. Se pueden añadir estos datos al archivo del proyecto y verificarlos antes de cada envío, claro, pero también se puede hacer en la canalización, construyendo la versión del paquete a partir del contexto y pasándola como argumento de la línea de comandos.
Convenimos que si en el mensaje de confirmación hay una línea del tipo release (v.\/ver.\/version) <número de versión> (rev.\/revisión <revisión>)?, tomaremos la versión del paquete de esta línea, le añadiremos la fecha actual y la pasaremos como argumento al comando dotnet pack. Si no hay línea, simplemente no construiremos el paquete.
Esta tarea la resuelve el siguiente 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=$versionAgregamos el script a la tarea pack and deploy job y observamos la construcción de paquetes estrictamente en presencia de la línea especificada en el mensaje de confirmación.
Total
Después de gastar aproximadamente media hora a una hora en la redacción de la configuración, depuración en powershell local y, quizás, un par de intentos fallidos, conseguimos una configuración sencilla para automatizar tareas rutinarias.
Por supuesto, GitLab CI/CD es mucho más amplio y complejo de lo que puede parecer después de leer esta guía - . Allí incluso hay , que permite
detecta automáticamente, construye, prueba, despliega y monitorea tus aplicaciones
Ahora, en los planes está configurar la canalización para desplegar aplicaciones en Azure, utilizando Pulumi y la detección automática del entorno objetivo, lo que se cubrirá en el próximo artículo.
Fuente: habr.com








