Automated website deployment with PowerShell and SmartFTP

by Shannon Deminick 2. September 2010 07:56

SmartFTP is a fantastic FTP application which handles syncing files very effectively. This means that when you upload your entire website, SmartFTP will automatically detect changes and only upload what is required (instead of overwriting all of the files like some FTP applications do). For each project at TheFARM we have build scripts which run and create a time stamped ZIP package for each deployment environment with all of the necessary files formatted appropriately for each. Our deployment process then involves unzipping the contents of this file, opening up SmartFTP, connecting to the deployment destination and transfering all of the deployment files up (which SmartFTP synchronizes for us).

I thought it would be much more efficient if we automated this process. So we did some investigation and it turns out the SmartFTP conveniently has an API! So we decided to see if we could write a PowerShell script to use the SmartFTP api to automagically transfer/sync all of our deployment files in our Zip package to the necessary FTP site and with a bit of trial and error we managed to do it! Now, I’m not PowerShell expert or anything, and in fact this was my very first PowerShell script ever written so I’m sure this could all be done a bit better, but it works! I’m not going to go into detail about the SmartFTP api or how to write PowerShell stuff because this script will work with some basic requirements:

  • You need both PowerShell and SmartFTP installed
  • Currently this only supports the standard FTP protocol, but if you need SFTP, etc… you can just change the $fav variable’s ‘Protocol’ property
  • The parameters, in this order are:
    • destination
      • the IP address, or host of your FTP server
    • user
      • the username used to login to the FTP server
    • password
      • the password used to login to the FTP server
    • path
      • The FTP path of where you want your files to go on your FTP server
    • port
      • The FTP port to use, default is 21
    • source
      • The source folder to copy to the FTP site, if not specified, uses the current directory that the PowerShell script is run from

Example usage:

FTPSync.ps1 123.123.123.123 MyUserName MyPassword 21 “C:\MyWebsiteFolder” “/websites/MyWebsite”

or you can just double click on the ps1 file and it will prompt you for these details.

So without further adieu, here’s the script!

