Wiki

Clone wiki

ClickOnceAndOctopusDeploy / Home

Status

The sample is committed to bb and works :)

Why

ClickOnceAndOctopusDeploy is an example application for using the ClickOnce technology with Octopus Deploy. One of the challenges with ClickOnce, is how to do configuration and signing. With this approach we create and configure (and sign) the ClickOnce package when installing to the production environment, leaving us with the possibility to use octopus variables, app.config transformations and powershell to configure the application to the corresponding environment.

We've not been able to find much information regarding this in the octopus community and figured that others could benefit from our POC. The ClickOnce packaging snippet is from blog post by Sam Ritchie

What

This repository contains a solution with 2 projects and a psake build script.

Web application

Standard web application with some static content. Uses OctoPack to package as a octopus NuGet package The ClickOnce package will be hosted on this site.

WPF application

Standard WPF application, but includes various executables (mage.exe, mageui.exe, nuget.exe), code signing certificate and octopus deployment scripts. Note that this project does not use OctoPack, but rather lets the build script package this octopus NuGet package. The reason for this, is to alter folder structure a little to simplify the ClickOnce generation when installing via octopus.

Psake build script

The build script is in powershell and uses psake to structure the build activities. It compiles the 2 projects and creates the WPF octopus NuGet package (ideally push to nuget feed, create an octopus release).

Prerequisites

To build (build.ps1) this sample, you'll need Visual Studio 2012. To install the octopus package, either using Octopus Deploy or manually (BuildAndInstall.ps1), you'll need to install the root certificate from build\tools\certificates\MyCA.cer on the 'production' machine (Note this is of course not needed, when using a signing certificate which is issued from a thrusted root authority). No password.

ClickOnce and Octopus Steps

  1. Compile project. The web project automatically creates a octopus NuGet package, since we are using OctoPack and a custom msbuild property.
  2. Take application binaries and move to sub folder - e.g. ClickOnce binaries. Leave octopus deployment dependencies in a folder at root level - e.g. executables, certificates, octopus deploy scripts etc. The folder structure could look like this * NugetPackage/Output dir ClickOnceArticates: application files deployment: Octopus deployment files. Includes script to create the ClickOnce package and deploy to web server
  3. Create a NuGet package from the altered output directory
  4. Push NuGet packages to nuget feed (or expose in TC)
  5. Use octo.exe to create an octopus release (consisting of both the web and WPF application) and e.g. deploy to Staging
  6. When deploying to staging, Octopus will run the deployment scripts in the WPF package on the production machine and generate the ClickOnce package.
  7. Copy the ClickOnce package to the preinstalled website folder.

Creating ClickOnce package

When installing the WPF application with Octopus, we use the hookin scripts provided - predeploy.ps1, deploy.ps1, postdeploy.ps1 and rollback.ps1. The following is ClickOnce generation script and helper method.

deploy.ps1

#!powershell

Import-Module .\deployment\clickOnceLib.psm1 -verbose

# Properties
$baseDir = resolve-path .\
$mageExe = '.\deployment\mage.exe'
$codeSigningCertificate = "$baseDir\deployment\MySPC.pfx"
$clickOnceApplicationName = "WpfApplication1"
$ClickOnceArtifactDir = "$baseDir\ClickOnceArtifacts"
$ClickOnceOutputDir = "$baseDir\ClickOnce"

$version = Get-AssemblyVersion "$ClickOnceArtifactDir\$clickOnceApplicationName.exe"

#Do your custom configuration here...

Write-Host "Packaging new ClickOnce version $version"
$files = Get-ChildItem -Path $ClickOnceArtifactDir | foreach {$_.fullname}
Create-ClickOncePackage $files -App_Name $clickOnceApplicationName -Version $version -Output_Dir "$ClickOnceOutputDir" -Cert $codeSigningCertificate -Deployment_Url "http://example.com/MyApp" -MageExe $mageExe

clickoncelib.psm1

#!powershell

# Create a ClickOnce package, based on program binaries.
# From http://samritchie.net/2012/03/28/create-net-4-clickonce-applications-from-the-command-line/
function Create-ClickOncePackage {
    param($app_name, $version, $output_dir, $cert, $deployment_url, $mageExe) 
    $files = $args[0]

    $version_dir = "$output_dir\$app_name_$($version.Replace(".", "_"))"
    mkdir $version_dir
    $relative_version_dir = [System.IO.Path]::GetFileName($version_dir)

    #Copy files into the output folder and generate the .manifest and .application files.
    Copy-Item $files -Destination $version_dir
    & $mageExe -New Application -ToFile "$version_dir\$app_name.exe.manifest" -Name $app_name -Version $version -Processor msil -CertFile "$cert" -FromDirectory $version_dir -TrustLevel FullTrust
    & $mageExe -New Deployment -ToFile "$output_dir\$app_name.application" -Name $app_name -Version $version -Processor msil -AppManifest "$version_dir\$app_name.exe.manifest" -AppCodeBase "$deployment_url/$relative_version_dir/$app_name.exe.manifest" -CertFile $cert -IncludeProviderURL true -ProviderURL "$deployment_url/$app_name.application" -Install true

    #Append .deploy to files for web server deployment, then re-sign the manifest. No idea why mage can't do this.
    Get-ChildItem $version_dir | Foreach-Object { if (-not $_.FullName.EndsWith(".manifest")) { Rename-Item $_.FullName "$($_.FullName).deploy" } } 
    & $mageExe -Sign "$version_dir\$app_name.exe.manifest" -CertFile $cert

    #Set parameters in deployment xml, then re-sign the deployment. Why can't we do this from the command line, Microsoft?
    $xml = [xml](Get-Content "$output_dir\$app_name.application")
    $deployment_node = $xml.SelectSingleNode("//*[local-name() = 'deployment']")
    $deployment_node.SetAttribute("minimumRequiredVersion", $version)
    $deployment_node.SetAttribute("mapFileExtensions", "true")
    $xml.Save("$output_dir\$app_name.application")
    & $mageExe -Sign "$output_dir\$app_name.application" -CertFile $cert
}

function Get-AssemblyVersion{
    param($file)
    return [System.Diagnostics.FileVersionInfo]::GetVersionInfo($file).FileVersion
}

Export-ModuleMember -function * -alias *
Write-Host 'Imported clickOnceLib.psm1'

Updated