Competition: What’s your favorite Redgate tool? Enter now.

TFS Automatic Deployment and Email Script

SteveGTRSteveGTR Posts: 91
Hopefully someone will find this useful. This script is called by a custom TFS process script after our website has been built and the package has been created on the NuGet feed. The xaml file is based on the code posted by Justin at http://thefutureofdeployment.com/callin ... r-exe-tfs/. Instead of executing DeploymentManager.exe from the xaml, my version executes the following powershell script that performs the following tasks:

1) Executes DeploymentManager.exe

2) Archives old NuGet packages

3) Sends out an email message with annotated changes

With a little modification, this script might be useful for your project.
# To run via -File parameter do the following:
# Set-ExecutionPolicy Unrestricted

# Get the packages that are going to be deployed

$Feed = "\\servername\DeploymentFeed";
$Archive = "\\servername\DeploymentFeedArchive";

$WebPagePackages = $Feed + "\WebPage*.nupkg"; 
$MergePageDatabasePackages = $Feed + "\MergePage*.nupkg";

$CurrentWebPackage = Get-ChildItem $WebPagePackages | sort LastWriteTime | select -last 1;
$CurrentMergePagePackage = Get-ChildItem $MergePageDatabasePackages | sort LastWriteTime | select -last 1;

# Execute the deployment manager

&"C:\Program Files\Red Gate\DeploymentManager.EXE" create-release --server=http://servername:8080/ --apiKey=YourKey --project=ProjectName --deployto=Development --releasenotes="Automatically generated from TFS website build." --waitfordeployment --httpConnectTimeout=00:05:00 | Write-Host;

if ($LastExitCode -ne 0)
{
	Write-Host ("Deployment manager returned error: " + $LastExitCode);
	
	exit $LastExitCode;
}

# Move all files, but the latest to archive

if ($CurrentWebPackage -ne $null)
{
	Write-Host ("Current WebPage package: " + $CurrentWebPackage.Name);

	$Parts = $CurrentWebPackage.Name.Split(".");
	
	if ($Parts[4] -ne $null)
	{
		$WebPageBuildNumber = $Parts[4];
		$Version = $Parts[1] + "." + $Parts[2] + "." + $Parts[3] + "." + $Parts[4];
		
		Write-Host ("Current WebPage Build Number: " + $WebPageBuildNumber);
		Write-Host ("Current Deployment Version: " + $Version);
	}
	
	$ArchiveFiles = Get-ChildItem -Path $WebPagePackages -Exclude $CurrentWebPackage.Name;

	if ($ArchiveFiles -ne $null)
	{
		foreach ($File in $ArchiveFiles) 
		{ 
			Write-Host ("Archiving: " + $File.Name);
			Move-Item $File $Archive | Write-Host;
		}
	}
}

if ($CurrentMergePagePackage -ne $null)
{
	Write-Host ("Current MergePage Database package: " + $CurrentMergePagePackage.Name);

	$Parts = $CurrentMergePagePackage.Name.Split(".");
	
	if ($Parts[3] -ne $null)
	{
		$MergePageBuildNumber = $Parts[0] + "_" + $Parts[2] + "." + $Parts[3];
		
		Write-Host ("Current MergePage Database Build Number: " + $MergePageBuildNumber);		
	}

	$ArchiveFiles = Get-ChildItem -Path $MergePageDatabasePackages -Exclude $CurrentMergePagePackage.Name;

	if ($ArchiveFiles -ne $null)
	{
		foreach ($File in $ArchiveFiles) 
		{ 
			Write-Host ("Archiving: " + $File.Name);
			Move-Item $File $Archive | Write-Host;
		}
	}
}