#requires -version 2.0 # Define inputs param ( [parameter(Mandatory=$true)] [string] $dest, [parameter(Mandatory=$true)] [string] $user, [parameter(Mandatory=$true)] [string] $pass, [parameter(Mandatory=$true)] [ValidatePattern('\d+')] [int] $port = 21, [parameter(Mandatory=$false)] [ValidateScript({ Test-Path -Path $_ -PathType Container })] [string] $source, [parameter(Mandatory=$true)] [ValidatePattern('\/+')] [string] $path ) # get current folder $currFolder = (Get-Location -PSProvider FileSystem).ProviderPath; # set current folder [Environment]::CurrentDirectory=$currFolder; # if the source isn't set, then use the current folder if ($source = "") { $source = $currFolder; } Write-Host "------------------------------------------------------" -foregroundcolor yellow -backgroundcolor black Write-Host("{0, -20}{1,20}" -f "Destination", $dest); Write-Host("{0, -20}{1,20}" -f "User", $user); Write-Host("{0, -20}{1,20}" -f "Pass", "********"); Write-Host("{0, -20}{1,20}" -f "Port", $port); Write-Host ""; Write-Host "Source:"; Write-Host $source; Write-Host ""; Write-Host "Path"; Write-Host $path; Write-Host "------------------------------------------------------" -foregroundcolor yellow -backgroundcolor black # Create application $smartFTP = New-Object -comObject SmartFTP.Application; $smartFTP.Visible = [bool]0; $smartFTP.CloseAll(); # create temp favorite item $fav = $smartFTP.CreateObject("sfFavorites.FavoriteItem"); $fav.Name = $user + " @ " + $dest + " (temp favorite by cmdInterface)"; # 1 = FTP standard protocol $fav.Protocol = 1; $fav.Host = $dest; $fav.Port = $port; $fav.Path = $path; $fav.Username = $user; $fav.Password = $pass; # forces it not to be saved $fav.Virtual = "true"; # Add temporary favorite to SmartFTPs FavoriteManager $favMgr = $smartFTP.FavoritesManager; $rootFolder = $favMgr.RootFolder; $rootFolder.AddItem($fav); # Get the transfer queue $queue = $smartFTP.TransferQueue; # stop the queue if it isn't already if ($queue.State -ne 1) { $queue.Stop(); } # Stopped = 1 # clear the queue foreach($item in $queue.Items) { $queue.RemoveItem($item); } # set the thread count for the queue $queue.MaxWorkers = 20; #enable logging $queue.Log = "true"; $queue.LogFolder = $currFolder + "\\LOG"; # create new transfer item $newItem = $smartFTP.CreateObject("sfTransferQueue.TransferQueueItem"); # set the item as a folder and copy operation, $newItem.type = 2; #FOLDER = 2 $newItem.Operation = 1; #COPY = 1 # Set the source $newItem.Source.type = 1; #LOCAL = 1 $newItem.Source.Path = $source; # Set the destination $newItem.Destination.type = 2; #REMOTE = 2 $newItem.Destination.Path = $path; $newItem.Destination.FavoriteIdAsString = $fav.IdAsString; #links up to our connection favorite # and finally add it $queue.AddItemTail($newItem); Write-Host "STARTING" -foregroundcolor yellow -backgroundcolor black; $queue.Start(); while ($queue.Items.Count -ne 0) { Write-Host "Processing...bytes transfered: " $queue.TransferredBytes; Start-Sleep -s 2; #wait 2 seconds } # store the total bytes $totalBytes = $queue.TransferredBytes; # cleanup smartftp app $queue.Quit(); $smartFTP.Exit(); # parse logs # regex to find "[DATE/TIME] STOR FILENAME # which indicates a file transfer $regex = new-object System.Text.RegularExpressions.Regex("\[[\w\-\:]*?\]\sSTOR\s(.+?)\[",,[System.Text.RegularExpressions.RegexOptions]::SingleLine); $totalFiles = 0; Write-Host "Files Transfered" -foregroundcolor cyan -backgroundcolor black Get-ChildItem $queue.LogFolder -include *.log -Recurse | foreach ($_) { $currFile = Get-Content $_.fullname; $match = $regex.Matches($currFile); if ($match.Count -gt 0) { foreach($m in $match) { Write-Host $m.Groups[1]; } $totalFiles++; } remove-item $_.fullname -Force -Recurse ; } Write-Host "COMPLETED (total bytes: " $totalBytes ", total files: )" $totalFiles -foregroundcolor cyan -backgroundcolor black; "------------------------------------------------------" # cleanup COM Remove-Variable smartFTP

Don’t deploy your .svn folders!

by Aaron Powell 18. January 2010 04:37

Last year there was an article posted on TechChrunch about the problem of deploying your .snv folder live, it’s a really great way to give everyone your websites source code!

Recently though while tweaking our 2 Click ASP.NET Web Application Deployment with MSBuild to include a static folder consisting of the umbraco/ umbraco_client folders (which we leave excluded from the project to ensure performance of Visual Studio) I noticed that we were including the .svn folders!

We’re generating an ItemGroup like this:

<ItemGroup>
  <Umbraco Include="$(LocationWorkingWeb)\umbraco\**\*.*"/>
  <UmbracoClient Include="$(LocationWorkingWeb)\umbraco_client\**\*.*"/>
</ItemGroup>

Which recursively adds the files from those folder, including .svn.

Balls!

Sure it’s not really a problem, we’ve got no source code stored in those folders (and anyone who is putting their own source in umbraco or umbraco_client is asking for trouble), but by including them you’re pretty much doubling the size of the folder structure too!

Luckily it’s quite easy to solve. MSBuild has a build-in Exclude attribute, so you just need to change it to look like this:

<ItemGroup>
  <Umbraco Include="$(LocationWorkingWeb)\umbraco\**\*.*" Exclude="$(LocationWorkingWeb)\umbraco\**\.svn\**\*" />
  <UmbracoClient Include="$(LocationWorkingWeb)\umbraco_client\**\*.*" Exclude="$(LocationWorkingWeb)\umbraco_client\**\.svn\**\*" />
</ItemGroup>

It looks a bit weird, you’ve got to recursively exclude the recursive contents of the .svn folder :P

It’s all about making sure you only deploy what you should have on a production server, and it goes in hand with remembering that PDB != Product Deployable Bits.

