Wednesday, 15 July 2015

Send Custom Build Notes with TFS Build Using PowerShell

One of the teams, required a customized build note including only Bug, User Story work items listed with few details. The default alert email of the TFS 2013 only send an email in below format which does not say which is the type of the work item associated with the build. It just said “work item <id>” for any work item and team was not happy about that.
image
Very interesting solution provided here with PS scripts by MIKAEL DEURELL.
Few issues were there in the sample script provided in MIKAEL DEURELL‘s article the team who requested this specific build note is not happy about.
1. Since generating of build notes, depends on the date of the last good build to get the associated work items for current build, it is not possible to queue a build for a previous changeset, to make it the last good build, and another with latest changeset, will not send the associated work items correctly.
2. It sends all the work items and the team only wanted user story and bug WITs.
3. If a task work item associated it does not send the parent work item.
4. Team wanted the note to be formatted in a more readable format.

To overcome these issues the enhanced version (by me) of the sample script provided in MIKAEL DEURELL‘s article  is below.


  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
[void][System.Reflection.Assembly]::LoadWithPartialName("Microsoft.TeamFoundation.Client")  
[void][System.Reflection.Assembly]::LoadWithPartialName("Microsoft.TeamFoundation.Common")
[void][System.Reflection.Assembly]::LoadWithPartialName("Microsoft.TeamFoundation.Build.Client")  
[void][System.Reflection.Assembly]::LoadWithPartialName("Microsoft.TeamFoundation.Build.Common")  
[void][System.Reflection.Assembly]::LoadWithPartialName("Microsoft.TeamFoundation.VersionControl.Client")  
[void][System.Reflection.Assembly]::LoadWithPartialName("Microsoft.TeamFoundation.VersionControl.Client.VersionSpec")
[void][System.Reflection.Assembly]::LoadWithPartialName("Microsoft.TeamFoundation.WorkItemTracking.Client")  

$tfsCollectionUrl = "http://tfsserver:8080/tfs/projectcollection"
$server = new-object Microsoft.TeamFoundation.Client.TfsTeamProjectCollection(New-Object Uri($tfsCollectionUrl))
$buildServer = $server.GetService([Microsoft.TeamFoundation.Build.Client.IBuildServer])
$buildDetail = $buildServer.QueryBuilds("teamprojectname", "builddefinitionname") | where { $_.BuildDefinition.LastGoodBuildUri -eq $_.Uri } #| select BuildNumber

$buildDetail.BuildNumber | Out-File "$PSScriptRoot\tmpBuildNumber.txt"

$versionControlServer = $server.GetService([Microsoft.TeamFoundation.VersionControl.Client.VersionControlServer])
$workItemStore = $server.GetService([Microsoft.TeamFoundation.WorkItemTracking.Client.WorkItemStore])
$LinkingService = $server.GetService([Microsoft.TeamFoundation.Client.TswaClientHyperlinkService])

$lastGoodBuildStartTime = $buildDetail.StartTime
$changesetVersionSpec = New-Object Microsoft.TeamFoundation.VersionControl.Client.ChangesetVersionSpec($buildDetail.SourceGetVersion.Substring(1)) 
$latest = [Microsoft.TeamFoundation.VersionControl.Client.VersionSpec]::Latest
$recursionType = [Microsoft.TeamFoundation.VersionControl.Client.RecursionType]::Full
$historySinceLastGoodBuild = $versionControlServer.QueryHistory("$/Orca/Main", $latest, 0, $recursionType, $null, $changesetVersionSpec, $latest,[Int32]::MaxValue, $true, $false)

$historySinceLastGoodBuild.Length | Out-File "$PSScriptRoot\tmpHistoryCount.txt"

$workItemsSinceLastGoodBuild = $historySinceLastGoodBuild | foreach-object {$_.workitems}

$workItemsSinceLastGoodBuild.Length | Out-File "$PSScriptRoot\tmpWorkItemCount.txt"

$tabName = "WorkItemTable"

#Create Table object
$WorkItemTable = New-Object system.Data.DataTable $tabName

#Define Columns
$colWorkItemType1 = New-Object system.Data.DataColumn WorkItemType,([string])
$colId = New-Object system.Data.DataColumn Id,([int])
$colTitle = New-Object system.Data.DataColumn Title,([string])
$colState = New-Object system.Data.DataColumn State,([string])
$colAssignedTo = New-Object system.Data.DataColumn AssignedTo,([string])
$colWILink = New-Object system.Data.DataColumn WILink,([string])

#Add the Columns
$WorkItemTable.columns.add($colWorkItemType1)
$WorkItemTable.columns.add($colId)
$WorkItemTable.columns.add($colTitle)
$WorkItemTable.columns.add($colState)
$WorkItemTable.columns.add($colAssignedTo)
$WorkItemTable.columns.add($colWILink)

