Cleanup the GlobalList.Xml

TFS has been using the GlobalList.xml file to store builds since 2010. This Global List is used in several workitems as the suggested list of values. For example, in the Bug there are two fields that use this Found In and Integrated In. These drop down fields present a list of builds that comes from the GlobalList.

Although it's not the great solution and I hear it's changing. It's what we have had for 6 years so we have used it. I have many clients that rely on these work item fields and therefore the list of builds.

However, when you delete a build it cleans up a lot of stuff, including the label, Drop Folder, Symbols and Test Results. But not the entry in the GlobalList. L

Ours was getting out of hand. So I built a Powershell script that I could run that would clean up the Global List by checking to see if the build had been deleted from TFS. If it was removed it removes the build from the list. Because this list is implemented as a Suggested list in the work items removing them has no effect on the old workitems referring to this build. They will still have their value. It just means the cleaned up build will not be in the list to be selected anymore.

[Update on Nov 24 2016] I have since cleaned up and fixed this Powershell script. The Script does not upload until you set $UpdateServer = 1

[void][System.Reflection.Assembly]::LoadWithPartialName("Microsoft.TeamFoundation.Client")  
[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")  
 
Add-Type -AssemblyName System.Xml
 
cls
#Add the Entry to the Global List 
 
[string] $GlobalListFileName = "D:\Temp\GlobalList.xml"
[string] $NewGlobalListFileName = "D:\Temp\NewGlobalList.xml"
$RemovedBuilds = new-object System.Collections.ArrayList
[String] $GlobalListName = "Builds - NBFC"
[string] $tfsServer = "http://tfs.nbfc.com/tfs/products"
[string] $witadmin = "C:\Program Files (x86)\Microsoft Visual Studio 14.0\Common7\IDE\witadmin.exe"
[bool] $UpdateServer = 0
 
function GlobalList( [string] $action, [string] $GlobalList)
{
 
    $arguments = $action + ' /collection:"' + $tfsServer + '" /f:"' + $GlobalList + '"'
    write-host $witadmin $arguments
    #start-process -FilePath $witadmin -Argumentlist $arguments -wait -WindowStyle Hidden
 
    $pinfo = New-Object System.Diagnostics.ProcessStartInfo
    $pinfo.FileName = """$witadmin"""
    $pinfo.RedirectStandardError = $true
    $pinfo.RedirectStandardOutput = $true
    $pinfo.UseShellExecute = $false
    $pinfo.Arguments = $arguments
    $p = New-Object System.Diagnostics.Process
    $p.StartInfo = $pinfo
    $p.Start() | Out-Null
    $p.WaitForExit()
    $stdout = $p.StandardOutput.ReadToEnd()
    $stderr = $p.StandardError.ReadToEnd()
 
    if ($p.ExitCode -eq 0)
    {
        Write-Host "$action Successful"
           Write-Host "stdout: $stdout"
    }
    else
    {
        Write-Host "$action Failed" 
        Write-Error "Error: $stderr" 
        exit $p.ExitCode
    }
    $p.Close()
 
}
 
GlobalList "exportGloballist" $GlobalListFileName
 
#Load the contents of GlobalList.xml
[xml]$doc = Get-Content($GlobalListFileName)      
$root = $doc.DocumentElement
$BuildsList = $root.SelectSingleNode("GLOBALLIST[@name='$GlobalListName']")
 
#sort the list of builds
$orderedBuildsCollection = $BuildsList.LISTITEM | Sort Value
$BuildsList.RemoveAll()
$orderedBuildsCollection | foreach { $BuildsList.AppendChild($_) } | Out-Null
 
#Connect to TFS and get the Build Server
$server = new-object Microsoft.TeamFoundation.Client.TfsTeamProjectCollection(New-Object Uri($tfsServer))
$buildServer = $server.GetService([Microsoft.TeamFoundation.Build.Client.IBuildServer])
 
#Create a list of builds that are no longer in TFS
foreach ($child in $BuildsList.ChildNodes)
{
    [array]$items = $child.value.Split("/")
    [string]$buildDefinitionName = $items[0]
    [string]$buildNumber = $items[1]
    #Look in the XAML Builds List 
    $buildDetail = $buildServer.QueryBuilds("NBFC", $buildDefinitionName) | ? { $_.BuildNumber -eq $buildNumber }
    if ($buildDetail)
    {
       write-host "$($buildDefinitionName) - $($buildNumber) is Valid"
    }
    else
    {
        $apicall = "http://TFS.NBFC.COM:8080/tfs/Products/NBFC/_apis/build/builds/?api-version=2.0&DefinitionName=$buildDefinitionName&buildNumber=$buildNumber"
        $apicall
        $json = invoke-RestMethod -uri "http://TFS.NBFC.COM:8080/tfs/Products/NBFC/_apis/build/builds/?api-version=2.0&DefinitionName=$buildDefinitionName&buildNumber=$buildNumber" -method Get -UseDefaultCredentials
        if ($json.count -eq 0)
        {
            $RemovedBuilds +=  $child.value
            write-Warning "$($buildDefinitionName) - $($buildNumber) Will be Removed"
        }
        else
        {
            write-host "$($buildDefinitionName) - $($buildNumber) is Valid"
        }
    }    
}
 
#[xml]$mainlist = get-content $GlobalListFileName
 
foreach ($toRemove in $RemovedBuilds)
{
    $query = "//LISTITEM[@value=""$toRemove""]"
    write-host "Select node $query"
    $BuildNode = $doc.SelectSingleNode($query)
    if ($BuildNode)
    {
        $BuildNode.ParentNode.RemoveChild($BuildNode)
    }
    else
    {
    write-host "Can't find Entry $toRemove"
    }
}
 
#Sort the list 
#$orderedBuildsCollection = $BuildsList.LISTITEM | Sort Value
#$BuildsList.RemoveAll()
#$orderedBuildsCollection | foreach { $BuildsList.AppendChild($_) } | Out-Null
 
$doc.save($NewGlobalListFileName)
 
 
if ($UpdateServer)
{
    GlobalList "importgloballist" $NewGlobalListFileName
}