Categories: .Net | Umbraco

2 Click ASP.NET Web Application Deployment with MSBuild

by Shannon Deminick 2. October 2009 06:12

A couple of months ago Shannon wrote a post on how we do 2 Click Deployments at The Farm (if you haven’t read this post I suggest you read it first as I make the assumption it’s been read and skip over a few sections that it covers). I’ve been working with him to try and improve our process of deployment, and one of those tasks has been moving away from using NAnt as the build runner to using MSBuild.

Why the shift? Well MSBuild has actually come a long way recently and despite common belief it’s not just for building .NET applications. In fact it can be used the same way that NAnt can be used, to execute arbitrary operations.
As mentioned in the original post we’ve used an internal tool for modifying the web.config (and other configuration files) but it had a bit a limitation as it meant that the files were always full of settings for all environments. It also wasn’t designed to update a single property of an XML node, you’d need to replicate the whole XML structure, even if it was just 1 attribute changing.

So I suggested that we migrate to a XSLT based config management, which is what I’d used previously. A former colleague of mine has a good post on how to set that up, it can be found here.

Moving to MSBuild also brought in another advantage, it would simplify our deployment process. Currently we’re using CruiseControl.NET to execute a NAnt script which in turn executed MSBuild.

Another goal was to allow us to deploy from the branch in source control, rather than from the trunk, this way you can deploy different branches if you want to test different functionality based on a branch without overriding the trunk. So we’ve updated our source control structure to look like this:

  • trunk
  • branch
    • v1.1
  • build

The new addition is the build folder, this folder will generally only hold a single file, and that’s the master build information, which we’ll call cc.build. This build file is the one that CruiseControl.NET will look for and check out. It then contains the instructions to which branch to check out, and then handle the rest of the execution of the build. A sample looks like this:

<Project ToolsVersion="3.5" DefaultTargets="CheckoutAndBuild" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <Import Project="$(MSBuildExtensionsPath)\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets"/>

  <PropertyGroup>
    <BranchVersion>v1.1_net</BranchVersion>
    <BuildType>_net</BuildType>
    <BranchFolder>$(BaseFolder)\$(BuildType)</BranchFolder>
    <RepoPath>file:///\svn/Client/branches/$(BranchVersion)</RepoPath>
  </PropertyGroup>

  <Target Name="CheckoutAndBuild">
    <RemoveDir Directories="$(BranchFolder)" />

    <SvnCheckout RepositoryPath="$(RepoPath)"
					LocalPath="$(BranchFolder)"
					ToolPath="C:\Program Files\CollabNet Subversion">
      <Output TaskParameter="Revision" PropertyName="Revision" />
    </SvnCheckout>

    <Message Text="Revision: $(Revision)"/>

    <Time Format="yyyyMMddtt">
      <Output TaskParameter="FormattedTime" PropertyName="buildDate" />
    </Time>

    <MSBuild Projects="$(BranchFolder)\cc.msbuild"
             Properties="CCNetWorkingDirectory=$(BaseFolder);BuildDate=$(BuildDate);DeployType=Dev;DeployLocation=$(BaseFolder)..\Deployment\Dev;BuildConfiguration=Debug;ShowMessages=True;IncludeSymbols=True" />
  </Target>

</Project>

So lets have a look at the break down of this file. First off, we’re using the MSBuild Community Tasks for tasks such as the SVN checkout, and a few other aspects which we’ll cover later.

When we call this MSBuild file from CruiseControl.NET and pass in a parameter, BaseFolder, which is the Working folder (see the Folder Structure section of the original post).
There’s some parameters combined so we get a path to where we’ll be checking out the branch, etc.

First thing is to remove any existing checkout folder, and then have SVN checkout the latest copy of the branch.
As Shannon mentioned in the original post we timestamp each sip package, so for this we’re using the Community Task date formatter to generate our timestamp.

Then it gets a bit tricky, because this file is a dumb file and doesn’t know what’s going to be done by the branch deployment file so we need to have a way in which we can actually do a build. This is done by using the MSBuild task which allows you to execute any specified MSBuild file(s). This is done by passing the file to execute into the Projects attribute. Since we’re going to execute the one within the branch we tell it to look there.
Then it’s just a matter of passing any properties you require into the MSBuild file, here we’ve got a bunch of different parameters which the target file can consume.

