ClickOnce in a Continuous Integration Scenario
Publishing a ClickOnce-based application in Visual Studio is very easy, as opposed to the automated deployment. But it's still tricky and, if you want to do it, you can either use a command-line tools like mage.exe or you can have MSBuild to do the work for you. As we are the masters of MSBuild now, we will use this build tool.
You can generate the publication files using MSBuild by using the Publish target on the solution file, like this:
msbuild CiDotNet.sln /t:Publish
There are several problems with this approach:
- The version number is not incremented.
- The publication files are created in the bin\$(Configuration)\app.publish folder and not on the destination location.
- The web site HTML file is not generated
Let's deal with these issues one at the time. The version number could be set from outside by providing the ApplicationVersion property on the MSBuild command line:
Msbuild CiDotNet.sln /t:Publish /p:ApplicationVersion=22.214.171.124
But how do we get the version number on the build server? Visual Studio takes it from the ApplicationRevision property inside the .csproj file and it will not be a good idea to mess with it on the server. But how about combining the ApplicationVersion number with the revision number?
First, make sure the MSBuild community tasks have been copied into the tools directory. Next, get the svn.exe and copy it to svn subdirectory in the tools folder. We'll use them both, as shown in Listing 1.
<Project DefaultTargets="Publish" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <UsingTask AssemblyFile="tools\MSBuildCommunityTasks\MSBuild.Community.Tasks.dll" TaskName="MSBuild.Community.Tasks.Subversion.SvnInfo"></UsingTask> <PropertyGroup> <ApplicationVersion Condition=" '$(ApplicationVersion)' == '' "> <b>1</b> 1.0.0. </ApplicationVersion> <b>1</b> <RevisionNumber Condition=" '$(RevisionNumber)' == '' "> <b>2</b> 0 2 </RevisionNumber> <b>2</b> </PropertyGroup> <b>2</b> <Target Name="Publish"> <SvnInfo RepositoryPath="http://ci1/svn/CiDotNetCh10/trunk" Username="user" Password="password" ToolPath="tools\svn"> <Output TaskParameter="Revision" PropertyName="RevisionNumber" /> <b>3</b> </SvnInfo> <MSBuild Targets="Publish" Projects="CiDotNet.sln" <b>4</b> ContinueOnError="false" Properties= <b>4</b> "ApplicationVersion=$(ApplicationVersion)$(RevisionNumber)"/> <b>4</b> </Target> </Project> <b> 1 Defines the immutable version part 2 Defines the mutable version part 3 Gets the version number from Subversion 4 Generates publish files </b>
After importing the SvnInfo MSBuild Community task we define the immutable #1 and mutable #2 version number parts. In the Publish target, we get the RevisionNumber #3 using the SvnInfo task and apply it to the ApplicationVersion property #4. Problem one, setting the version number, is now solved.
The second problem is that the files are not copied to the target location. The ClickOnce files are created in bin\$(Configuration)\app.publish folder. They need to be copied to the web server. The easiest way is to define the target location like:
Then gather the source files:
<ItemGroup> <DeploymentSourceFiles Include="CiDotNet.WinCalc\bin\$(Configuration)\app.publish\**\*.*" /> </ItemGroup>
After the publishing files are created they can be copied:
<Copy SourceFiles="@(DeploymentSourceFiles)" DestinationFiles="@(DeploymentSourceFiles->' [CA]$(DeploymentFolder)\%(RecursiveDir)%(Filename)%(Extension)')"/>
The last problem is the lack of the HTML website. There is no elegant way to deal with that. The best solution is to take the created index.html Web site and create a kind of template with it. Let's copy the index.html file to the solution project folder and change the current version to a string stored in the ApplicationVersion variable. We will use the FileUpdate task from the MSBuild Community Tasks. Check out Listing 2 for the file update usage.
<Copy SourceFiles="index.htm" DestinationFiles="$(DeploymentFolder)\index.htm"/> <Copy SourceFiles="wincalc.png" <b>1</b> DestinationFiles="$(DeploymentFolder)\wincalc.png"/> <b>1</b> <FileUpdate Files="$(DeploymentFolder)\index.htm" IgnoreCase="true" Multiline="true" Singleline="false" Regex="ApplicationVersion" ReplacementText="$(ApplicationVersion)$(RevisionNumber)"/> <b>2</b> <b> 1 Copy template files 2 Update version number </b>
The use of the HTML template gives you one more opportunity to customize the ClickOnce website to suit your needs. For example, you can provide additional information to the user. First, we take the custom-made HTML "template" file together with an application screenshot and copy it to the destination folder #1. Afterward we apply the FileUpdate task, search for the ApplicationVersion string, and replace it with the version number #2. Don't forget to import the FileUpdate task from MSBuild Community Tasks. Your ClickOnce Web site could look like that in Figure 4.
You can take this build script and set it to make your Continuous Integration server use it every time something changes in the repository. You will get a brand new application deployed every time something changes in the repository.
Automating delivery and deployment and incorporating it into the Continuous Integration process is a very natural thing to do. It feels quite right to take the compiled, tested, analyzed and documented code that looks like a delicious sweet candy and wrap it up with colorful wrapping.
The delivery and deployment scenarios depend on your individual needs. You can, for example, provision some Virtual PC setups just like the physical ones. You might want to add the automatically generated documentation to the package. You may also need to take into account laws, such as Sarbanes/Oxley (SOX) in the United States, which prohibit development from touching QA or production servers. In this case, you can use agents on QA and production servers to get the latest build. Finally, you might want to create some safety net functionality in your build script to redo the changes if something goes wrong.