POST POST

SEP
3
2015

Configuring Features for Many TeamProjects in TFS 2015

One of the problems that comes with having multiple Team Project Collections and multiple Team Projects (in TFS) is the administrative burden required to upgrade or manage all of these projects. Security permissions, WIT modifications, configuration are all a 0..n problem so the more Team Projects you have, the more work it is, out of the box, to manage your TFS implementation.

There are numerous people and projects who have stepped up to help reduce this burden with applications, PowerShell scripts, and techniques for getting more work done with less effort.

One of those projects is Features4tfs, a command line application project that builds on a couple blog posts to make feature configuration easier when dealing with multiple TeamProjects.

Unfortunately, I've discovered that something happened between TFS 2015 RC and TFS 2015 RTM and this project no longer works. I've updated the code to use the latest RTM Object model binaries but I've just been unable to get it working. A few other people have run into this problem as well, and we've been unable to get any help or answers about this problem.

Regardless of getting help or not, I need to keep my client's migration/upgrade project moving forward and to that end, PowerShell, IE Automation and my recent work with the TFS 2015 Object Model in PowerShell to the rescue!

Implementing the Automate-IEConfigureFeatures Script

In order to understand this script, you'll need to make sure you understand what I'm doing with IE Automation and using a TFS PowerShell Module that I've discussed previously on this (and my) blog. I'll be using techniques from both those posts.

First, we need to enhance my TFS PowerShell module to add a cmdlet that it didn't have from the previous post.

Implement Get-TfsTeamProjects CmdLet

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
function Get-TfsTeamProjects() {
<#
.SYNOPSIS
Get a collection of Team Projects from a Team Project Collection
.DESCRIPTION
Get a collection of Team Projects from a Team Project Collection (TPC) using the Id (guid) from the TPC object
.EXAMPLE
Get-TfsTeamProjects $configServer "000000-0000-000000-000000000" <--- GUID
.EXAMPLE
Get-TfsTeamProjects $cs <tpcID Here>
.PARAMETER configServer
The TfsConfigurationServer object that represents a connection to TFS server that you'd like to access
.PARAMETER teamProjectCollectionId
The id (guid) of the TeamProjectCollection that you'd like to get a list of TeamProjects from
#>

[CmdLetBinding()]
param(
[parameter(Mandatory = $true, ValueFromPipeline = $true)]
[Microsoft.TeamFoundation.Client.TfsConfigurationServer]$configServer,
[parameter(Mandatory = $true)]
[guid]$teamProjectCollectionId

)
begin{}
process{
$tpc = $configServer.GetTeamProjectCollection($teamProjectCollectionId)
#Get WorkItemStore
$wiService = $tpc.GetService([Microsoft.TeamFoundation.WorkItemTracking.Client.WorkItemStore])
#Get a list of TeamProjects
$wiService.Projects
}
end{}
} #end function Get-TfsTeamProjects

In this CmdLet, we build on our understanding of the TFS Object Model and, using the WorkItemStore, get a list of all TeamProject in a TPC and return that list from the cmdlet.

Composing our IE Automation Script

Luckily, the Feature Configuration page is simple, easily addressable, and behaves consistently so it is actually very easy to automate.

Now we're going to Import-Module (ipmo alias in PowerShell) my TFS PowerShell module. We'll use that functionality for connecting to TFS and getting the lists of TeamProjectCollections and TeamProjects. This Script is not going to be a cmdlet, so it isn't going to be as pretty (or well documented, or perhaps efficient) as the TFS module we've been using.

There is a function in this Script to help with quickly finding buttons that we're expecting on the TFS Web Access Admin page we're working on.

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
cls

ipmo <TfsPowerShellModuleNameHere>

function Find-Button($ieDoc, $btnText){
$btns = $ieDoc.getElementsByTagName("button")
foreach($innerBtn in $btns)
{
if($innerBtn.parentElement.className -ne "ui-dialog-buttonset") {continue}
$innerSpans = $innerBtn.getElementsByTagName("span")
foreach($span in $innerSpans)
{
if (($span.InnerText) -and ($span.InnerText.Contains($btnText))) {
#find the button that has a span that has the text btnText
$span.parentElement
break;
}
}
}
}

$ie = new-object -ComObject "InternetExplorer.Application"

$cs = Get-TfsConfigServer <TFS AppTier URL here>
$tpcIds = Get-TfsTeamProjectCollectionIds $cs

foreach ($tpcId in $tpcIds){

$tpc = Get-TfsTeamProjectCollection $cs -teamProjectCollectionId $tpcId
[string]$tpcUri = $tpc.Uri.AbsoluteUri

$projects = Get-TfsTeamProjects -configServer $cs -teamProjectCollectionId $tpcId
foreach ($proj in $projects){
[string]$projectName = $proj.Name
$requestUri = [string]::Format("{0}/{1}/_admin#_a=enableFeatures", $tpcUri, $projectName.Replace(" ", "%20"))
$verifyButtonText = "Verify"
$configureButtonText = "Configure"
$closeButtonText = "Close"

$ie.visible = $true
$ie.silent = $true
$ie.navigate($requestUri)
while($ie.Busy) { Start-Sleep -Milliseconds 100 }

$doc = $ie.Document

#discover Verification button
$btn = Find-Button $doc $verifyButtonText

if ($btn -eq $null) { continue }

#start Verification
$btn.click()

Start-Sleep -Milliseconds 1000

$buttonNotFound = $true
#wait for verification to complete
while ($buttonNotFound) {
$closeBtn = $null;$configBtn = $null;
$closeBtn = Find-Button $doc $closeButtonText
$configBtn = Find-Button $doc $configureButtonText
if (($closeBtn -ne $null) -or ($configBtn -ne $null)){
$buttonNotFound = $false;
}else {
Start-Sleep -Milliseconds 1000
}
}

if ($closeBtn -ne $null) {
Write-Host "Cannot configure features for TeamProject " -NoNewline
Write-host "($($proj.Name)). " -NoNewLine -ForegroundColor Yellow
Write-Host "It needs to be upgraded first."
$warningText = $doc.getElementById("issues-textarea-id").InnerText
Write-Host $warningText -ForegroundColor Red | fl -Force
$closeBtn.click()
}
elseif ($configBtn -ne $null) {
#start Configuration
$configBtn.click()

#wait for configuration to complete
Start-Sleep -Milliseconds 500

#close Configuration
$buttonNotFound = $true
while ($buttonNotFound) {
$closeBtn = $null;
$closeBtn = Find-Button $doc $closeButtonText
if ($closeBtn -ne $null){
$buttonNotFound = $false;
}else {
Start-Sleep -Milliseconds 500
}
}

$closeBtn.click()

}
else{
Write-Host "Failed to find a button"
}
}
}

Final Thoughts

So that is it. I hope that the script is self-explanatory enough for you. I hope that you take away from this blog that there are usually many ways to solve a problem and sometimes we just have to roll up our sleeves and get our hands dirty and do our work in a functional and non-elegant manner. Don't let minor technical glitches get in the way of getting your work done.

There are the side benefits to this that you don't need to understand how the Feature Configuration works at a code level. You just need to be able to get your automation to click buttons.


Dave White

Email Email
Web Web
Twitter Twitter
GitHub GitHub
LinkedIN LinkedIn
RSS

Looking for someone else?

You can find the rest of the Western Devs Crew here.

© 2015 Western Devs. All Rights Reserved. Design by Karen Chudobiak, Graphic Designer