$workItemsSinceLastGoodBuild |
foreach-object { 
    if ($_.Type.Name -eq "Task") 
    {
        # Get parent work item of task
        foreach($link in $_.WorkItemLinks)
        {
             if ($link.LinkTypeEnd.Name -eq "Parent")
             {
                $ParentWorkItem = $workItemStore.GetWorkItem($link.TargetId)

                #Create a row
                $row = $WorkItemTable.NewRow()

                #Enter data in the row
                $row.WorkItemType = $ParentWorkItem.Type.Name
                $row.Id =  $ParentWorkItem.Id
                $row.Title =  $ParentWorkItem.Title
                $row.State =  $ParentWorkItem.State
                $row.AssignedTo =  $ParentWorkItem.Fields["Assigned To"].Value
                $row.WILink = $LinkingService.GetArtifactViewerUrl($ParentWorkItem.Uri).AbsoluteUri

                #Add the row to the table
                $WorkItemTable.Rows.Add($row)

                break
             }
        }
    }
    else 
    {
        # Adding associated work item other than task

        #Create a row
        $row = $WorkItemTable.NewRow()

        #Enter data in the row
        $row.WorkItemType = $_.Type.Name
        $row.Id =  $_.Id
        $row.Title =  $_.Title
        $row.State =  $_.State
        $row.AssignedTo =  $_.Fields["Assigned To"].Value
        $row.WILink = $LinkingService.GetArtifactViewerUrl($_.Uri).AbsoluteUri

        #Add the row to the table
        $WorkItemTable.Rows.Add($row)

    }

}

$WorkItemTable.Rows.Count | Out-File "$PSScriptRoot\tmpWorkItemTableCount.txt"

$style = "<style>BODY{font-family: Arial; font-size: 10pt;}"
$style = $style + "TABLE{border: 1px solid black; border-collapse: collapse;}"
$style = $style + "TH{border: 1px solid black; background: #dddddd; padding: 5px; }"
$style = $style + "TD{border: 1px solid black; padding: 5px; }"
$style = $style + "</style>"


$messageBody = $WorkItemTable |sort WorkItemType,Id -Unique | Select-Object WorkItemType, Id, State, Title, AssignedTo, WILink | ConvertTo-HTML -head $style | Out-String

$Recipients = @("user1@yourOrg.com", "user2@yourOrg.com", "user3@yourOrg.com")

Send-MailMessage -From "tfs@yourorg.com" -To $Recipients -SmtpServer "YourSMTPServer" -Body $messageBody -Subject "$Env:TF_BUILD_BUILDNUMBER Build Notes" -BodyAsHtml

One more enhancement could be made to the script to generate the note in pre build and send it in post build. That is by changing the below script segment to write to a temp file in pre build.


1
2
3
4
5
$messageBody = $WorkItemTable |sort WorkItemType,Id -Unique | Select-Object WorkItemType, Id, State, Title, AssignedTo, WILink | ConvertTo-HTML -head $style | Out-String

$Recipients = @("user1@yourOrg.com", "user2@yourOrg.com", "user3@yourOrg.com")

Send-MailMessage -From "tfs@yourorg.com" -To $Recipients -SmtpServer "Ad-ex2003.domainx.local" -Body $messageBody -Subject "$Env:TF_BUILD_BUILDNUMBER Build Notes" -BodyAsHtml

Above can be changed as shown below so that it writes to a temp file in pre build.


1
$WorkItemTable |sort WorkItemType,Id -Unique | Select-Object WorkItemType, Id, State, Title, AssignedTo, WILink | ConvertTo-HTML -head $style | Out-File "$PSScriptRoot\..\tmpBuildNote.txt"

In post build the temp file content can be read like shown below and send the email.


1
2
3
4
5
$messageBody = Get-Content "$PSScriptRoot\..\tmpBuildNote.txt" | Out-String

$Recipients = @("user1@yourOrg.com", "user2@yourOrg.com", "user3@yourOrg.com")

Send-MailMessage -From "tfs@yourorg.com" -To $Recipients -SmtpServer "yourSMTPserver" -Body $messageBody -Subject "$Env:TF_BUILD_BUILDNUMBER Build Notes" -BodyAsHtml

The build scripts should be checked in to TFS as shown below.

image

If there is multiple script then a run script can be used to call other scripts. For example pre build run script for above shown picture is below.


1
2
Invoke-Expression "$PSScriptRoot\GenerateBuildNotes.ps1"
Invoke-Expression "$PSScriptRoot\ApplyVersionToAssemblies.ps1"

TFS build definition can be set to call the pre build and post build scripts.

image

With this build note as shown below can be obtained.

image

No comments: