Actualizar automáticamente la versión del ejecutable con el nº de build de Jenkins

Lo que voy a explicar en este post es como automatizar el cambio de número de versión de la aplicación en todos los ensamblados y que queden actualizados con el número de build de Jenkins. Supongo que puede valer para cualquier otro servidor de integración continua.

Herramientas necesarias

Nuget

Para facilitar las tareas, iremos descargando difentes paquetes desde Nuget. Si no lo tienes instalado puedes hacerlo desde el extension manager de Visual Studio.

MSBuildTasks

He visto varias maneras de hacer el versionado automático. No me ha convencido ninguna y, por eso, estoy publicando este post. Para modificar el assemblyInfo con el número de build utilizaremos MSBuildTasks

Concepto de las tareas a realizar

Antes de compilar, es necesario tomar el número de build de Jenkins y actualizar con él la información del assemblyInfo. Como no queremos repetir esta tarea para cada ensamblado, crearemos un único ensamblado con la información común a todos los ensamblados. Además, cada ensamblado puede tener su propio assemblyInfo con la información específica.

Crear el CommonAssemblyInfo.cs

Este fichero lo agregaremos en una carpeta de la solución. Si no la tenemos creada, hacemos clic con el botón derecho en la solución y seleccionamos Add –> New Solution Folder
image
Dentro de esta carpeta, agregamos un nuevo archivo. Yo he agregado una clase llamada CommonAssemblyInfo.cs, he borrado el contenido que agrega VS y en su lugar he puesto algo así:
CommonAssemblyInfo.cs
  1. using System;
  2. using System.Reflection;
  3. using System.Resources;
  4. using System.Runtime.CompilerServices;
  5. using System.Runtime.InteropServices;
  6.  
  7. [assembly: AssemblyCompany("Compaa")]
  8. [assembly: AssemblyProduct("Mi Aplicacin")]
  9. [assembly: AssemblyCopyright("Copyright  Compaa 2012")]
  10. [assembly: AssemblyConfiguration("Debug")]
  11. [assembly: NeutralResourcesLanguage("en")]
  12. [assembly: ComVisible(false)]
  13.  
  14. // Version information
  15. [assembly: AssemblyVersion("1.0.2.0")]
  16. [assembly: AssemblyFileVersion("1.0.2.0")]
  17. [assembly: AssemblyInformationalVersion("1.0.2.0")]
Bien, ahora hay que ir a cada uno de los ensamblados y editar, uno por uno, el AssemblyInfo.cs En cada uno hay que dejar solo la parte que es específica del ensamblado. Por ejemplo:

Code Snippet
  1. using System.Reflection;
  2.  
  3. [assembly: AssemblyTitle("Mi Ensamblado")]
  4. [assembly: AssemblyDescription("Descripcion de mi ensamblado")]

También, por cada ensamblado, hay que agregar una referencia al CommonAssemblyInfo.cs. Para eso, hacemos clic con el botón derecho en el proyecto y le damos a agregar elementos existente. Al seleccionar el archivo, debemos tener cuidado de seleccionar agregar como link.
image

Configurar MSBuildTasks

El objetivo es modificar el CommonAssemblyInfo.cs antes de compilar. Pero solo lo queremos modificar una vez por compilación ¿no? Así que buscamos, dentro de la solución, cual es el ensamblado que se compilará primero. ¿Como lo buscamos? Fácil: botón derecho sobre la solución –> Project dependencies. Seleccionamos la pestaña Build Order y el primer proyecto es el que nos interesa.
imageimage
Nos vamos a ese proyecto y le agregamos MSBuildTasks desde Nuget: Botón derecho en el proyecto –> Manage Nuget packages. Buscamos MSBuildTasks y le damos a instalar.
Ahora toca editar el .csproj a mano. Descargamos el proyecto desde VS y luego le damos a editar (A mi me resulta más cómodo abrirlo desde el explorador de windows con el Notepad++). Vemos que el MSBuilTasks nos ha agregado las siguientes líneas a nuestro archivo de proyecto.

Code Snippet
  1. <MSBuildCommunityTasksPath>$(SolutionDir)\Build</MSBuildCommunityTasksPath>

y

Code Snippet
  1. <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
  2. <Import Project="$(SolutionDir)\.nuget\nuget.targets" />
  3. <Import Project="$(SolutionDir)\Build\MSBuild.Community.Tasks.targets" />

  • La línea 1 se corresponde con un import que tendremos en todos los proyectos
  • La línea 2 se corresponde con el nuget, donde se llevan las referencias de los paquetes instalados. Esta línea está presente si habilitamos la solución para que descargue automáticamente los paquetes faltantes. Lo recomiendo enormemente (salvo que tengamos mala conexión a Internet), sobre todo si no nos apetece subir los binarios al repositorio de control de versiones.
  • La línea 3 es la que nos interesa y que nos da acceso a las tareas que vamos a necesitar a continuación.
