Azure Pipelines y Azure Artifacts: cómo ordenar la publicación de librerías .NET internas
Una explicación práctica de qué son Azure Pipelines y Azure Artifacts, para qué sirven y cómo los he usado para pasar de referencias manuales a paquetes NuGet internos más trazables y mantenibles.
Azure Pipelines y Azure Artifacts: cómo ordenar la publicación de librerías .NET internas
En muchos entornos .NET, tarde o temprano aparece el mismo problema: hay librerías compartidas que usan varios proyectos y hay que decidir cómo distribuirlas.
Durante mucho tiempo, una forma bastante habitual de resolverlo ha sido copiar DLLs, usar carpetas compartidas, referenciar binarios directamente o mantener cierta lógica manual alrededor de esas dependencias. Funciona, hasta que deja de ser cómodo. O hasta que empiezan las dudas: qué versión está usando cada proyecto, quién ha actualizado una librería, si el binario que hay en una carpeta es realmente el último, o cómo reproducir una compilación de forma limpia.
En mi caso, trabajando en integración de sistemas dentro de la organización Integrations de Dorlet, he estado dando pasos para llevar ese tipo de librerías compartidas hacia un modelo más ordenado: compilar con Azure Pipelines, generar paquetes NuGet y publicarlos en Azure Artifacts.
No voy a entrar en detalles internos de repositorios, feeds, nombres de proyectos ni configuración concreta. La idea del artículo es contar el enfoque general, qué problema resuelve y qué aprendizajes me llevo del proceso.
Qué es Azure Pipelines
Azure Pipelines es la parte de Azure DevOps que permite automatizar procesos de integración y entrega continua. Dicho de forma sencilla: sirve para ejecutar tareas cada vez que cambia el código, cuando se lanza una compilación manualmente o cuando se quiere preparar una versión.
Un pipeline puede hacer muchas cosas, pero en un flujo típico de desarrollo suele encargarse de pasos como:
- restaurar dependencias;
- compilar el código;
- ejecutar tests;
- generar artefactos;
- publicar paquetes;
- desplegar en un entorno, si aplica.
Para este caso, el objetivo principal no era desplegar una aplicación, sino automatizar la construcción y publicación de librerías .NET compartidas.
Es decir: que el proceso de generar una versión de una librería no dependiera de hacerlo a mano desde un equipo concreto, sino de un flujo reproducible y trazable.
Qué es Azure Artifacts
Azure Artifacts es el servicio de Azure DevOps para almacenar y compartir paquetes. Entre otros formatos, permite trabajar con paquetes NuGet, que es el formato habitual para distribuir librerías en el ecosistema .NET.
Un feed de Azure Artifacts funciona como un repositorio privado de paquetes. En lugar de tener una DLL en una carpeta de red, publicas un paquete NuGet con una versión concreta. Luego otros proyectos pueden consumir ese paquete como una dependencia más.
La diferencia parece pequeña, pero cambia bastante la forma de trabajar.
Con una DLL compartida manualmente, muchas veces dependes de convenciones, carpetas, copias y cuidado humano. Con un paquete NuGet interno, puedes declarar explícitamente qué paquete necesitas y qué versión quieres usar.
Eso aporta orden.
El problema de fondo: librerías compartidas y referencias manuales
Cuando varios proyectos dependen de librerías comunes, la distribución manual empieza a generar fricción.
Algunos ejemplos bastante reconocibles:
- una librería cambia y no todos los proyectos se actualizan igual;
- no queda claro qué versión tiene cada consumidor;
- aparecen copias de binarios en distintas ubicaciones;
- cuesta saber si lo que se está compilando es exactamente lo que se publicó;
- actualizar una dependencia requiere más pasos manuales de los deseables.
No es solo una cuestión de comodidad. También afecta al mantenimiento y a la trazabilidad.
En integración de sistemas esto es especialmente importante, porque suelen convivir proyectos con tecnologías, frameworks y ritmos distintos. Puedes tener librerías modernas en .NET Standard, proyectos más clásicos en .NET Framework, e incluso algún proyecto heredado que necesita un tratamiento diferente en compilación.
Por eso no basta con decir “metemos un pipeline y ya está”. Hay que diseñar un flujo que sea útil para distintos tipos de proyectos sin convertir el YAML en una colección de excepciones difíciles de mantener.
El enfoque: compilar, empaquetar y publicar
El flujo que he trabajado se puede resumir en tres pasos principales:
- Compilar la librería.
- Generar un paquete NuGet cuando corresponda.
- Publicar ese paquete en un feed interno de Azure Artifacts.
La parte interesante está en separar bien dos escenarios:
- validar que el proyecto compila;
- publicar una nueva versión del paquete.
No siempre que se ejecuta un pipeline tiene sentido publicar un NuGet. A veces solo quieres comprobar que el código compila correctamente. Otras veces sí quieres generar una versión nueva para que la consuman otros proyectos.
Esa separación me parece clave. Si el pipeline publica siempre, acabas generando ruido. Si no publica nunca, vuelves a depender de pasos manuales. El punto medio es que el pipeline sepa cuándo debe limitarse a validar y cuándo debe empaquetar y publicar.
Plantillas reutilizables: menos YAML repetido
Otro punto importante ha sido apoyarse en plantillas comunes de pipeline.
Cuando tienes varios proyectos parecidos, copiar y pegar YAML funciona al principio, pero envejece mal. Cada pequeño cambio hay que replicarlo en varios sitios y, con el tiempo, aparecen diferencias que nadie recuerda si son intencionadas o accidentales.
La alternativa es tener una plantilla base que reciba parámetros:
- nombre del proyecto;
- ruta del archivo
.csprojo.vbproj; - tipo de compilación;
- versión del paquete;
- si debe generar NuGet o no;
- framework de salida, cuando aplique.
Con eso, cada proyecto define lo suyo, pero la lógica común vive en un único sitio.
No elimina toda la complejidad, pero ayuda mucho a mantener el criterio. Y en pipelines, tener criterio suele ser más importante que tener muchas opciones.
No todos los proyectos .NET se comportan igual
Una de las cosas que se ven rápido al montar estos flujos es que no todos los proyectos .NET son iguales.
En proyectos modernos, muchas operaciones encajan bastante bien con comandos como:
- task: DotNetCoreCLI@2
displayName: 'Pack NuGet package'
inputs:
command: 'pack'
packagesToPack: '**/*.csproj'
versioningScheme: 'byEnvVar'
versionEnvVar: 'PACKAGE_VERSION'
outputDir: '$(Build.ArtifactStagingDirectory)'
Este ejemplo es genérico y simplificado, no una copia de ningún pipeline interno.
Para proyectos SDK-style, dotnet restore, dotnet build y dotnet pack suelen encajar bastante bien. Pero en proyectos .NET Framework clásicos, o en proyectos VB.NET heredados, puede ser más adecuado usar restauración con NuGet y compilación con Visual Studio Build o MSBuild.
Este punto me parece importante porque a veces se habla de CI/CD como si todos los proyectos siguieran el mismo patrón. En la práctica, hay que convivir con lo nuevo y con lo existente.
La clave está en modernizar sin romper lo que todavía necesita un camino distinto.
Publicar en Azure Artifacts
Una vez generado el paquete .nupkg, el siguiente paso es publicarlo en un feed interno de Azure Artifacts.
De nuevo, un ejemplo conceptual y anonimizado sería:
- task: NuGetCommand@2
displayName: 'Push NuGet package'
inputs:
command: 'push'
packagesToPush: '$(Build.ArtifactStagingDirectory)/*.nupkg'
publishVstsFeed: '<FEED_INTERNO>'
allowPackageConflicts: true
Lo importante no es la tarea exacta, sino el concepto: el paquete no se queda como un fichero suelto, sino que se publica en un repositorio interno desde el que otros proyectos pueden consumirlo.
A partir de ahí, los consumidores dejan de depender de una ruta o de una copia manual. Pasan a depender de un paquete y de una versión.
Eso hace que el flujo sea más limpio y más fácil de razonar.
Versionado: publicar algo que se pueda identificar
Publicar paquetes sin una estrategia mínima de versionado no sirve de mucho.
El pipeline debe saber qué versión está generando. Puede recibirla como parámetro, calcularla a partir del número de build o seguir el criterio que cada equipo decida. Lo importante es que el paquete publicado tenga una versión identificable.
Esto permite que un proyecto consuma, por ejemplo, una versión concreta de una librería y no “lo último que alguien dejó en una carpeta”.
Esa diferencia es pequeña en apariencia, pero muy grande cuando hay que mantener varios proyectos a la vez.
Controlar la concurrencia
Otro detalle que aparece cuando automatizas la publicación es la concurrencia.
Si varios pipelines intentan publicar paquetes al mismo tiempo, puede haber situaciones incómodas: bloqueos, conflictos, publicaciones pendientes o comportamientos difíciles de seguir.
Una forma de tratarlo en Azure Pipelines es usar environments con bloqueo exclusivo y comportamiento secuencial. La idea es que la fase de publicación pase por un recurso común que solo permita una publicación cada vez.
Conceptualmente:
lockBehavior: sequential
jobs:
- deployment: PublishNuGet
environment: '<ENVIRONMENT_PUBLICACION>'
strategy:
runOnce:
deploy:
steps:
- script: echo "Publicar paquete NuGet"
No siempre hará falta complicarse con esto desde el primer día, pero cuando empiezas a tener varios paquetes y varios pipelines, controlar la publicación se vuelve importante.
Permisos: la parte poco vistosa, pero necesaria
Los permisos en Azure DevOps también forman parte del trabajo.
Un pipeline no debería tener más permisos de los que necesita, pero sí los suficientes para hacer su trabajo. Si tiene que publicar en un feed, el servicio de build necesita permiso para publicar en ese feed. Si además tiene que leer otros repositorios o actualizar documentación, necesitará permisos específicos para esas acciones.
Aquí conviene aplicar el principio de mínimo privilegio:
- dar acceso solo a lo necesario;
- revisar qué identidad ejecuta el pipeline;
- no resolver errores de permisos dando permisos amplios “por si acaso”;
- separar bien lectura, contribución y publicación.
Es una parte menos atractiva que escribir YAML, pero marca la diferencia entre un flujo útil y un flujo frágil.
Limpiar lógica legacy
En el proceso también apareció algo habitual: lógica antigua en los pipelines.
Por ejemplo, caminos de empaquetado basados en .nuspec y nuget.exe pack, junto a caminos más modernos basados en dotnet pack.
Mantener ambos caminos puede tener sentido durante una transición. Pero si una rama legacy ya no se usa, acaba siendo ruido. Hace el pipeline más difícil de leer, más difícil de probar y más fácil de romper.
Una de las conclusiones que me llevo es que automatizar no consiste solo en añadir pasos. También consiste en quitar lo que ya no aporta.
Un pipeline sencillo, claro y homogéneo suele ser mejor que uno lleno de opciones que nadie usa.
Qué beneficios aporta este enfoque
Pasar de referencias manuales o binarios compartidos a paquetes NuGet internos tiene varios beneficios prácticos:
- las librerías compartidas se distribuyen de forma más controlada;
- cada consumidor puede depender de una versión concreta;
- se reduce la copia manual de DLLs;
- la compilación queda validada por el pipeline;
- la publicación es más repetible;
- el equipo gana trazabilidad;
- el YAML común se puede mantener en plantillas;
- los errores manuales disminuyen;
- es más fácil evolucionar las librerías compartidas con cierto orden.
No es una solución mágica, ni hace que todo sea automático de un día para otro. Pero sí cambia la base de trabajo.
En lugar de depender de memoria, carpetas y pasos manuales, empiezas a apoyarte en versiones, paquetes y procesos reproducibles.
Conclusión
Azure Pipelines y Azure Artifacts encajan muy bien cuando tienes librerías .NET compartidas y quieres distribuirlas de forma más profesional dentro de una organización.
Para mí, el valor principal no está solo en “generar un NuGet”. Está en ordenar el flujo completo: compilar, decidir cuándo publicar, versionar, controlar permisos, evitar conflictos y reducir lógica antigua.
En integración de sistemas, donde conviven tecnologías y proyectos de distintas épocas, ese orden se agradece mucho.
Al final, un buen pipeline no debería ser una caja negra ni una colección de parches. Debería ser una forma clara de convertir código en algo consumible, versionado y mantenible.
Y cuando hablamos de librerías compartidas, eso marca una diferencia bastante real en el día a día.