The MSBuild task is then replicated as many times for each different environment/ location which you want to build for.

 

Each branch maintains its own msbuild file, which is also called cc.msbuild so that we can keep the naming consistent across all locations. This file is the one which is responsible for:

  • Compiling the project
  • Modifying the config files
  • Zipping the release

But this can easily be expanded upon, depending how the branch needs to be handled, there’s nothing that says it couldn’t also do the copying to the appropriate server, etc. Lets take a look inside this file.

<Project ToolsVersion="3.5" DefaultTargets="ZipFiles" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <Import Project="$(MSBuildExtensionsPath)\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets"/>

  <PropertyGroup>
    <LocationWorkingWeb>$(CCNetWorkingDirectory)\_net\Client.Web</LocationWorkingWeb>
    <ProjectWeb>$(LocationWorkingWeb)\Client.Web.csproj</ProjectWeb>
    <BuildFolder>$(DeployLocation)\$(BuildDate)\</BuildFolder>
  </PropertyGroup>

  <Target Name="ZipFiles" DependsOnTargets="FormatFiles">
    <ItemGroup>
      <ZipFiles Include="$(BuildFolder)**\*.*" Condition="$(IncludeSymbols) == 'True'" />
      <ZipFiles Include="$(BuildFolder)**\*.*" Exclude="$(BuildFolder)**\*.pdb" Condition="$(IncludeSymbols) == 'False'" />
    </ItemGroup>

    <Zip Files="@(ZipFiles)"
         ZipFileName="$(DeployLocation)\$(BuildDate).zip"
         WorkingDirectory="$(BuildFolder)"/>

    <RemoveDir Directories="$(BuildFolder)" />
  </Target>

  <Target Name="FormatFiles" DependsOnTargets="BuildWebProject">
    <MSBuild Projects="transformers.msbuild"
             Properties="XslFile=$(CCNetWorkingDirectory)\_net\CruiseControl\Web.config.xslt;OutputFile=$(BuildFolder)Web.config;InputFile=$(LocationWorkingWeb)\Web.config;Environment=$(DeployType)" />
  </Target>

  <Target Name="BuildWebProject">
    <MSBuild Projects="$(ProjectWeb)"
             Properties="Configuration=$(BuildConfiguration);OutDir=$(BuildFolder)bin\;WebProjectOutputDir=$(BuildFolder)"
             Targets="Clean;Build;ResolveReferences;_CopyWebApplication"/>
  </Target>
</Project>

This file, again uses the MSBuild Community Tasks, but its also a whole lot smarter about what’s happening for this particular build. I’ve specified that the DefaultTargets of the file is the ZipFiles which is really the final task which I want to execute.
One really nice thing about MSBuild is that you can specify a DependsOnTargets attribute for a target, which states that the particular target(s) must have executed before the called one will execute. So by looking at how the dependency is configured the project will have been compiled and the files formatted will both have been done. Makes it so much simpler to execute something, rather than having a ‘runner’ target which is just responsible for calling a bunch of targets in order we can just have them done in order of dependency.

So we’ve got a BuildWebProject target which will again call an external MSBuild file, in this case our csproj file, passing in some of the information which was given to us from the ‘master’ file, in a manner in which Visual Studio itself would have done.

Next we format the files. This goes off to an external MSBuild file, so we can use the same operations for every single file we want to format. The contents of transformers.msbuild is as follows:

<!-- ROBOTS IN DISGUISE -->
<Project ToolsVersion="3.5" DefaultTargets="Xslt" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <Import Project="$(MSBuildExtensionsPath)\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets"/>
  <ItemGroup>
    <XslFileWithParams Include="$(XslFile)">
      <environment>$(Environment)</environment>
    </XslFileWithParams>
  </ItemGroup>
  <Target Name="Xslt">
    <Xslt RootTag="" Inputs="$(InputFile)" Output="$(OutputFile)" Xsl="@(XslFileWithParams)" />
  </Target>
</Project>

