Auto ClickOnce Deploys with TeamCity + MSBuild
Currently when you go to install MahTweets, you’re greeted with this less than reassuring dialog:

Unknown publisher? Well, you should know it’s from the MahTweets team, but you can’t be certain – the next beta of MahTweets will be signed properly so a known publisher will appear (or you can be sneaky and install our nightlies auto-generated by TeamCity+MSBuild, which is what this article is all about!). Once done, you’ll see a dialog more like this:

First step is getting the verified publisher parts working. For that, I highly recommend Jeff Wilcox’s guide to Getting Started with code signing for under (USD)$100. Jeff’s guide is great, until you come up to the issue of wanting to automate the whole build process.
TeamCity
Next is TeamCity (TC) from JetBrains. If you’re new to the game, TC is one of many Continuous Integration (CI) servers, this one happens to have a free/limited version, which is perfect for small open source projects or for a personal CI. Being Java, TC runs on any platform.
MSBuild
This is the only “hard” part – partially because Mage is frustrating but mostly because MSBuild is frustrating. It was a matter of a lot of trial and error. To make things a little easier, all the build files are in a subdirectory of the root of the project creatively titled BuildOutput. Inside that there are the subfolders for the three main stages – Application (compiled output), Install (ClickOnce output) and Zip (the zipped ClickOnce output).
You can download the latest version of the MahTweets msbuild script from SVN or Fisheye
Most build scripts have a few different stages to them, and ours is no different. For us, the flow is something like
- Cleanup – make sure all the required build directories exist (and you could have it delete the contents first)
- Version – since it is automated builds, we make sure the AssemblyInfo contains the Subversion revision number associated with this build
- Compile – actually make the project to publish
- Publish – this is the difficult/frustrating section, and I’ll only really go in depth about Publish.
- Our particular Publish process deletes all the duplicated files, cleans up any PDB’s and XMLs that were compiled, and removes the .manifest and .application generated.
- Next is the first stage of the ClickOnce files – creating the MahTweets.exe.manifest from the files created in Compile using the devil known as Mage.
- After creating the manifest, the rest of the files are copied over into the appropriate directory (Install/Application Files/MahTweets_X_Y_Z_R/ and then their subfolders, where X is MajorVersionNumber, Y is Minor, Z is Build and R is revision) – this could be performed before or after, makes no real difference.A key thing to note here is all files are appended with “.deploy” at the end – if you’re serving up your ClickOnce files from non-IIS servers, this isn’t needed, but IIS won’t serve up files ending with DLL or Config.
- While mage could be used for the next stage, all reports are that GenerateDeploymentManifest (part of MSBuild) is far more suited to the job. This creates the MahTweets.application file – the file that launches the ClickOnce install process.
- Mage is called once more “update” the MahTweets.application file, but in reality all it does is sign it
- Package – to be honest, for us at the moment, the package section is unnecessary, but it is nice to have around. Packaging in this case just zips up the ClickOnce install to a single compressed file.
Eventually, we could turn this into packaging up the files to do a portable build, but we’re not there just yet.
MSBuild: Publish
As I mentioned, the meat of this post is is to do with the Publish section – see the links in the above section to get our full MSBuild file if its of interest.
Looking in more detail at the section below
- The ItemGroup then Delete cleans up the files generated in Compile that are duplicated – most projects shouldn’t need this, but we do some funny things around plugins
- The next interesting command is the Exec calling mage – this creates the .exe.manifest file from the directory, and then signs the manifest.
- Next we copy all the files (using that cryptic Copy command) and rename them to append .deploy – as mentioned above, this is related to IIS
- Next GenerateDeploymentManifest creates the .application file – this is an XML file that ClickOnce gets all the details about the install from.
- And finally mage signs the .application file
This general process should work for all, but there are a few flags to be aware of
- As we needed MahTweets to be a 32bit app (because of some dependencies) –Processor x86 is required in the initial mage calls
- In GenerateDeploymentManifest’s parameters
- MapFileExtensions=”true” lines up with renaming files to .deploy
- Platform=”x86″ sets the processor correctly
- TargetFrameworkMoniker=”.NETFramework,Version=v4.0,Profile=Client” is used because we’re targeting the .NET 4 Client Profile for increased compatibility.
<Target Name="Publish" DependsOnTargets="Compile">
<!-- remove unnecessary plugins - dependencies which should be already loaded into appdomain -->
<ItemGroup>
<FilesToDelete Include="$(OutputFolderApplication)**\*.pdb" />
<FilesToDelete Include="$(OutputFolderApplication)**\*.xml" />
<FilesToDelete Include="$(OutputFolderPlugins)MahTweets.Core.*" />
<FilesToDelete Include="$(OutputFolderPlugins)MahTweets.Library.*" />
<FilesToDelete Include="$(OutputFolderPlugins)MahTweets.UI.*" />
<FilesToDelete Include="$(OutputFolderPlugins)Chrome.dll" />
<FilesToDelete Include="$(OutputFolderPlugins)GMap.*"/>
<FilesToDelete Include="$(OutputFolderPlugins)IronPython*"/>
<FilesToDelete Include="$(OutputFolderPlugins)Microsoft*"/>
<FilesToDelete Include="$(OutputFolderPlugins)StructureMap.dll"/>
<FilesToDelete Include="$(OutputFolderPlugins)Newtonsoft.Json.dll"/>
<FilesToDelete Include="$(OutputFolderPlugins)System.Data.SQLite.dll"/>
<FilesToDelete Include="$(OutputFolderPlugins)Windows7.SensorAndLocation.dll"/>
<FilesToDelete Include="$(OutputFolderApplication)MahTweets.exe.manifest" />
<FilesToDelete Include="$(OutputFolderApplication)MahTweets.application" />
</ItemGroup>
<Delete Files="@(FilesToDelete)" />
<PropertyGroup>
<CurrentVersion>$(OutputInstallPath)Application Files\MahTweets_$(Major)_$(Minor)_$(Build)_$(Revision)\</CurrentVersion>
</PropertyGroup>
<MakeDir Directories="$(OutputInstallPath)" Condition="!Exists('$(OutputInstallPath)')" />
<MakeDir Directories="$(OutputInstallPath)Application%20Files" Condition="!Exists('$(OutputInstallPath)Application Files')" />
<MakeDir Directories="$(CurrentVersion)" Condition="!Exists('$(CurrentVersion)')" />
<Copy SourceFiles="$(MSBuildCommunityTasksPath)setup.exe" DestinationFolder="$(OutputInstallPath)" OverwriteReadOnlyFiles="true" />
<Copy SourceFiles="$(MSBuildCommunityTasksPath)publish.htm" DestinationFolder="$(OutputInstallPath)" OverwriteReadOnlyFiles="true" />
<Message Text="Generating Application Manifest" />
<Exec Command='mage.exe -New Application -Processor x86 -ToFile "$(CurrentVersion)MahTweets.exe.manifest" -name MahTweets -Version $(Major).$(Minor).$(Build).$(Revision) -FromDirectory $(OutputFolderApplication) -ch $(CertHash) -IconFile mahtweetsicon.ico' />
<ItemGroup>
<NewApplicationFiles Include="$(OutputFolderApplication)**\*.*" />
</ItemGroup>
<Copy SourceFiles="@(NewApplicationFiles)" DestinationFiles="@(NewApplicationFiles->'$(CurrentVersion)\%(RecursiveDir)%(Filename)%(Extension).deploy')"/>
<Message Text="Generating Deployment Manifest" />
<ItemGroup>
<EntryPoint />
</ItemGroup>
<CreateItem Include='BuildOutput\Install\Application%20Files\MahTweets_$(Major)_$(Minor)_$(Build)_$(Revision)\MahTweets.exe.manifest' AdditionalMetadata='TargetPath=Application%20Files\MahTweets_$(Major)_$(Minor)_$(Build)_$(Revision)\MahTweets.exe.manifest'>
<Output TaskParameter="Include" ItemName="EntryPoint"/>
</CreateItem>
<Message Text="EntryPoint specified at '$(EntryPoint)'" />
<GenerateDeploymentManifest AssemblyName="MahTweets.exe"
AssemblyVersion="$(Major).$(Minor).$(Build).$(Revision)"
DeploymentUrl="$(DeployToUrl)"
Description="MahTweets - an open-source Twitter client"
EntryPoint="@(EntryPoint)"
Install="true"
OutputManifest="$(OutputInstallPath)\MahTweets.application"
Product="MahTweets"
Publisher="MahApps"
SupportUrl="$(SupportUrl)"
MapFileExtensions="true"
UpdateEnabled="true"
UpdateMode="Foreground"
Platform="x86"
TargetFrameworkMoniker=".NETFramework,Version=v4.0,Profile=Client">
<Output ItemName="DeployManifest" TaskParameter="OutputManifest"/>
</GenerateDeploymentManifest>
<Message Text="Deployment Manifest stored to '@(DeploymentManifest)'" />
<Exec Command='mage.exe -Update $(OutputInstallPath)\MahTweets.application -Publisher MahApps -ch $(CertHash)'/>
</Target>
Thumbprint vs file+password
You may have noticed that I’m using “CertHash” instead of the usual CertFile (and pointing to a PFX) + CertPass. The advantage with this is that its easy to take this key and distribute it so it builds on any and all machines, so long as they can get a copy of the key file. The disadvantage is, your “secret” password is visible to anybody who has access to your build server.
However, if you install the certificate on your desired machines, you can use the ‘thumbprint’ which is a hash/unique identifier to that key. Since the thumbprint is unique to that key – it’s the same value on all machines, so install the key onto the machines you want (and enter the password during installation) and nobody you don’t want having access to that password will ever be able to see it.
The downside to this is that it could still be abused – if somebody had access to TC with enough rights to create a new project or somebody committing to one of your source control repos, by just knowing that thumbprint, the could trick your machine into signing their own code under your key.
Making sure your build agent has access to your key
By default, TC Build Agents run under the LocalSystem account (on Windows) and when you install a certificate (by double clicking on it) it only installs under that account. Thankfully, Laurent Kempé figured out an easy way to install the certificate under the LocalSystem account.
You will need a tool from SysInternals called PsExec, then using PsExec:
> Psexec.exe -i -s cmd.exe
you will have ran a command prompt on your system in the Local System Account (LSA).
Using that new command prompt, cd to the folder containing your certificate and start it
> my_TemporaryKey.pfx
Then you will face the Certificate Import Wizard in which you click Next > Next > Type the password > Next.
Your certificate is installed now for Local System Account (LSA), and your build should work now.
Not using ClickOnce? No worries – sign the executable(s) instead
signtool.exe sign /f PathToKeysAndCert.Pfx /p “MySuperSecretPasswordToUseThePfxFile” /v “C:\MyFileToSign.exe”,

[...] This post was mentioned on Twitter by Paul Jenkins, Bugra Ayan. Bugra Ayan said: Auto ClickOnce Deploys with TeamCity + MSBuild: Currently when you go to install MahTweets, you’re greeted with th… http://bit.ly/aSoBCd [...]