![]()
UPDATE #4: Just found another nice looking editor PowerShellIDE.
UPDATE #3: PowerShell team responded and their guidance to avoid issues with CMD messing up the arguments passed to the PowerShell script is to use single quotes around the PowerShell arguments. I updated the build event example below.
UPDATE #2: Fixed a minor bug in the build DeploymentScript.ps1. It was starting to parse the "Additional Args" at an index of 3 when it should have starting at 4.
UPDATE #1: Forgot to link to PowerShell Analyzer. A nice tool for working with PowerShell, have not played with it a ton yet, but it looks promising.
Working in a team with multiple developers and different environments often introduces the need for multiple deployment configurations.
There are quite a few interesting tools out there for advanced deployment configuration tasks:
- NAnt
- FinalBuilder
- BuildIt
- More?
Link in the comments if you know of some good ones that I have missed.
In my case I just need something simple that each developers on my team (currently only 4-5 regular contributors) can use and customize. In the past I have used Windows Scripting Host scripts written in JScript to perform my post build events but with the release of PowerShell I was interested in finding a task that would afford me the excuse of learning a new technology (you can never know too many).
My solution at the moment is a PowerShell script that loads information from an XML file based on Project, Build Configuration, User Name, and Build Event Type (Pre or Post). The XML file is essentially a list of PowerShell scripts depending on the passed in variables it loads the proper PowerShell script and executes it.
<tangent degree="Minor">
I'm really excited about how it can be extended with the use of custom CmdLets; a CmdLet pack that supported common Visual Studio Team Foundation activities like TF.exe would add a lot of value of the type of scenario that I am playing with here. I am also interested in finding CmdLets or information on how to perform remote command execution (executing CMD or PowerShell commands on remote machines). Anyone know of examples or projects that cover these scenarios?
</tangent>
Onto the examples (I know what you are here for)...
Pre/Post Build Event in Visual Studio .NET
A simple example of the actually CMD line that is executed as part of Visual Studio's configured post-build event. Use single quotes for the arguments that should be passed to the PowerShell script. That will avoid CMD causing any problems by the way it handles double quoted string. Note that for some reason the quotes that come immediately after a Visual Studio variable that ends with a \ have to have an additional \ placed in front of them. I have communicated with the PowerShell team about this and believe that they are aware of the behavior.
The first 5 arguments are required and are used to lookup the script that should be executed. Additional argument are optional and can be configured on a per project basis, they are loaded into a Hashtable using a (key)=(value) format and are used to replace variables in the executed script.
powershell.exe "$(SolutionDir)DeployScript.ps1" 'Post' '$(SolutionDir)' '$(TargetFileName)' '$(ConfigurationName)' 'targetDir=$(TargetDir)' 'projectDir=$(ProjectDir)'
DeployConfig.xsd
The documented schema that can be used to validate DeployConfig.xml. Word wrap makes it a little messy.. sorry about that.
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="configuration">
<xs:annotation>
<xs:documentation>The root element of the DeployConfig XML document used by the DeployScript.ps1 PowerShell script.</xs:documentation>
</xs:annotation>
<xs:complexType>
<xs:sequence>
<xs:element name="project" maxOccurs="unbounded">
<xs:annotation>
<xs:documentation>Each project that is being configured with a build event will have it's own project node</xs:documentation>
</xs:annotation>
<xs:complexType>
<xs:sequence>
<xs:element name="buildConfig" maxOccurs="unbounded">
<xs:annotation>
<xs:documentation>Each build configuratoin (ie. Debug, Release) should have it's own buildConfig node.</xs:documentation>
</xs:annotation>
<xs:complexType>
<xs:sequence>
<xs:element name="user" maxOccurs="unbounded">
<xs:annotation>
<xs:documentation>Each user can be configured to execute a different PowerShell script by creating a new user node that specifies their Active Directory username (eg. domain\user). If no node is found for the current user DeployScript.ps1 will search for a user node with the name attribute set to 'default'</xs:documentation>
</xs:annotation>
<xs:complexType>
<xs:sequence>
<xs:element name="buildEvent" maxOccurs="2">
<xs:annotation>
<xs:documentation>Each user can have two build events specified for each project and build configuration. One can be executed on pre-build and one on post-build. The text for this node is the actual script that will be executed on build. It is recommended that the commands within this node be wrapped with CDATA.</xs:documentation>
</xs:annotation>
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute name="name" use="required">
<xs:annotation>
<xs:documentation>Possible values are 'Pre' and 'Post' depending on when the event should be executed.</xs:documentation>
</xs:annotation>
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:enumeration value="Pre"/>
<xs:enumeration value="Post"/>
</xs:restriction>
</xs:simpleType>
</xs:attribute>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
</xs:sequence>
<xs:attribute name="name" type="xs:string" use="required">
<xs:annotation>
<xs:documentation>The name of the user that this build script applies to. Use 'default' to specify a default script that should be run if the current username is not found.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
</xs:element>
</xs:sequence>
<xs:attribute name="name" type="xs:string" use="required">
<xs:annotation>
<xs:documentation>The Build Configuration of the executing project build. Visual Studio Build Event Variable: $(ConfigurationName)</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
</xs:element>
</xs:sequence>
<xs:attribute name="targetFileName" type="xs:string" use="required">
<xs:annotation>
<xs:documentation>The Target File Name of the project that the build event is being configured for. Visual Studio Build Event Variable: $(TargetFileName)</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:schema>
DeployConfig.xml
This is a simple example of a post build event for a sample project using the Visual Studio Post Build Event example above. Note that the optional arguments in the Post Build Event are used to populate the actual command using string.Replace. Keys from the optional arguments Hashtable are searched for (encapsulated in {}) and replaced with the value from the Hashtable.
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<project targetFileName="Namespace.Sample.dll">
<buildConfig name="Debug">
<user name="domain\username">
<buildEvent name="Post"><![CDATA[remove-item -path "\\server\c$\Inetpub\wwwroot\bin\*.pdb", "\\server\c$\Inetpub\wwwroot\bin\*.dll", "\\server\c$\Inetpub\wwwroot\*.svc", "\\server\c$\Inetpub\wwwroot\*.config" -force
copy-item -path "{targetDir}*.dll","{targetDir}*.pdb" -destination "\\server\c$\Inetpub\wwwroot\bin" -force
copy-item -path "{projectDir}*.config","{projectDir}*.svc" -destination "\\meservices01\c$\Inetpub\wwwroot" -force]]></buildEvent>
</user>
</buildConfig>
</project>
</configuration>
DeployScript.ps1
The PowerShell script that is executed during the Pre/Post build event. I invite anyone with good ideas to modify this script and repost it to their blog with a link back to this post. Join the conversation and help me make this script better!
trap {
$errorTxt = $_.Exception.Message
write-output "$errorTxt"
break
}
$buildEvent = $args[0]
$solutionDir = $args[1]
$targetFile = $args[2]
$configName = $args[3]
write-output ""
write-output "*******************************************"
write-output "Required Args:"
write-output ""
write-output "Build Event: $buildEvent"
write-output "Configuration Name: $configName"
write-output "Target File Name: $targetFile"
write-output "Solution Directory: $solutionDir"
write-output "*******************************************"
write-output "Additional Args:"
write-output ""
$argsHash = @{}
for($x=4; $x -lt $args.Length; $x++) {
$curArg = $args[$x]
$curArgSplit = $args[$x].split("=")
$argsHash.add($curArgSplit[0], $curArgSplit[1]);
write-output "$curArg"
}
write-output "*******************************************"
write-output ""
write-output "Deployment Started"
$configPath = (join-path $solutionDir "DeployConfig.xml")
[xml]$configDoc = get-content $configPath
write-output "Configuration Loaded from $configPath"
$projNode = $configDoc.configuration.project | ?{$_.targetFileName -eq $targetFile}
if($projNode -ne $null) {
write-output "Project Node Found: $targetFile"
}
else {
throw "No Project node could be found: $targetFile";
}
$configNode = $projNode.buildConfig | ?{$_.name -eq $configName}
if($configNode -ne $null) {
write-output "Build Config Node found: $configName"
}
else {
throw "No Build Config node could be found: $configName";
}
#$compName = (get-wmiobject win32_computersystem).Name
$currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
$userNode = $configNode.user | ?{$_.name -eq $currentUser}
if($userNode -eq $null) {
write-output "User Node not found for $currentUser"
$userNode = $configNode.user | ?{$_.name -eq 'default'}
if($userNode -ne $null) {
write-output "User Node found: default"
}
else {
throw "No User Node (named or default) could be found: $currentUser"
}
}
else {
write-output "User Node found: $currentUser"
}
$buildEventNode = $userNode.buildEvent | ?{$_.name -eq $buildEvent}
if($buildEventNode -eq $null) {
throw "No build event could be found for the current build event: $buildEvent"
}
else {
write-output "Build Event Node found: $buildEvent"
}
$deployCmd = $buildEventNode.get_InnerText()
foreach($arg in $argsHash.Keys) {
$deployCmd = $deployCmd.Replace("{$arg}", $argsHash[$arg]);
}
write-output "Executing Command:"
invoke-expression $deployCmd
write-output "Deployment Ended"