This is a basic task file which adds a parameter (named envrionments) to our XSLT and then uses the MSBuild Community Tasks to do the transformation.
Note – this is a slight deviation from Alistair’s blog post, we don’t have a different XSLT per machine, we have a single XSLT which we use the parameter to determine what to do. This makes it easier to see all the options we want to change per environment.

Lastly the dependencies hierarchy throws us back to the ZipFiles target, where we gather all the files together that we want (and there’s an option to exclude the pdb files, after all PDB != Product Deployable Bits ;) ), call the Zip task (make sure you specify a working folder otherwise you’ll end up with a crazy hierarchy in the Zip!) and then we delete the folder which we built to.

 

Conclusion

MSBuild is a good tool which can be used for doing operations other than just compiling a .NET project, as you can see here we’re using it to pull files down out of SVN or transform a file via XSLT. This doesn’t have anything to do with compilation, or .NET for that matter.

It make take a bit of work to get set up, there was quit a bit of frustration vented during this process but now that it’s working there’s no looking back.

Categories: .Net | Hosting | Umbraco

2 Click ASP.Net Web Application Deployment

by Shannon Deminick 10. July 2009 13:17

At CodeGarden09 I had mentioned during one of the open space sessions that we had developed a 2 click deployment strategy for some of our projects and someone asked if I could share the solution, so here it goes!

The tools we use to do this are:

Get Zip File here: TheFARMDeployUtilities.zip (513.89 kb)

We generally have 3 different environments for our websites: Development, Staging, Live (Production). For 2 click deployments to work, we then need to have CruiseControl.Net server installed on each server environment. Your development machine or the machine that is performing the 2 click deploy should have access to all of the CruiseControl.Net servers so that each project can be added to your CCTray application. We lock down our CruiseControl.Net servers via firewall and running them on non-standard ports to only allowing traffic to them from our office environment.

This strategy is 2 steps because:

  • Gives you the control over when the project is built
  • Gives you control over when the built files are deployed live
  • Though it could be done in 1 step, I feel that it’s safer to ensure that the project is built properly before it goes live

This deployment process also takes into account:

  • A rollback process which ensures any build that is deployed can be rolled back to a previous build very quickly
  • The problem of multiple application restarts (or long application restarts) when uploading files via FTP since each DLL is essentially copied up one at a time, this process ensures that all built files are deployed to the environment without delay.

Basic Workflow

In order to understand the next steps its best to get a brief overview of what steps are involved when the deployment occurs:

  1. All code is committed to the repository (to a branch, or wherever the stable code should be) that you wish to deploy

Click 1

  1. A CruiseControl ‘Force Build’ is initiated for the project (normally via CCTray)
  2. CruiseControl checks out the latest version from Subversion and puts it in it’s working folder for the project
  3. CruiseControl runs a NANT task which:
    1. Creates a build date time stamp (i.e. 20090710AM)
    2. Runs an MSBuild process for each deployment environment (dev/stage/live) to build and deploy the web application. Depending on the environment, it will build in Release vs. Debug mode.
    3. Formats configuration files to include only the relevant settings for each environment
    4. Zips the builds for each deployment environment named with the build date time stamp
    5. FTPs the Zip file to a deploy folder on the deployment servers (stage/live)

Click 2

  1. A CruiseControl ‘ForceBuild’ is initiated on the environment’s server you wish to deploy to (normally via CCTray)
  2. CruiseControl runs a NANT task which:
    1. Unzips the latest build files into the staging/live folders for IIS.

CruiseControl Server Setup

The CruiseControl (build) server requires a few bits of software installed and configured:

The CruiseControl servers on staging/live environments only require that CruiseControl & NANT is installed.

Folder Structure

On the CruiseControl build server, we create this folder structure for each project:

  • Artifact
    • Debug
      • buildlogs (stores the CruiseControl build logs for the project)
  • Deployment
    • Dev (the folder for the Dev environment builds)
    • Live (the folder for the Live environment builds)
    • Staging (the folder for the Staging environment builds)
  • Working (where all of the source control files are checked out to)

On the staging/live servers we have a folder in the root of the website labelled: “_DEPLOY” which is where the deployment zip files get FTP’d to. We ensure that this folder doesn’t have read access so public people can’t go downloading your build files! In this _DEPLOY folder we have a deploy.build NANT task (see below).

