Oct
27
2009

Build ClickOnce deployment packages using MSBuild and Team City

The other day I was requested to automate our build process to issue different ClickOnce setup for the same application. The main difference was some configuration files pointing to different back end web services.

To start I had to create new build configurations on Team City which used the following settings for the Build Runner:

  1. Targets: Rebuild Publish
  2. Configuration: One per build configurations; e.g DeployClickOnce, integrationDeployClickOnce

Targets and Configuration

Then in my Visual Studio 2008 solution I created several Solution configuration reflecting the different configurations that I needed during my deployment, e.g. DeployClickOnce

Visual Studio 2008 solutions

Then using the project properties from the solution explorer in Visual Studio I had to set all Publish options I was interested in; Publish Location, Installation Folder Url, Install Mode and Settings, Prerequisites…

4048988539_2a9f77285f_o[1]

The issue now is that the Publish Version automatically increment the revision with each publish. But this doesn’t work with our continuous integration server Team City as it would need to checkin the modified file back to subversion. SO a different approach was needed.

The solution I used is to use the Build Number offered by Team City, so I had to modify the MSBuild script to use the the BUILD_NUMBER. To do that, right click in the Solution Explorer on our project and select Edit Project File:

4049748852_2ea06972ca_o[1]

Then you will face the your MSBuild script, and you will have to search for the configuration that we defined some steps before:

  1. <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'DeployClickOnce|AnyCPU' ">

Then before the end of the closing PropertyGroupd tag add the following lines:

  1.   <!-- ClickOnce getting build number from Team City -->
  2.   <ApplicationRevision>$(BUILD_NUMBER)</ApplicationRevision>
  3.   <InstallUrl>http://myserver.com/</InstallUrl>
  4. </PropertyGroup>

ApplicationRevision will overwrite the Application revision using the BUILD_NUMBER defined by Team City.

InstallUrl is another configuration that we want to override because we want to create multiple ClickOnce setup installed from different urls. So for DeployClickOnce you will have an InstallURL and for integrationDeployClickOnce you will have another one.

Now we are ready for the final step in which we want to exchange some configuration files related to the different ClickOnce builds and Publish the output to an IIS server so that our testers can access the different ClickOnce package for the different stages.

ClickOnce secure the different files that are created with checksums so that they cannot be mitigated during the installment transfer. So the only option we have to be able to exchange our configuration files is before compilation. So we had a Target to our MSBuild script:

  1. <Target Name="BeforeCompile">
  2.   <CallTarget Targets="ExchangeDefaultSettings" ContinueOnError="false" />
  3.   <CallTarget Targets="ExchangeAppConfig" ContinueOnError="false" />
  4. </Target>

The BeforeCompile target will be called before each build and will exchange our App.config and another settings file containing stage dependant configuration.

Here is the simple target which exchanges the App.config stored in a configs folder:

  1. <Target Name="ExchangeAppConfig">
  2.   <Message Text="####### CONFIG Exchange $(Configuration)|$(Platform)  ---------#" />
  3.   <Copy Condition=" '$(Configuration)' == 'DeployClickOnce' "
  4.         SourceFiles="$(SolutionFolder)\Sources\Application\configs\localhost.App.config"
  5.         DestinationFiles="$(SolutionFolder)\Sources\Application\App.config"
  6.         ContinueOnError="false" />
  7.   <Copy Condition=" '$(Configuration)' == 'integrationDeployClickOnce' "
  8.         SourceFiles="$(SolutionFolder)\Sources\Application\configs\integration.App.config"
  9.         DestinationFiles="$(SolutionFolder)\Sources\Application\App.config"
  10.         ContinueOnError="false" />
  11. </Target>

The ExchangeDefaultSettings Target works the same.

So before any compilation of our solution using the DeployClickOnce, integrationDeployClickOnce solution configuration the App.config and the default settings file are exchanged. So after the compilation they will be correct according to the stage that we target.

The final step is to Publish the ClickOnce package created to the IIS server. As we defined the Targets: Rebuild Publish, there will be a Rebuild and then a Publish phase in our build script. So now we have to take care of the Publish target.

So we add a Target Publish as here:

  1. <Target Name="Publish">
  2.   <CallTarget Condition=" '$(Configuration)' == 'DeployClickOnce' "
  3.               Targets="DeployClickOnce"
  4.               ContinueOnError="false" />
  5.   <CallTarget Condition=" '$(Configuration)' == 'integrationDeployClickOnce' "
  6.               Targets="DeployClickOnce"
  7.               ContinueOnError="false" />
  8. </Target>

Which just call a Target DeployClickOnce for the configuration we are interested in: DeployClickOnce and integrationDeployClickOnce.

The DeployClickOnce Target is responsible to xcopy the packages created by the Publish Target to the different IIS path used to host our ClickOnce deployment setup:

  1. <!-- Deploy Click Once-->
  2. <Target Name="DeployClickOnce">
  3.   <Message Text="####### Deploy ClickOnce $(Configuration)|$(Platform)  ---------#" />
  4.   <Exec Command="xcopy /E /Y $(ClickOnceSrc)\*.* $(ClickOnceDestination)" />
  5. </Target>

This is achieved by using two variables ClickOnceSrc and ClickOnceDestination which are also defined per solution configuration like the ApplicationUrl and InstallUrl. The destination is a folder on a IIS server which already has a manually customized Publish.htm file.

  1. <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'DeployClickOnce|AnyCPU' ">
  2.   <ClickOnceSrc>$(TestsFolder)\Output\$(OutputPath)app.publish</ClickOnceSrc>
  3.   <ClickOnceDestination>E:\Inetpub\Application</ClickOnceDestination>

And

  1. <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'integrationDeployClickOnce|AnyCPU' ">
  2.   <ClickOnceSrc>$(TestsFolder)\Output\$(OutputPath)app.publish</ClickOnceSrc>
  3.   <ClickOnceDestination>E:\Inetpub\Application\integration</ClickOnceDestination>

Now you have two build configurations which output two valid ClickOnce setup packages using different stage dependant configurations that your tester can install directly from your ClickOnce web site. And if you have configured the automatic update of the application through the ClickOnce Application Updates then the applciation will be updated automatically when a tester start you application.

Enjoy!

Add comment

  Country flag

biuquote
  • Comment
  • Preview
Loading

About Laurent

Laurent Kempé

Laurent Kempé is the editor, founder, and primary contributor of Tech Head Brothers, a French portal about Microsoft .NET technologies.

He is currently employed by Innoveo Solutions since 10/2007 as a Senior Solution Architect, certified Scrum Master and Founding Member.

Founder, owner and Managing Partner of Jobping, which provides a unique and efficient platform for connecting Microsoft skilled job seekers with employers using Microsoft technologies.

Laurent was awarded Most Valuable Professional (MVP) by Microsoft from April 2002 to April 2012.

JetBrains Academy Member
Certified ScrumMaster
My status

Twitter

Flickr

www.flickr.com
This is a Flickr badge showing public photos and videos from Laurent Kempé. Make your own badge here.

Month List

Page List