Alinear el número de Build con la versión de los Assemblies

(Lo que está en cursiva está escrito por mi, lo normal es de la cosecha de Martin)

Otra de las dudas que surgieron durante el evento, es la posibilidad de alinear el número de Team Build con el número de nuestra versión de assembly.

Acerca de esto, mi compañero MVP de Team System de Irlanda, Martin Woodward, ya escribió un artículo, que tenéis aquí y que voy a traducir en este post.

Me gusta que el número de mis builds sea el mismo número de versión de mis assemblies (y por tanto de los entregables). Esto hace las cosas mucho más fáciles de seguir, de este modo si recibo un informe de un bug de un cliente, puedo mirar el número de versión y buscar fácilmente por la etiqueta en source contro para ver el código. En todos los entregables distribuidos al cliente, siempre mostramos el número de versión obtenido del assebmly actual al principio de la información de diagnóstico, de este modo se puede comprobar la versión que tienen y empezar a revisarlo. Esto ayuda a archivar los bugs con su versión correcta de código y reportar en que versión se han arreglado (usando la integración entre las Team Builds y los Work Items en TFS).

La gente se sorprende que esta característica no está incluida "out-of-the-box" en Team Build, así que he pensado que podría tomarme un momento para escribir un documento explicativo de como he realizado este trabajo internamente. Como podréis ver, en TFS 2008 tenemos todos los puntos de enlace básicos para realizar esto.

Para comenzar, explicar que nuestro número de versión de .NET se diferencia ligeramente del esquema de nuestro número de versión de Java. En nuestros productos Java, la parte de "número de build" de la versión, es el número de changeset de TFS en ese momento. En .NET hay 4 componentes del número de versión (1.0.1234.5678) y el valor máximo para cada uno de ellos es 65535. Nuestro servidor de producción de TFS está actualmente en el changeset 7698 lo que significa que "disponemos" de 6 años  de este esquema de numeración – esto serçia perfectamente satisfactorio si tuviesemos una eópca de changeset después de cada "major release" (de este modo resetarías el número de build para que sea el changeset actual). Pero como Team build necesita un nombre único para cada build – usar el changeset podría tener mayor riesgo de que se tuviesen dos builds con el mismo número. así que mejor que usar el changeset, he preferido hacer que el número de build de .NET sea un número incremental. Y uso la funcionalidad por defecto de Team Build para crear una etiqueta para ese número de build para hacer el seguimiento de ese número en el control de versiones. El número incremental se almacena en un fichero en el directorio por defecto del directorio donde se guardan los binarios resultantes de la buid (el drop location).

Otro punto que quiero explicar es que, personalmente, no me gusta el standard de Microsoft a la hora de versionar asembblies:

<Major>.<Minor>.<Build>.<Service>

Para mí, es más fácil de leer:

<Major>.<Minor>.<Service>.<Build>

Dónde <Build> es el número que incremento cada vez que se realiza una build. Hasta dónde yo conozco, la diferencia es más bien estética, ya que eso no cambia el modo en que el CLR resuelve las versiones del assembly, de todos modos sois libres de corregirme en los comentarios si esto no es así.

Así que, vamos a ver como conseguir esto. Lo primero, TFS 2008 dispone de un "target" que podemos sobreescribir para generar nuestros propios números de build llamado "BuildNumberOverrideTarget". Lo más importante es que cada número de build debe ser único, por tanto, una buena regla es usar algo como BuildDefinitionName_1.0.0.1234. Dentro de BuildNumberOverrideTarget simplemente asignaremos el valor a la propiedad "BuildNumber", para poner el número que queramos, aquí está el nuestro:

<PropertyGroup>
  <VersionMajor>1</VersionMajor>
  <VersionMinor>0</VersionMinor>
  <VersionService>0</VersionService>
  <VersionBuild>0</VersionBuild>
</PropertyGroup>
<Target Name="BuildNumberOverrideTarget">
  <!– Create a custom build number, matching the assembly version –>     
  <Message Text="Loading last build number from file &quot;$(DropLocation)\buildnumber.txt&quot;" />
  <IncrementingNumber NumberFile="$(DropLocation)\buildnumber.txt">
    <Output TaskParameter="NextNumber" PropertyName="VersionBuild" />
  </IncrementingNumber>
  <PropertyGroup>
    <BuildNumber>$(BuildDefinitionName)_$(VersionMajor).$(VersionMinor).$(VersionService).$(VersionBuild)</BuildNumber>
  </PropertyGroup>
  <Message Text="Build number set to &quot;$(BuildNumber)&quot;" /> 
</Target>

Lo primero que hago, es una llamada a una tarea propia que he escrito y que incrementa el número de build almacenado en el fichero que le pasamos. Quería hacer esto mientras mantenemos el fichero bloqueado para el caso de que dos builds intenten actualizar el fichero en el mismo momento. A continuación cogemos este número y montamos el BuildNumber basándonos en este valor. El código de la tarea para incrementar este número lo tenéis aquí:

using System;
using System.IO;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;

namespace Teamprise.Tasks
{
    /// <summary>
    ///   A simple task to increment the number stored in a passed file.
    /// </summary>
    public class IncrementingNumber : Task
    {
        public override bool Execute()
        {
            NextNumber = IncrementNumber();
            return true;
        }
        public int IncrementNumber()
        {
            using (FileStream fs = new FileStream(NumberFile, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None))
            {
                StreamReader reader = new StreamReader(fs);

                long pos = 0;
                String line = reader.ReadLine();

                // Ignore comments in file
                while (line != null && line.StartsWith("#"))
                {
                    pos = pos + line.Length + System.Environment.NewLine.Length;
                    line = reader.ReadLine();
                }

                int number = -1;
                if (line != null)
                {
                    number = Int32.Parse(line);
                }
                NextNumber = number + 1;

                // Rewind the file stream back to the beginning of the number part.
                fs.Position = pos;

                StreamWriter writer = new StreamWriter(fs);
                writer.WriteLine(NextNumber.ToString());
                writer.Flush();
                writer.Close();
            }
            return NextNumber;
        }
        [Required]
        public string NumberFile { get; set; }
        [Output]
        public int NextNumber { get; set; }
    }
}

Podéis compilar este código en un assemby que podéis situar en el mismo directorio en source control que el fichero TFSBuild.proj de nuestra build, y que es cargado al principio del proceso mediante esta instrucción dentro del fichero .proj:

<UsingTask TaskName="Teamprise.Tasks.IncrementingNumber"
           AssemblyFile="Teamprise.Tasks.dll" />

Lo siquiente que tenemos que hacer es obtener el nuevo número de versión y forzarlo dentro del fichero AssemblyInfo. Personalmente, yo prefiero que el fichero AssemblyInfo esté almacenado en source control para tener un número bien definido para cada release (por ejemplo 1.0.0.0), y hacer que el servidor de build lo versione. Algunas personas prefieren volver a hacer checkin de esto en source control, si lo hacéis, tened precaución de poner en el comentario el texto "***NO_CI***" para asegurarnos de que el check-in no lanze una nueva compilación de CI, que nos podría colocar en un bucle infinito de builds.

Así que para modificar nuestro número de versión después de haberlo descargado de source control, usando una técnica prestada de  Richard Banks, nuestra interpretación es la siguiente:

<ItemGroup> 
  <AssemblyInfoFiles Include="$(SolutionRoot)\**\assemblyinfo.cs" /> 
</ItemGroup>   
<Target Name="AfterGet"> 
  <!-- Actualizamos los ficheros con el número de versión generado --> 
  <Message Text="Modifying AssemblyInfo files under &quot;$(SolutionRoot)&quot;." /> 
  <Attrib Files="@(AssemblyInfoFiles)" Normal="true" /> 
  <FileUpdate Files="@(AssemblyInfoFiles)"                                 
              Regex="AssemblyVersion\(&quot;.*&quot;\)\]"                 
              ReplacementText="AssemblyVersion(&quot;$(VersionMajor).$(VersionMinor).$(VersionService).$(VersionBuild)&quot;)]" /> 
  <FileUpdate Files="@(AssemblyInfoFiles)" 
              Regex="AssemblyFileVersion\(&quot;.*&quot;\)\]" 
              ReplacementText="AssemblyFileVersion(&quot;$(VersionMajor).$(VersionMinor).$(VersionService).$(VersionBuild)&quot;)]" /> 
  <Message Text="AssemblyInfo files updated to version &quot;$(VersionMajor).$(VersionMinor).$(VersionService).$(VersionBuild)&quot;" /> 
</Target>

Como podéis ver, estamos usando una tarea personalizada que se llama "Attrib" y que es parte de las tareas básicas del  MSBuild Community Tasks para poner los ficheros como lectura/escritura y después usamos la tarea (del mismo paquete) de FileUpdate para acualizar las partes necesarias mediante expresiones regulares.

Y esto es todo lo que se necesita hacer, ahora nuestras builds tienen un número incremental que tiene el número de versión incluido igual que en la información del fichero del assembly.

Bueno, como podéis ver esto no es excesivamente complicado, si bien se requieren conocimientos de como trabajar con la extensibilidad de las builds (Martin es un auténtico crack en esto), para familiarizaros con esto podeís descargaros el SDK de Visual Studio 2008 y ver la documentación de extensibilidad de las Team Builds.

Espero que os haya servido de ayuda a todos, y thanks Martin 😉

One thought on “Alinear el número de Build con la versión de los Assemblies

Leave a Reply

Your email address will not be published. Required fields are marked *