Configuration File Formatting

Everyone seems to have their own way of managing different configuration settings for different environments and so do we! I like to be able to see all of the configuration settings for each environment in one file so that i know what’s being defined without looking through different files. To do this, our configuration settings look something like this:

<!--<DEV>-->
<add key="mySetting" value="DevEnvironmentSettingValue" />
<!--</DEV>-->
<!--<STAGING>
<add key="mySetting" value="StagingEnvironmentSettingValue" />
</STAGING>-->
<!--<LIVE>
<add key="mySetting" value="LiveEnvironmentSettingValue" />
</LIVE>-->

 

With the above design, development settings are always active when developing. When CruiseControl runs it’s build one of the NANT tasks that are run is The FARM’s configuration settings parser plugin which removes any configuration settings in config files that are not required for the current build environment.

We’ve also developed a handy little Visual Studio plugin to make formatting these configuration settings very easy. All that is required is highlighting a configuration element, clicking Tools –> The Farm Config –> Format for Dev (or staging/live)

CruiseControl.Net Setup

Deployment/Build Server

I’ve put documentation about each item directly in the XML:

<project name="MYPROJECT" category="WEB" queue="MYPROJECT" queuePriority="1" >
<!-- The working folder, this is where our repository files will be checked out to -->
<workingDirectory>D:\CruiseControl\Projects\MYPROJECT\Working</workingDirectory>
<!-- This is where our log files, and debug info will be saved -->
<artifactDirectory>D:\CruiseControl\Projects\MYPROJECT\Artifact\Debug</artifactDirectory>
<sourcecontrol type="svn">
<!-- We use a file based Subversion repository... here's an example of the location -->
<trunkUrl>file:///\myserver.mydomain.local/MYPROJECT/DEV_Repository/trunk/</trunkUrl>
<!-- Our Subversion install is here -->
<executable>C:\Program Files\CollabNet Subversion\svn.exe</executable>
<autoGetSource>True</autoGetSource>
</sourcecontrol>
<!-- We always do Force Builds, instead of automatic builds based on source control so our
server doesn't die when people commit multiple times for different projects, therefore
triggers is set to nothing -->
<triggers/>
<tasks>
<nant>
<!-- We use NANT to do all of our build. NANT in turn runs MSBuild and other
methods that we require such as FTP, etc... -->
<executable>C:\Program Files\Nant\bin\nant.exe</executable>
<baseDirectory>D:\CruiseControl\Projects\MYPROJECT\Working</baseDirectory>
<!-- This is the location of the NANT build file... it is in our working folder
which means that it is committed into the repository so that our devs are free
to modify it to suit their needs -->
<buildFile>D:\CruiseControl\Projects\MYPROJECT\Working\cc.build</buildFile>
<buildTimeoutSeconds>3600</buildTimeoutSeconds>
</nant>
</tasks>
<publishers>
<xmllogger />
<!-- This ensures that our log files don't build up too big -->
<artifactcleanup cleanUpMethod="KeepLastXBuilds" cleanUpValue="50" />
</publishers>
</project>

Staging/Live Servers

For each site, just define another project which simply runs a NANT script which basically just unzips the zip build file into the working IIS site folder (see below)

<project name="_STAGING_MYSITE">
<workingDirectory>E:\Inetpub\MYSITE</workingDirectory>
<artifactDirectory>E:\Inetpub\MYSITE\_DEPLOY\Debug</artifactDirectory>
<tasks>
<nant>
<executable>C:\Nant\bin\nant.exe</executable>
<baseDirectory>E:\Inetpub\MYSITE</baseDirectory>
<buildFile>E:\Inetpub\MYSITE\_DEPLOY\deploy.build</buildFile>
</nant>
</tasks>
<publishers>
<xmllogger />
<artifactcleanup cleanUpMethod="KeepLastXBuilds" cleanUpValue="50" />
</publishers>
</project>

NANT Build File

Deployment/Build Server

I’ve put documentation about each item directly in the XML:

<!-- Ensure NO namespace is declared as FTPTask for NANT 
has a bug when namespaces are declared -->
<project name="Site" default="DeployAll">