Justo debajo, veremos 2 secciones Target BeforeBuild y Target AfterBuild que están comentadas. Tenemos que descomentar la BeforeBuild. Nos debería quedar algo así:
Code Snippet
  1. <Target Name="BeforeBuild">
  2.   <PropertyGroup>
  3.     <BUILD_NUMBER Condition=" '$(BUILD_NUMBER)' == '' ">0</BUILD_NUMBER>
  4.   </PropertyGroup>
  5.   <Message Text="Actualizando versin de ensamblados" />
  6.   <FileUpdate Files="$(SolutionDir)\CommonAssemblyInfo.cs" Multiline="true" Singleline="false" Regex="(AssemblyInformationalVersion\(&quot;[0-9]+\.[0-9]+\.[0-9]+)(\.[0-9]+)(&quot;\))" ReplacementText="$1.$(BUILD_NUMBER)$3" />
  7.   <FileUpdate Files="$(SolutionDir)\CommonAssemblyInfo.cs" Multiline="true" Singleline="false" Regex="(AssemblyVersion\(&quot;[0-9]+\.[0-9]+\.[0-9]+)(\.[0-9]+)(&quot;\))" ReplacementText="$1.$(BUILD_NUMBER)$3" />
  8.   <FileUpdate Files="$(SolutionDir)\CommonAssemblyInfo.cs" Multiline="true" Singleline="false" Regex="(AssemblyFileVersion\(&quot;[0-9]+\.[0-9]+\.[0-9]+)(\.[0-9]+)(&quot;\))" ReplacementText="$1.$(BUILD_NUMBER)$3" />
  9.   <FileUpdate Files="$(SolutionDir)\CommonAssemblyInfo.cs" Multiline="true" Singleline="false" IgnoreCase="true" Regex="(AssemblyConfiguration\(&quot;)([a-z]+)(&quot;\))" ReplacementText="$1$(Configuration)$3" />
  10. </Target>
Vamos a ver línea a línea qué estamos haciendo:
Líneas 2 a 4: Definimos una propiedad, BUILD_NUMBER, que, curiosamente, se llama exáctamente igual que la variable que publica Jenkins durante el proceso de compilación. Jenkins publica las variables como variables de entorno en el S.O., por lo que es muy fácil capturar sus valores desde cualquier herramienta.
La estoy definiendo de forma condicional, es decir, defino la propiedad tomando su valor del S.O., pero si BUILD_NUMBER no existe en el S.O. entonces le asigno un valor por defecto. Esto es útil para que el script funcione cuando compilamos en local. Si se quiere se puede poner un valor especial para saber que ha sido una compilación en local y no una compilación automática.
Línea 5: Un simple mensaje para saber por dónde voy cuando estoy examinando la salida de la compilación
Líneas 6 a 9: Modifico el archivo CommonAssemblyInfo.cs en base a una búsqueda con expresiones regulares.
  • En las líneas 6, 7 y 8 busco AssemblyInformationalVersion, AssemblyVersion y AssemblyFileVersion respectivamente, seguido por 4 grupos de números separados por puntos. En la definición de la expresión regular hago uso de los grupos para poder especificar el reemplazo en el atributo ReplacementText. Por ejemplo con ReplacementText="$1.$(BUILD_NUMBER)$3" sustituyo lo encontrado con AssemblyInformationalVersion, seguido por el contenido de la variable BUILD_NUMBER y seguido con la terminación: las comillas y cierre del paréntesis.
  • En la línea 9 busco AssemblyConfiguration y reemplazo la configuración con la configuración de la compiliación: Debug, Release,…

Listo. Ahora, cada vez que Jenkins nos compile la aplicación, ésta tendrá la versión del ensamblado actualizada con el nº de build. Con el nº de build, en Jenkins puedo ver el commit y los cambios que van en esa build.
Solo queda comentar que la nomenclatura de la numeración de la versión se puede adaptar según las necesidades. Nosotros conservamos los 3 primeros dígitos para versión mayor, menor y fix. El cuarto dígito nos da el nº de build. Ahora que tenemos bien versionados los cambios no nos fijamos nunca en el tercer dígito. Los 2 primeros los editamos a mano cuando queremos hacer un cambio mayor.
Espero que os haya servido.

Editado el 8/2/2013

Al parecer las cosas han cambiado un poco (ahora la versión de MSBuildTasks es la 1.4.0.56) desde la versión 1.4.0.45. La verdad es que es bastante mosqueante que entre 2 números de revisión (que no de versión) existan estas incompatibilidades.

La línea
<MSBuildCommunityTasksPath>$(SolutionDir)\.build</MSBuildCommunityTasksPath>
hay que meterla a mano en el .csproj. Esta debe ir dentro del primer propertyGroup del archivo de proyecto.

También hay que agregar la línea
<Import Project="$(SolutionDir)\.build\MSBuild.Community.Tasks.targets" />

Importante fijarse que la carpeta que se creaba al instalar desde Nuget antes era Build y ahora es .build, con punto por delante.

Bueno, con estos cambios vuelve a funcionar
Un saludo

Comentarios

Anónimo ha dicho que…
Muchas gracias por el aporte, aunque tuve que hacerle algunas modificaciones para adaptarlo a mi proyecto me ha servido de mucho.

Saludos.

Entradas populares de este blog

Install NET Core 2.1 SDK on Rasapbian

Pasar parámetros dinámicos a Attributes