Je vais commencer une série de billets sur des problématiques courantes liées à TFS 2008. Bien que la mode soit actuellement plutôt autour de TFS 2010 et de VS 2010, la version 2008 est encore présumablement déployée chez bon nombre et certains n’ont pas les moyens de dégager le temps ou les ressources pour migrer vers TFS 2010.
Le but sera de donner des recettes qui répondent à des besoins du terrain plutôt classiques pour les projets sous TFS 2008 en essayant de proposer des solutions les plus simples. L’industrialisation de vos builds vous permettra ainsi de gagner un temps précieux en automatisant un maximum de tâches sur l’ensemble de vos projets.
Build versionnant : recette rapide
Je ne suis clairement pas le 1er à poster sur l’incrémentation des numéros de version dans les builds, on bénéficie aujourd’hui de nombreux posts sur le sujet et grâce à eux nous avons aujourd’hui un certain recul, de même nous bénéficions de frameworks MSBuild en Open Source tout à fait opérationels (certains plus ou moins en activité). Faire une passe plus en profondeur à ce jour me semble intéressant.
Malgré la longueur de ce post, le travail est pré-mâché pour être applicable rapidement.
Voici ce que nous allons mettre en place :
- Un build qui pose un label de type “1.2.3.4” ou “MonProjet_1.2.3.4”
- Le nom des builds contiendra le numéro de version et non plus la date (ex: “MonBuild_1.2.3.4”)
- Le numéro de version est incrémenté à chaque build (build ou revision)
- Le build va modifier les fichiers sources AssemblyInfo.cs (.vb) pour y mettre le numéro de version. Cependant, cela ne se produit que sur le serveur de build, les sources en question en sont pas mis à jour dans le contrôleur de source
- Le numéro de version est maintenu dans un fichier Xml dans le contrôleur de source, on peut le modifier à tout moment et cela sera répercuté sur les binaires au prochain build
Une version est avant tout technique
Je dois m’expliquer sur certains choix. Pourquoi systématiquement incrémenter le numéro de version ? Et si je veux produire un version qui tombe rond : 1.1.0.0 ?
A mon sens il faut arrêter de vouloir décider à l’avance des versions que l’on va livrer. Cela se traduit généralement par une perversion du système de build / packaging. En effet, on n’est jamais à l’abri d’un build qui échoue, ou d’un build de faible qualité qui ne sera pas viable pour une livraison, même intermédiaire. N’est-ce pas détourné que de prétendre qu’un build passera tous les tests jusqu’à livraison ? Si vous ne vous trompez jamais, je vous embauche et je vous paye cher ;)
Je pense qu’il faut accepter que la version d’un produit buildé est une version technique, et qu’il est préférable de communiquer une version “simplifiée” au client, un nom de code par exemple, suivi du numéro de patch. Bref, faire correspondre une version technique avec une version marketing (que le cient soit interne ou externe ne change rien).
De même, ne vous inquiétez pas s’il y a des trous entre les versions, si entre deux livraisons on passe de la 1.2.56.0 à la 1.2.185.0. Certains builds échoueront, d’autres seront buggés. C’est pour cela que le plus simple est sans doute d’avoir un système qui permette à tout build d’être potentiellement livrable. J’entends par là :
- un label en bonne et dûe forme incluant le numéro de version
- un numéro de version incorporé dans les binaires
- un nom de build facilement repérable (encore une fois le numéro de version est pratique)
- un package livrable en sortie
Ce type de build est typiquement exécutable en tant que Nightly build (je le recommande) et à la demande lorsqu’une livraison est prévue (nb : on peut facilement forcer un build déjà programmé avec TFS).
Tâches MSBuild utilisées
Avec MSBuild est facile de créer ses propres tâches (Tasks) : en deux coups de cuillère à pot on référence quelques assemblies Microsoft.Build.*, on hérite de la classe Task et c’est parti. J’ai fait le choix de ne pas créer de nouvelle tâche (bien que cela eût été plus facile parfois que de comprendre les existantes !) afin de vous épargner de les builder vous-même ou d’avoir des configs de serveur de build un peu torp exotiques avec trop nombreuses tierces parties. En effet, si vous utilisez ainsi des tâches compilées maison, cela vous demande également de créer les projets équivalent dans le contrôleur de sources (du moins j’espère que vous auriez ce réflexe !).
Je me suis appuyé sur les 3 frameworks MSBuild les plus connus à ce jour :
La 1ère étape consiste donc à les télécharger et à les déployer sur le serveur de build. Pour cela, il faut soit installer les MSI soit dézipper les binaires dans un sous-répertoire de %programfiles%\MSBuild (pour SDCTasks), vous devez obtenir respectivement ExtensionPack, MSBuildCommunityTasks et SDCTasks.
Implémentation
Dans le fichier .proj de votre build, commencez par repérer la 1ère balise Import et collez ceci en dessous :
<Import Project="$(MSBuildExtensionsPath)\ExtensionPack\MSBuild.ExtensionPack.tasks" />
<Import Project="$(MSBuildExtensionsPath)\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets" />
<UsingTask AssemblyFile="$(MSBuildExtensionsPath)\SDCTasks\Microsoft.Sdc.Tasks.dll" TaskName="Microsoft.Sdc.Tasks.VersionNumber.Parse" />
Ces tâches permettent de déclarer les tâches que nous allons utiliser dans les 3 frameworks préalablement cités.
Puis insérez les Targets suivantes :
<Target Name="GetProjectVersionNumber">
<!-- récupération du fichier Xml -->
<Message Text="Getting version number from Xml file %(ProjectVersionNumberFile.FullPath)" />
<ExtensionPack.VisualStudio.TfsSource TaskAction="Get" ItemCol="@(ProjectVersionNumberFile)" Force="true" />
<Error Condition="!Exists('%(ProjectVersionNumberFile.FullPath)')" Text="Missing project info XML with version number [%(ProjectVersionNumberFile.FullPath)]."/>
<!-- lecture du numéro de version -->
<Community.Tasks.XmlRead XPath="/Project/CurrentVersion" XmlFileName="%(ProjectVersionNumberFile.FullPath)">
<Output TaskParameter="Value" PropertyName="ProjectVersionNumber" />
</Community.Tasks.XmlRead>
<Message Text="Project CurrentVersion is $(ProjectVersionNumber)"/>
<Error Condition="$(ProjectVersionNumber)==''" Text="Version information was not found the Xml version file."/>
<!-- Analyse du numéro de version -->
<Sdc.Tasks.VersionNumber.Parse Text="$(ProjectVersionNumber)">
<Output TaskParameter="MajorNumber" PropertyName="ProjectMajorNumber" />
<Output TaskParameter="MinorNumber" PropertyName="ProjectMinorNumber" />
<Output TaskParameter="BuildNumber" PropertyName="ProjectBuildNumber" />
<Output TaskParameter="RevisionNumber" PropertyName="ProjectRevisionNumber" />
</Sdc.Tasks.VersionNumber.Parse>
<!-- Incrément du numéro de version selon les valeurs fixées dans les propriétés BuildNumberIncrement et RevisionNumberIncrement -->
<ExtensionPack.Science.Maths TaskAction="Add" Numbers="$(ProjectBuildNumber);$(BuildNumberIncrement)">
<Output TaskParameter="Result" PropertyName="ProjectBuildNumber" />
</ExtensionPack.Science.Maths>
<ExtensionPack.Science.Maths TaskAction="Add" Numbers="$(ProjectRevisionNumber);$(RevisionNumberIncrement)">
<Output TaskParameter="Result" PropertyName="ProjectRevisionNumber" />
</ExtensionPack.Science.Maths>
<PropertyGroup>
<ProjectVersionNumber>$(ProjectMajorNumber).$(ProjectMinorNumber).$(ProjectBuildNumber).$(ProjectRevisionNumber)</ProjectVersionNumber>
</PropertyGroup>
<Message Text="New version is $(ProjectVersionNumber)"/>
<ExtensionPack.VisualStudio.TfsSource TaskAction="Checkout" ItemCol="@(ProjectVersionNumberFile)" />
<!-- MAJ du numéro dans le fichier Xml -->
<Community.Tasks.XmlUpdate XPath="/Project/CurrentVersion" XmlFileName="%(ProjectVersionNumberFile.FullPath)" Value="$(ProjectVersionNumber)" />
<!-- Archivage du fichier dans le contrôleur de sources -->
<TfsSource TaskAction="Checkin" ItemCol="@(ProjectVersionNumberFile)" Comments="Update for build version $(ProjectVersionNumber) - $(NoCICheckinComment)" OverrideText="Needed for build automation" />
</Target>
<!-- Cette tâche permet de modifier le nom du build et le label qui sera posé -->
<!-- Elle déclenche la tâche GetProjectVersionNumber ci-dessus -->
<Target Name="BuildNumberOverrideTarget" DependsOnTargets="CoreInitializeWorkspace;GetProjectVersionNumber">
<PropertyGroup>
<!-- Le nom du build, ne pas hésiter à customiser ce nom -->
<BuildNumber>$(TeamProject)_$(ProjectVersionNumber)</BuildNumber>
<!-- Le nom du label, customisable également -->
<LabelName>$(ProjectVersionNumber)</LabelName>
<!-- Le répertoire où sera posé le label dans le contrôleur de sources -->
<LabelScope>$/$(TeamProject)/Main</LabelScope>
</PropertyGroup>
</Target>
<Target Name="BeforeCompile">
<!-- Afin de modifier le ou les fichiers qui contiennent des numéros de version, on enlève l'attribut lecture-seule -->
<Community.Tasks.Attrib Files="@(VersionInfoSourceFile)" readOnly="false" />
<!-- Puis on écrase toute version trouvée avec la version courante (devrait marcher avec C# et VB) -->
<Community.Tasks.FileUpdate Files="@(VersionInfoSourceFile)" Regex="(\d+)\.(\d+)\.(\d+)\.(\d+)" ReplacementText="$(ProjectVersionNumber)" />
</Target>
A la fin du fichier, avant la balise </Project> insérez le bloc suivant :
<PropertyGroup>
<!-- L'incrément utilisé pour le 3ème numéro dans la version (build) -->
<BuildNumberIncrement>1</BuildNumberIncrement>
<!-- L'incrément utilisé pour le 4ème numéro dans la version (revision) -->
<RevisionNumberIncrement>0</RevisionNumberIncrement>
</PropertyGroup>
<ItemGroup>
<!-- Chemin vers le fichier Xml qui contient le numéro de version -->
<ProjectVersionNumberFile Include="$(SolutionRoot)\TeamBuildTypes\AutoIncrProject\ProjectInfo.xml" />
<!-- Chemin vers le fichier source qui contient l'attribut de version -->
<VersionInfoSourceFile Include="$(SolutionRoot)\Main\WindowsFormsApplication1\Properties\AssemblyInfo.cs" />
</ItemGroup>
Fichier Xml
Afin de garder trace du numéro de version, j’utilise un fichier Xml tout simple qui sera maintenu par le build dans le contrôleur de source. J’ai préféré utiliser un fichier indépendant plutôt qu’un fichier source afin de le placer dans un répertoire indépendant de celui des sources. Ce n’est pas très important, mais je préfère éviter de polluer ainsi les sources directement avec des archivages déclenchés par les builds (cf Résultat).
Créez donc un fichier ProjectInfo.xml dans votre Team Project et collez-y le contenu suivant :
<?xml version="1.0" encoding="utf-8"?>
<Project>
<CurrentVersion>1.0.0.0</CurrentVersion>
</Project>
Configuration
Voilà c’est presque prêt il ne reste plus qu’à customiser une ou deux choses dans notre script de build :
- BuildNumberIncrement et RevisionNumberIncrement : généralement l’un est à 1 et l’autre à 0. Lorsque je crée une branche pour la maintenance, je préfère incrémenter le numéro de révision et laisser le numéro de build figé (il correspond du coup au numéro qui a été livré depuis la branche principale).
- ProjectVersionNumberFile : Je m’arrange pour que ce fichier soit généralement dans le scope de mes branches, mais pas dans celui des sources.
- VersionInfoSourceFile : Il est possible de mettre plusieurs fichiers en dupliquant la balise. J'ai une légère préférence ceci dit pour ne référencer qu’un seul fichier VersionInfo.cs (ou .vb) commun à plusieurs projets en utilisant des linked items. Moins y’en a dans la config du build et mieux on se porte.
Résultat
Vos builds auront désormais des noms plus clairs…
…sauf s’ils plantent avant la fin de la tâche BuildNumberOverrideTarget !
L’historique de vos sources sera “pollué” par les builds. Autant on peut parfois considérer cela comme du bruit, autant cela peut s’avérer bien pratique d’avoir cette vision entrelacée des changesets au fil des versions :
Si vous avez placé votre fichier Xml en dehors du scope des sources, vous pourrez justement éviter cette “pollution” en demandant l’historique sur les sources seulement !
Notez le commentaire ***NO_CI***, il correspond à la propriété pré-définie $(NoCICheckinComment) et permet d’éviter le déclenchement d’un build d’intégration continue. Sans cela, on aurait une belle boucle infinie de builds !
Les labels créés au cours des build :
Attention à vos labels
Vos builds vont être effacés en fonction de la politique de rétention que vous aurez choisie (cf définition du build). Afin d’éviter de perdre un build qui a finalement été livré, pensez à le “retenir indéfiniement” en effectuant un clic-droit dessus :
En cas de perte d’un build livré, ce n’est pas trop grave : vous retrouverez très vite le changeset qui vous intéresse grâce à… l’historique du fichier ProjectInfo.xml ! Vous pourrez recréer un label sur la base de ce changeset.
Notes
Vous noterez la dépendance de la Target BuildNumberOverrideTarget envers CoreInitializeWorkspace, sans cette astuce (merci à ce post) le workspace ne serait pas toujours créé avant de récupérer le fichier VersionInfo.Xml et ça ne pourrait pas marcher, à moins d’avoir recours à des “tf.exe view” mais ça complique la sauce.
A vous…
J’espère que cette recette vous sera utile autant qu’à moi, si vous venez à l’adaptez n’hésitez pas à me dire en quoi, ça fait toujours plaisir que quelque chose sert :)
Vous aurez probablement une erreur à la 1ère exécution à cause d’un chemin de fichier non trouvé, un répertoire en trop, en moins ? Si, si, moi aussi j’ai bloqué un peu bêtement sur mon propre projet d’exemple ;)