<!-- ** PROPERTY DEFINITIONS, These are the only things you should need to change -->

<!-- define the folder containing the web application project -->
<property name="location.working.web" value="${CCNetWorkingDirectory}\_net\MySite.Web"/>

<!-- define the web applicatoin project project file -->
<property name="project.web" value="${location.working.web}\MySite.Web.csproj"/>

<!-- define the deployment folders for each environment -->
<property name="location.deploy.dev" value="${CCNetWorkingDirectory}\..\Deployment\Dev"/>
<property name="location.deploy.staging" value="${CCNetWorkingDirectory}\..\Deployment\Staging"/>
<property name="location.deploy.live" value="${CCNetWorkingDirectory}\..\Deployment\Live"/>

<!-- define the MSBuild executable -->
<property name="exe.msconfig" value="C:\WINDOWS\Microsoft.NET\Framework\v3.5\MSBuild.exe"/>

<!-- define the ftp property connections for the stage/live environments -->
<property name="ftp.staging.server" value="123.123.123.123"/>
<property name="ftp.staging.user" value="admin"/>
<property name="ftp.staging.password" value="hello"/>
<property name="ftp.staging.path" value="MYSITE/FILES/_DEPLOY"/>
<property name="ftp.live.server" value="321.321.321.321"/>
<property name="ftp.live.user" value="admin"/>
<property name="ftp.live.password" value="hello"/>
<property name="ftp.live.path" value="MYSITE/FILES/_DEPLOY"/>

<!-- ** END PROPERTY DEFINITIONS -->

<!-- the primary build target which does the entire build -->
<target name="DeployAll" depends="SetBuildDate,Init,DevSite,StagingSite,LiveSite,CopyStaticFiles" />

<!-- sets some GLOBAL properties for use on all builds -->
<target name="Init" description="Sets the global working and project properties">
<property name="GLOBAL.location.working" value="${location.working.web}"/>
<property name="GLOBAL.project" value="${project.web}"/>
</target>

<!-- set a build date property to today's date with am/pm stored in ${build.date}-->
<target name="SetBuildDate" description="Creates a build date property for use in our deployment scripts">
<tstamp property="build.date" pattern="yyyyMMddtt" verbose="true" />
<echo message="Current build label: ${build.date}" level="Debug" />
</target>

<!-- Deploy Dev with Debug, this doesn't time stamp or zip the output
since it's a dev environment, the location should be set to your
IIS folder -->
<target name="DevSite" description="Compile Dev Web MySite.">
<property name="GLOBAL.location.deploy" value="${location.deploy.dev}"/>
<!-- build project in Debug mode -->
<property name="GLOBAL.project.config" value="Debug"/>
<property name="GLOBAL.deploy.type" value="DEV"/>

<!-- build the web application (see below) -->
<call target="BuildWebProject" />
</target>

<!-- Deploy Staging with Debug -->
<target name="StagingSite" description="Compile Staging Web MySite.">
<!-- build the project to the time stamped folder -->
<property name="GLOBAL.location.deploy" value="${location.deploy.staging}\${build.date}"/>
<!-- build project in Debug mode -->
<property name="GLOBAL.project.config" value="Debug"/>
<!-- define that the STAGING settings should be active on deploy -->
<property name="GLOBAL.deploy.type" value="STAGING"/>

<!-- build the web application (see below) -->
<call target="BuildWebProject" />
<!-- format the configuration files for the project to have the correct
environments settings active -->
<call target="FormatFileForDeploy" />
<!-- zip the build -->
<call target="ZipDeployFiles" />

<!-- transfer the build to the staging environment's deploy folder -->
<connection id="myconn" server="${ftp.staging.server}" username="${ftp.staging.user}" password="${ftp.staging.password}" />
<ftp connection="myconn" verbose="true" showdironconnect="true" remotedir="${ftp.staging.path}">
<put localdir="${location.deploy.staging}" type="bin">
<include name="${build.date}.zip" />
</put>
</ftp>

</target>