if ($WebPageBuildNumber -ne $null -and $MergePageBuildNumber -ne $null)
{

	#---- Start of code to enumerate build details

	[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");

	if ((Get-PSSnapin -Name Microsoft.TeamFoundation.PowerShell -ErrorAction SilentlyContinue) -eq $null)
	{
		Add-PSSnapin Microsoft.TeamFoundation.PowerShell;
	} 

	$html = "<table>";

	$Packarray = 
		@(
		[pscustomobject]@{PackageName="Website package: $CurrentWebPackage";ProjectName="MergePage";BuildName="MergePage";LocationToSearch="$/MergePage";BuildNumber=$WebPageBuildNumber},
		[pscustomobject]@{PackageName="Database package: $CurrentMergePagePackage";ProjectName="PageDatabases";BuildName="MergePage";LocationToSearch="$/PageDatabases/MergePage";BuildNumber=$MergePageBuildNumber}
	);

	foreach ($p in $Packarray)
	{
		$html += 
			"<tr>" + 
				"<td colspan=4>" + 
					$p.PackageName + 
				"</td>" + 
			"</tr>";

		$projectName =  $p.ProjectName;
		$buildName = $p.BuildName;
		$locationToSearch = $p.LocationToSearch;
		$buildNumber = $p.BuildNumber;

		$tfsCollectionUrl = "http://servername:8080/tfs/MergePageCollection";

		$server = new-object Microsoft.TeamFoundation.Client.TfsTeamProjectCollection(New-Object Uri($tfsCollectionUrl));

		$tfs = get-tfsserver $tfsCollectionUrl;

		$buildServer = $server.GetService([Microsoft.TeamFoundation.Build.Client.IBuildServer]);

		$buildDetail = 
			$buildServer.QueryBuilds($projectName, $buildName) | 
			where {($_.BuildNumber -eq $buildNumber -or ($_.BuildFinished -eq $true -and $_.Status -eq "Succeeded")) -and $_.LabelName -and $_.LabelName -ne "" } | 
			sort -desc StartTime | 
			select BuildNumber, StartTime, FinishTime;

		$vcs = $server.GetService([Microsoft.TeamFoundation.VersionControl.Client.VersionControlServer]);

		$i1 = 0;
		$i2 = $i1 + 1;

		# Construct the date range using label dates

		$LabelDetail = $vcs.QueryLabels($BuildDetail[$i2].BuildNumber, $locationToSearch, $null, $false) | select LastModifiedDate;

		$dateRange = "D" + (Get-Date($LabelDetail.LastModifiedDate) -format s) + "~";

		$LabelDetail = $vcs.QueryLabels($BuildDetail[$i1].BuildNumber, $locationToSearch, $null, $false) | select LastModifiedDate;

		$dateRange += "D" + (Get-Date($LabelDetail.LastModifiedDate) -format s);

		$changeDetail = Get-TfsItemHistory $locationToSearch -Server $tfs -Version $dateRange -Recurse -IncludeItems | sort -desc ChangesetId;

		$changeFound = $false;

		# Because the Webpage building process is still active and hasn't finished, use the current date on the build
		# agent. This is the best I could do.
		if ($BuildDetail[$i1].FinishTime -eq $null -or $BuildDetail[$i1].FinishTime -lt $BuildDetail[$i1].StartTime)
		{
			$FinishTime = Get-Date;
		}
		else
		{
			$FinishTime = $BuildDetail[$i1].FinishTime;
		}

		$html += 
			"<tr>" + 
				"<td colspan=2 nowrap>" + 
					"Build: " + $BuildDetail[$i1].BuildNumber + 
				"</td>" +
				"<td nowrap>" +
					"Started: " + (Get-Date($BuildDetail[$i1].StartTime) -format g) + 
				"</td>" + 
				"<td nowrap>" + 
					"Duration: {0:N1} minutes" -f ($FinishTime - $BuildDetail[$i1].StartTime).TotalMinutes + 
				"</td>" + 
			"</tr>";

		foreach ($changeSet in ($changeDetail | select ChangesetId, Committer, CreationDate, Comment))
		{
			$changeFound = $true;
	
			$html +=
				"<tr>" +
					"<td>" +
						" " +
					"</td>" +
					"<td nowrap>" +
						"Changeset: " + $changeSet.ChangesetID + 
					"</td>" +
					"<td nowrap>" +
						"Date: " + (Get-Date($changeSet.CreationDate) -format g) + 
					"</td>" +
					"<td nowrap>" +
						"User: " + $changeSet.Committer +
					"</td>" +
				"</tr>";
			
			if ($changeSet.Comment -and $changeSet.Comment -ne "")
			{
				$html += 
					"<tr>" +
						"<td>" +
							" " +
						"</td>" +
						"<td colspan=3>" +
							"Comments: " + $changeSet.Comment +
						"</td>" +
					"</tr>";
			}
			else
			{
				$html += 
					"<tr>" +
						"<td>" +
							" " +
						"</td>" +
						"<td colspan=3>" +
							"Comments: None" +
						"</td>" +
					"</tr>";	
			}
	
			foreach ($typeSet in ($changeDetail | where { $_.ChangesetId -eq $changeSet.ChangesetID } | Select-Object -Expand "Changes"))
			{
				$html +=
					"<tr>" +
						"<td>" +
							" " +
						"</td>" +
						"<td>" +
							" " +
						"</td>" +
						"<td colspan=2 nowrap>" +
							"Action: " + $typeSet.ChangeType +
						"</td>" +
					"</tr>";				
		
				foreach ($itemSet in ($typeSet | Select-Object -Expand "Item"))
				{
					$html +=
						"<tr>" +
							"<td>" +
								" " +
							"</td>" +
							"<td>" +
								" " +
							"</td>" +
							"<td>" +
								" " +
							"</td>" +					
							"<td>" +
								"Item: " + $itemSet.ServerItem +
							"</td>" +
						"</tr>";		
				}
			}
		}

		if ($changeFound -eq $false)
		{
			$html +=
				"<tr>" +
					"<td>" +
						" " +
					"</td>" +
					"<td colspan=3>" +
						"Nothing new included in build" + 
					"</td>" +
				"</tr>";			
		}
	}

	$html += "</table>";
}
else
{
	$html = "Could not determine version information, check the log file.";
}

#---- End of code

# Email notification messages

$EmailFrom = "fromemail";
$EmailTo = "toemails";
$Subject = "Version: " + $Version + " deployed to Development";
$Body = $html;
$SMTPServer = "smtpserver";
$SMTPClient = New-Object Net.Mail.SmtpClient($SmtpServer, 587);
$SMTPClient.EnableSsl = $true;
$SMTPClient.Credentials = New-Object System.Net.NetworkCredential("validemail", "password");
$Message = New-Object Net.Mail.MailMessage($EmailFrom, $EmailTo, $Subject, $Body);
$Message.IsBodyHtml = $true;
$SMTPClient.Send($Message);

exit 0;

Comments

  • Here's an example of the information that is formatted in the email:
    Subject: Version: 2.3.31.251 deployed to Development
    
    Website package: \\servername\DeploymentFeed\WebPage.2.3.31.251.nupkg 
    Build: 251 Started: 11/29/2013 11:00 PM Duration: 16.3 minutes 
      Changeset: 24226 Date: 11/29/2013 9:58 AM User: user1 
      Comments: None 
        Action: Edit 
          Item: $/MergePage/WebPAGE/Reporting/Controls/RevisionList.ascx 
    Database package: \\servername\DeploymentFeed\MergePage.0.20131122.1.nupkg 
    Build: MergePage_20131122.1 Started: 11/22/2013 11:22 AM Duration: 2.4 minutes 
      Changeset: 24206 Date: 11/22/2013 11:22 AM User: user2 
      Comments: None 
        Action: Edit 
          Item: $/PageDatabases/MergePage/Stored Procedures/dbo.Admin_Generate_Delinquent_PPR_FFR.sql 
        Action: Edit 
          Item: $/PageDatabases/MergePage/Stored Procedures/dbo.Admin_Generate_Delinquent_PPR_FFR_Inprocess.sql 
        Action: Edit 
          Item: $/PageDatabases/MergePage/Stored Procedures/dbo.usp_GetDelinquentTTA.sql 
        Action: Edit 
          Item: $/PageDatabases/MergePage/Stored Procedures/dbo.uspGetOMBControlNoInfo.sql 
        Action: Edit 
          Item: $/PageDatabases/MergePage/Views/dbo.vwAttachment.sql
    
  • Thanks for sharing Steve, this looks very helpful!
    Justin Caldicott
    Developer
    Redgate Software Ltd
Sign In or Register to comment.