Accueil Nos publications Blog Partagez vos indicateurs de qualité avec TFS et PowerShell.

Partagez vos indicateurs de qualité avec TFS et PowerShell.

Dans une démarche de transparence, responsabilisez votre équipe, justifiez et récompensez leurs efforts par l’automatisation du partage quotidien d’indicateurs de qualité factuels et visuels. Interrogez votre serveur de build TFS à l’aide de PowerShell et partagez par mail les indicateurs : code analysis, code coverage, résultats des tests et avertissements de compilation.

  • Connexion à TFS

  • Obtenir les données de builds TFS

  • Partagez les données de build

  • Tous les modules et scripts PowerShell détaillés dans cet article sont compilés dans le tfsKit disponible sur GITHUB

    Connexion à TFS

    Charger les assemblies TFS

    La première étape afin de travailler avec TFS et PowerShell est de charger les assemblies TFS dans votre session PowerShell.

    Sur une machine avec Visual Studio 2015 installé, le chemin par défaut pour les API TFS est :

    tfsPath

    Vous pouvez les charger depuis le chemin par défaut, autrement je conseille de les exporter dans un répertoire pour générer un kit stand alone qui peut être lancé depuis n’importe quel client, même si TFS n’est pas installé, ou l’est mais dans une version différente du serveur (utiliser les assemblies du serveur).

    Vous devez ajouter les types suivants à votre script PowerShell pour être en mesure d’interroger TFS et votre serveur de build :

    tfsAssembliesShort

    Avec les dépendances, la liste des dll impliquées est plus large. Pour exporter les dlls, sélectionnez les membres suivants :

    tfsAssembliesLong

    Les assemblies nécessaires pour interroger TFS 2015 ou 2013 sont disponibles sur GITHUB

    Code

    Les assemblies peuvent être chargées avec la cmdLet Add-Type désignée pour charger des classes .NET dans votre session PowerShell.

    
    # inquire your assemblies path
    $assembliesPath = "F:\GitHub\Powershell\tfsKit\Assemblies"
    
    # inquire TFS assemblies list
    $tfsAssemblies = ("Microsoft.TeamFoundation.Client.dll","Microsoft.TeamFoundation.Build.Client.dll","Microsoft.TeamFoundation.TestManagement.Client.dll","Microsoft.TeamFoundation.WorkItemTracking.Client.dll","Microsoft.VisualStudio.Services.Client.dll","Microsoft.TeamFoundation.WorkItemTracking.Client.DataStoreLoader.dll","Microsoft.TeamFoundation.Build.Common.dll")
    
    # import assemblies
    $tfsAssemblies | % { Add-Type -Path ([IO.Path]::Combine($assembliesPath, $_)) }
    

    Cet exemple simple ne nous permet pas d’avoir le détail de l’erreur si l’import échoue. Pour cela vous devez récupérer depuis l’exception la propriété LoaderExceptions.

    Comme tous les noms d’assemblies commencent par “Microsoft” et finissent par “.dll”, nous pouvons aussi raccourcir l’appel.

    
    try
    {
    $assembliesShortName = @("TeamFoundation.Client","TeamFoundation.Build.Client","TeamFoundation.TestManagement.Client","TeamFoundation.WorkItemTracking.Client","VisualStudio.Services.Client","TeamFoundation.WorkItemTracking.Client.DataStoreLoader","TeamFoundation.Build.Common")
    $assembliesShortName | % { Add-Type -Path ([IO.Path]::Combine($global:AssembliesFolder, "Microsoft.$_.dll")) }
    }
    catch [System.Reflection.ReflectionTypeLoadException]
    {
    Write-Error $($_.Exception | Select-Object LoaderExceptions | Out-String)
    }
    

    Dans Module-TFS.psm1, nous utilisons le wrapper Add-Assemblies du Module-IO.psm1, nous utilisons également ce module pour les fonctions de Login.

    
    # import TFS assemblies
    Add-Assemblies @("TeamFoundation.Client","TeamFoundation.Build.Client","TeamFoundation.TestManagement.Client","TeamFoundation.WorkItemTracking.Client","VisualStudio.Services.Client","TeamFoundation.WorkItemTracking.Client.DataStoreLoader","TeamFoundation.Build.Common")
    

    Se connecter à votre collection de projets TFS

    Pour effectuer des requêtes à travers notre collection de projets TFS, nous devons instancier un objet TfsTeamProjectCollection depuis l’URI de la collection TFS et vos credentials TFS. Pour cette action, nous utilisons la méthode static GetTeamProjectCollection de la classe TfsTeamProjectCollectionFactory du namespace Microsoft.TeamFoundation.Client.

    Ensuite, pour s’assurer d’être authentifié à une collection de projets TFS valide, nous utilisons la méthode EnsureAuthenticated de l’objet TfsTeamProjectCollection ; cette méthode définit la propriété HasAuthenticated (Boolean) de l’objet source, et s’assure que la collection de projets est valide (aucune erreur n’est retournée lors de l’instanciation de l’objet. Si votre URI ou vos credentials sont faux, vous aurez une erreur lors de l’exécution de la méthode EnsureAuthenticated).

    La méthode GetTeamProjectCollection possède plusieurs surcharges, notez que Microsoft marque avec l’attribut obsolete les surcharges utilisant des credentials TFS.

    Name

    Description

    GetTeamProjectCollection(Uri)

    Gets the TfsTeamProjectCollection instance that is associated with the serveur at the specified URI.

    GetTeamProjectCollection(RegisteredProjectCollection)

    Gets the TfsTeamProjectCollection instance that is associated with the specified RegisteredProjectCollection instance.

    GetTeamProjectCollection(Uri, ICredentialsProvider)

    Obsolete. Gets the TfsTeamProjectCollection instance that is associated with the serveur at the specified URI and un fallback credentials provider.

    GetTeamProjectCollection(RegisteredProjectCollection, ICredentialsProvider)

    Obsolete. Gets the TfsTeamProjectCollection instance that is associated with the specified RegisteredProjectCollection instance and fallback credentials provider.

    GetTeamProjectCollection(String, Boolean, Boolean)

    Retrieves the TfsTeamProjectCollection objet that is used for un given serveur.

    Puisque Microsoft marque avec l’attribut obsolete les surcharges utilisant des credentials TFS, les moyens supportés pour gérer les credentials sont :

    tfsCredentials

    • Lancer les commandes PowerShell depuis un compte active directory autorisé.
    • Lancer le script depuis une machine avec TFS installé et votre serveur TFS ajouté à la collection de serveurs.
    • Laisser PowerShell enregistrer un cookie TFS lorsque vous lancez la commande pour la première fois et êtes invité à entrer vos credentials.

    Code

    Comme les credentials sont enregistrés dans nos cookies, l’URI de la collection TFS est le seul paramètre obligatoire dont nous avons besoin pour appeler la méthode GetTeamProjectCollection.

    
    $tfsCollectionUri = [System.Uri]"https://tfs.myprojectname.com/DefaultCollection"
    
    $projectsCollection = [Microsoft.TeamFoundation.Client.TfsTeamProjectCollectionFactory]::GetTeamProjectCollection($tfsCollectionUri) #this method never throw exceptions
    
    $projectsCollection.EnsureAuthenticated() #this method throw an exception on bad URI or wrong credentials
    
    if($projectsCollection.HasAuthenticated)
    {
    # actions list
    }
    else
    {
    Write-Warning "you are not authenticated to $tfsCollectionUri" #this case should never append as EnsureAuthenticated() throw an exception on bad credentials
    }
    

    Obtenir les services de votre collection de projets TFS

    Plusieurs services sont utiles depuis une collection de projets TFS :

    • builds servers : collection de serveurs qui font tourner les Build TFS ; ils sont nécessaires pour interroger les builds TFS.
    • work item store : connection work item tracking client sur les serveurs Team Foundation ; ce service est nécessaire pour interroger les projets TFS.
    • test management service : objet principal de l’API test management client, ce service est nécessaire pour obtenir les résultats des tests unitaires, ainsi que la couverture de code (dépendant d’un projet TFS).

    Pour obtenir ces services, nous utilisons la méthode GetService de l’objet TfsTeamProjectCollection. Cette méthode retourne une instance du service requêté s’il peut être trouvé, ou une ‘reference null’ dans le cas contraire.

    Code

    
    $buildServer = $projectsCollection.GetService([Microsoft.TeamFoundation.Build.Client.IBuildServer])
    
    $workItemStore = $projectsCollection.GetService([Microsoft.TeamFoundation.WorkItemTracking.Client.WorkItemStore])
    
    $testManagementService = $projectsCollection.GetService([Microsoft.TeamFoundation.TestManagement.Client.ITestManagementService])
    

    Fonction : connexion à TFS, obtenir la collection de projets et les services TFS

    Retrouvez la Function Get-TfsConnection dans le module Module-TFS.psm1 sur GITHUB.

    Obtenir les données de builds TFS

    Comme nous l’avons vu dans les sections précédentes, il est utile d’obtenir le service build server de la collection de projets, car ce service nous permet d’interroger les builds.

    Les exemples suivants partent du prédicat que vous avez instancié un build server dans le champ $buildServer.

    Obtenir une build TFS

    L’interface IBuildServer du namespace Microsoft.TeamFoundation.Build.Client possède la méthode QueryBuilds, voici ses surcharges :

    Name

    Description

    QueryBuilds(String)

    Gets all builds for a team project.

    QueryBuilds(IBuildDefinition)

    Gets all builds for a build definition.

    QueryBuilds(IBuildDefinitionSpec)

    Gets all builds for a build definition specification.

    QueryBuilds(IBuildDetailSpec)

    Gets a single build query result for the specified build specification.

    QueryBuilds(IBuildDetailSpec[])

    Gets the build query results for the specified list of build specifications.

    QueryBuilds(String, String)

    Gets all builds for a team project and definition.

    La surcharge la plus simple pour obtenir les informations utilise le nom du projet et le nom de la build comme paramètres. Elle retourne les informations complètes (QueryOptions = “All”, InformationTypes = “*”) de toutes les occurrences de la build, mais c’est un processus lourd (jusqu’à à 1 go de mémoire utilisé) et très long, environ 16 minutes (les exemples ont été chronométrés depuis une build d’intégration continue âgée de 6 mois)

    Code

    process time : 983757 ms

    
    $tfsBuilds = $buildServer.QueryBuilds("yourProjectName", "yourBuildName")
    

    La surcharge la plus versatile utilise un objet IBuildDetailSpec comme paramètre. Elle nous offre un contrôle complet sur l’interrogation à l’aide des propriétés suivantes :

    Name

    Description

    BuildNumber

    Gets or sets the number of the desired builds. Wildcard characters are supported.

    DefinitionSpec

    Gets the build definition specification of the desired builds.

    DefinitionUris

    Gets the build definition uniform resource identifiers (URIs) of the desired builds.

    InformationTypes

    Gets or sets the information types that will be returned from the query or queries.

    MaxBuildsPerDefinition

    Gets or sets the maximum number of builds to return per definition.

    MaxFinishTime

    Gets or sets the end of the finish time range of the specified builds.

    MinChangedTime

    Gets or sets the earliest revision date and time of the desired builds.

    MinFinishTime

    Gets or sets the start value of the finish time range of the specified builds.

    Quality

    Gets or sets the quality of the desired builds.

    QueryDeletedOption

    Gets or sets options to query deleted builds.

    QueryOptions

    Gets or sets the additional data that will be returned from the queries.

    QueryOrder

    Gets or sets the ordering scheme to use when the user sets a maximum number of builds.

    Reason

    Gets or sets the reason for the desired builds.

    RequestedFor

    Gets or sets the user for whom the build was requested.

    Status

    Gets or sets the statuses of the desired builds.

    Si vous voulez interroger toutes les occurrences d’une build, vous pouvez choisir de retourner le niveau minimum d’informations (QueryOptions = “None”, InformationTypes = $null) ; le processus est bien plus rapide, environ 0.3 secondes.

    Code

    process time : 319 ms

    
    $buildSpecification = $buildServer.CreateBuildDetailSpec("yourProjectName", "yourBuildName")
    
    $buildSpecification.QueryOptions = "None"
    $buildSpecification.InformationTypes = $null
    
    $tfsBuilds = $buildServer.QueryBuilds($buildSpecification)
    

    Si vous interrogez un numéro de build, vous interrogez une seule occurrence. Le processus reste rapide même si vous demandez toutes les informations disponibles pour la build, environ 1.7 secondes.

    process time : 1726 ms

    
    $buildSpecification = $buildServer.CreateBuildDetailSpec("yourProjectName", "yourBuildName")
    
    $buildSpecification.BuildNumber = "yourBuildNumber"
    
    $tfsBuild = $buildServer.QueryBuilds($buildSpecification)
    

    Notre but est souvent d’interroger la dernière occurrence d’une build pour un nom de build donné. Comme nous ne pouvons pas deviner le build number de cette occurrence, combiner les 2 exemples précédents nous permet d’obtenir toutes les informations de la dernière occurrence d’une build en un minimum de temps (les informations apportées par la propriété QueryOptions ne sont pas nécessaires) ; le processus a été chronométré à environ 2.1 secondes.

    process time : 2131 ms

    
    $buildSpecification = $buildServer.CreateBuildDetailSpec("yourProjectName", "yourBuildName")
    
    $buildSpecification.QueryOptions = "None"
    $buildSpecification.InformationTypes = $null
    
    $tfsBuilds = $buildServer.QueryBuilds($buildSpecification)
    
    # by default, QueryOrder property on build specification is StartTimeAscending
    $tfsLastBuildNumber = ($tfsBuilds.Builds | ? { $_.Status -ne "InProgress" } | Select-Object -Last 1).BuildNumber
    
    $buildSpecification.InformationTypes = "*"
    $buildSpecification.BuildNumber = $tfsLastBuildNumber
    
    $tfsBuild = $buildServer.QueryBuilds($buildSpecification)
    

    Fonction : obtenir une build TFS

    Retrouvez la Function Get-TfsBuilds dans le module Module-TFS.psm1 sur GITHUB.

    Compilation, code analysis, unit tests & code coverage

    Les exemples suivants partent du prédicat que vous avez instancié une build TFS.

    Obtenir les avertissements et erreurs de la build

    Les résultats de la compilation et du code analysis sont imbriqués dans les build errors et warnings.

    Pour obtenir les erreurs de la build, nous utilisons la méthode GetBuildErrors de la classe InformationNodeConverters depuis le namespace Microsoft.TeamFoundation.Build.Client.

    Pour obtenir les avertissements de la build, nous utilisons la méthode GetBuildEWarnings de la classe InformationNodeConverters depuis le namespace Microsoft.TeamFoundation.Build.Client.

    Ces 2 méthodes acceptent une interface IBuildDetails comme paramètre (la fonction Get-TfsBuilds retourne les builds comme objet BuildDetails) et retournent une List de BuildWarning ou BuildError.

    Ensuite pour séparer les résultats de compilation et code analysis, nous pouvons trier les résultats grâce aux propriétés ErrorType et WarningType. (la propriété WarningType est absente de la MSDN !?)

    Code

    
    $buildErrors = [Microsoft.TeamFoundation.Build.Client.InformationNodeConverters]::GetBuildErrors($tfsBuild)               
    $buildWarnings = [Microsoft.TeamFoundation.Build.Client.InformationNodeConverters]::GetBuildWarnings($tfsBuild)
    
    $compilationErrors = $buildErrors | ? { $_.ErrorType -eq "Compilation" }
    $compilationWarnings = $buildWarnings | ? { $_.WarningType -eq "Compilation" } 
    $codeAnalysisErrors = $buildErrors | ? { $_.ErrorType -eq "StaticAnalysis" }
    $codeAnalysisWarnings = $buildWarnings | ? { $_.WarningType -eq "StaticAnalysis" }
    

    Obtenir les résultats des tests unitaires et la couverture de Code

    Le service TestManagementService est nécessaire pour obtenir les tests unitaires et la couverture de code, nous avons vu précédemment comment l’obtenir.

    Depuis ce service, nous pouvons obtenir une interface ITestManagementTeamProject à l’aide de la méthode GetTeamProject qui utilise le nom du projet comme paramètre.

    L’interface ITestManagementTeamProject possède les helpers TestRuns & CoverageAnalysisManager.

    Pour obtenir les résultats des tests unitaires, nous utilisons la méthode TestRuns.ByBuild. Pour obtenir la couverture de code nous utilisons la méthode CoverageAnalysisManager.QueryBuildCoverage. Ces 2 méthodes prennent l’Uri d’une build comme paramètre (l’Uri est un membre de l’objet BuildDetails). La méthode QueryBuilCoverage utilise aussi l’énumération CoverageQueryFlags comme flag pour définir la quantité de données à retourner.

    Code

    
    $managementService =  $tfsBuilds.Tfs.TestManagementService.GetTeamProject("yourProjectName")
    
    $unitTests = $managementService.TestRuns.ByBuild($tfsBuild.Uri)
    
    $codeCoverage = $managementService.CoverageAnalysisManager.QueryBuildCoverage($tfsBuild.Uri,[Microsoft.TeamFoundation.TestManagement.Client.CoverageQueryFlags]::BlockData -bor [Microsoft.TeamFoundation.TestManagement.Client.CoverageQueryFlags]::Functions -bor [Microsoft.TeamFoundation.TestManagement.Client.CoverageQueryFlags]::Modules)
    

    Fonction : obtenir le détail d’une build

    Retrouvez la Function Get-TfsBuildDetails dans le module Module-TFS.psm1 sur GITHUB.

    Fonction : obtenir le détail de la dernière occurrence d’une build

    Retrouvez la Function Get-TfsLastBuildDetails dans le module Module-TFS.psm1 sur GITHUB.

    Partagez les données de build

    Mise en forme HTML

    Tous les objets renvoyés par les fonctions ci-dessus sont formatés sous forme de tableau à une dimension, néanmoins le type des occurrences du tableau varie : Collections.Hashtable, Collections.Specialized.OrderedDictionary ou PSCustomObject.

    Pour les convertir en tableau HTML, il nous faut d’abord obtenir les en-têtes

    Name

    Description

    PSCustomObject

    Serves as a placeholder object that is used when the PSObject() constructor, which has no parameters, is used.

    Collections.Hashtable

    Represents a collection of key/value pairs that are organized based on the hash code of the key.

    Collections.Specialized.OrderedDictionary

    Represents a collection of key/value pairs that are accessible by the key or index.

    Keys

    Gets an ICollection object containing the keys in the OrderedDictionary collection.

    Get-Member

    Gets the properties and methods of objects.

    Code

    
    if ($testLine -is [Collections.Hashtable] -or $testLine -is [Collections.Specialized.OrderedDictionary])
    {
    $propertiesName = $testLine.Keys
    }
    elseif ($testLine -is [PSCustomObject])
    {
    $propertiesName = $testLine | Get-Member -MemberType NoteProperty
    }
    else
    {
    Throw "this method only allow PsCustomObject or Hashtable content"
    }
    

    Maintenant que nous avons les en-têtes, nous pouvons construire les tableaux HTML avec en TH le nom des propriétés et en TD leurs valeurs.

    
    # write table start
    $htmlTable += "<table>$([Environment]::NewLine)"
    
    # write table header
    $htmlTable += "`t<tr$($TrClass)>"
    
    foreach ($propertyName in $propertiesName)
    {
    $htmlTable += "<th$($ThClass)>$propertyName</th>"
    }
    
    $htmlTable += "</tr>$([Environment]::NewLine)"
    
    # write table rows
    foreach ($entity in $Object)
    {
    $htmlTable += "`t<tr>"
    
    foreach ($propertyName in $propertiesName)
    {
    $propertyValue = [Web.HttpUtility]::HtmlEncode($entity.$propertyName)
    
    $htmlTable += "<td>$propertyValue</td>"
    }
    
    $htmlTable += "</tr>$([Environment]::NewLine)"
    }
    
    #write table end
    $htmlTable += "</table>"
    

    Fonction : convertir les objets PowerShell en tableau HTML

    Retrouvez la Function Write-ObjectToHtml dans le module Module-IO.psm1 sur GITHUB.

    Envoi d’emails

    La première chose à faire pour envoyer un email, c’est de connaître les paramètres de son compte email. Pour Gmail par exemple, je les ai obtenus sur une page de support Google : https://support.google.com/a/answer/176600?hl=en

    • Server : smtp.googlemail.com
    • Port : 587
    • UseSsl : true

    Le 2ème point d’attention, c’est la sauvegarde des credentials. Pour cela PowerShell propose le format crypté CLIXML et les fonctions Get-Credential, Export-Clixml et Import-Clixml.

    
    $emailCredentialsFilePath = "C:\Example\mail@$($env:username)@$($env:computername).clixml"
    
    # enregistrer les crédentials :
    
    Get-Credential -Message "Please enter your Email credentials" | Export-Clixml $emailCredentialsFilePath
    
    # appeler les crédentials :  
    
    $emailCredentials = Import-Clixml $emailCredentialsFilePath
    

    Tout est prêt pour commencer à envoyer des emails ; la fonction Send-MailMessage est la plus pratique, mais elle montre ses limites lors de lancements frénétiques répétés pour les tests. Pour contourner ce problème, on peut construire une méthode qui instancie et dispose proprement les objets System.Net.Mail.SmtpClient et System.Net.Mail.MailMessage.

    Name

    Description

    Send-MailMessage

    Send an email message

    System.Net.Mail.SmtpClient

    Allows applications to send e-mail by using the Simple Mail Transfer Protocol (SMTP).

    System.Net.Mail.MailMessage

    Represents an e-mail message that can be sent using the SmtpClient class.

    Get-Credential

    Gets a credential object based on a user name and password.

    Export-Clixml

    Creates an XML-based representation of an object or objects and stores it in a file.

    Import-Clixml

    Imports a CLIXML file and creates corresponding objects in Windows PowerShell.

    Code

    
    # get email title and recipients
    $subject = "Build report CI-SOAT_20170227.1"
    $recipients = @{scrumMaster@soat.fr, ProductOwner@soat.fr, Architect@soat.fr, ProjectDirector@soat.fr}
    $attachments= @{"C:\Example\Compilation.html", "C:\Example\UnitTests.html", "C:\Example\CodeCoverage.html"}
    
    Write-Verbose "Sending email $subject to $recipients"
    
    # i use System.Net.Mail.SmtpClient Object instead of Send-MailMessage function to get more controls and dispose functionnality
    try
    {
    # create mail and server objects
    $message = New-Object -TypeName System.Net.Mail.MailMessage
    $smtp = New-Object -TypeName System.Net.Mail.SmtpClient($buildInfoData.BuildReports.Mail.Server)
    
    # build message
    $recipients | % { $message.To.Add($_) }
    $message.Subject = $subject
    $message.From = New-Object System.Net.Mail.MailAddress($emailCredentials.UserName)
    $message.Body = $mailHtml
    $message.IsBodyHtml = $true
    $attachments | % { $message.Attachments.Add($(New-Object System.Net.Mail.Attachment $_)) }
    
    # build SMTP server
    $smtp = New-Object -TypeName System.Net.Mail.SmtpClient([string]$buildInfoData.BuildReports.Mail.Server)
    $smtp.Port = [int]$buildInfoData.BuildReports.Mail.Port
    $smtp.Credentials = [System.Net.ICredentialsByHost]$emailCredentials
    $smtp.EnableSsl = [bool]$buildInfoData.BuildReports.Mail.UseSsl
    
    # send message
    $smtp.Send($message)
    
    Write-Host "Email message sent" 
    }
    catch
    {
    Write-Warning "$($_.Exception | Select Message, Source, ErrorCode, InnerException, StackTrace | Format-List | Out-String)" 
    }
    finally
    {
    Write-Verbose "Disposing Smtp Object"
    $message.Dispose()
    $smtp.Dispose()
    }
    

    Script : Partagez vos indicateurs de qualité

    Le fichier de données au format XML BuildInfo.xml contient les paramètres et le Template HTML.

    Le script PowerShell Send-BuildInfos.ps1 effectue tout ce que nous avons vu dans cet article. Les entrées sont stockées dans BuildInfo.xml, TFS est interrogé via le Module-TFS.psm1, les fonctions de Login et de conversion HTML sont fournies par Module-IO.psm1, toutes les ressources sont disponibles sur GITHUB.

    Conclusion

    Vous avez maintenant tous les éléments en main pour télécharger ou produire un script PowerShell, qui lancé à partir d’un poste client ayant accès à votre serveur TFS, partagera vos indicateurs de qualité vers la liste d’emails de votre choix.

    Vous pouvez définir une tâche planifiée pour partager le résultat de la dernière occurrence d’une build tous les jours, ou l’ajouter en action post build d’une build d’intégration.

    Vous pouvez également charger le module-Tfs.psm1 dans une session PowerShell pour interroger votre serveur en temps réel en ajoutant des conditions sur les statuts de build ou les dates.

    © SOAT
    Toute reproduction interdite sans autorisation de la société SOAT