<!-- Deploy Live with Release -->
<target name="LiveSite" description="Compile Live Web MySite.">
<!-- build the project to the time stamped folder -->
<property name="GLOBAL.location.deploy" value="${location.deploy.live}\${build.date}"/>
<!-- build project in RELEASE mode -->
<property name="GLOBAL.project.config" value="Release"/>
<!-- define that the LIVE settings should be active on deploy -->
<property name="GLOBAL.deploy.type" value="LIVE"/>

<!-- build the web application (see below) -->
<call target="BuildWebProject" />
<!-- format the configuration files for the project to have the correct
environments settings active -->
<call target="FormatFileForDeploy" />
<!-- zip the build -->
<call target="ZipDeployFiles" />

<!-- transfer the build to the live environment's deploy folder -->
<connection id="myconn" server="${ftp.live.server}" username="${ftp.live.user}" password="${ftp.live.password}" />
<ftp connection="myconn" verbose="true" showdironconnect="true" remotedir="${ftp.live.path}">
<put localdir="${location.deploy.live}" type="bin">
<include name="${build.date}.zip" />
</put>
</ftp>
</target>

<!-- ** BUILD METHODS-->

<!-- Global method to build web applicatio projects using MSBuild -->
<target name="BuildWebProject">
<exec basedir="${GLOBAL.location.working}" program="${exe.msconfig}" workingdir="." failonerror="true">
<arg value="${GLOBAL.project}" />
<arg value="/nologo" />
<arg value="/t:Clean;Build" />
<arg value="/t:ResolveReferences;_CopyWebApplication"/>
<arg value="/p:OutDir=${GLOBAL.location.deploy}\bin\;Configuration=${GLOBAL.project.config}"/>
<arg value="/p:WebProjectOutputDir=${GLOBAL.location.deploy}"/>
</exec>
</target>

<!-- This formats any files that are included in our include property -->
<!-- to be in the correct format for dev,staging,live -->
<target name="FormatFileForDeploy">
<thefarm-process-config configtype="${GLOBAL.deploy.type}">
<fileset basedir="${GLOBAL.location.deploy}">
<include name="**/Regions.xml" />
<include name="**/EmailFormDefinitions.xml" />
<include name="**/*.config" />
</fileset>
</thefarm-process-config>
</target>

<!-- Zips all files in the current deployment folder -->
<target name="ZipDeployFiles">
<zip zipfile="${GLOBAL.location.deploy}.zip">
<fileset basedir="${GLOBAL.location.deploy}">
<include name="**/*" />
</fileset>
</zip>
<delete dir="${GLOBAL.location.deploy}" />
</target>

<!-- ** END BUILD METHODS-->

</project>

Staging/Live Servers

<project name="MyStagingSite" default="DoDeploy">

<target name="DoDeploy" depends="SetBuildDate,UnzipDeployFiles" />

<!-- set a build date property to today's date with am/pm-->
<target name="SetBuildDate">
<tstamp property="build.date" pattern="yyyyMMddtt" verbose="true" />
<echo message="Current build label: ${build.date}" level="Debug" />
</target>

<!-- unzip the build file into the IIS working folder -->
<target name="UnzipDeployFiles">
<unzip zipfile="${CCNetWorkingDirectory}\_DEPLOY\${build.date}.zip" todir="${CCNetWorkingDirectory}" />
</target>

</project>

Rollback

Since each build is contained in a date/time stamped ZIP file it is obviously fairly easy to rollback your site to a previous build, you would just have to know what date/time the last stable build was.

Conclusion

Though the above seems like a lot of work, it really isn’t since all the work is already done for you! All you really need to do once you have your CruiseControl servers setup is create your projects, modify the NANT build script properties for each project and that’s it.

This makes deployment extremely easy and safe since there’s no room for human error. All of the files will go exactly where they need to go. Developers can’t upload the wrong configuration files to the wrong servers or the wrong files to the wrong places. Another great benefit is that when the server unzips the build, the .Net application restarts quickly because it has all of the files to start compiling instantly.

Obviously to do this, you would need access to your staging and development servers, but even if you didn’t you could still get the first step done (which is the major step). The NANT scripts above are quite basic, we use them to build Sandcastle .Net documentation and all sorts of other things.

Always great to know how everyone else is doing the things described in this doc, so please let me know!

Categories: .Net | Hosting | Umbraco