Azure Devops CI/CD pipeline for ClickOnce applications (.Net Framework)

Azure Devops CI/CD pipeline for ClickOnce applications (.Net Framework)

Published on: 6 February 2022

Author: Ramesh Kanjinghat

When it comes to deploying ClickOnce applications, we have 3 options
  • Install from the Web or a Network Share.
  • Install from a CD.
  • Start the application from the Web or a Network Share.
To make it easy to understand I will use a simple visual studio solution that has a windows Forms project. So, let's start with the requirements

About the code

Requirements

  • 1: Deploy a .Net Framework 4.* windows application as ClickOnce so, that it can be installed from the Web or a Network Share. (First deployment strategy).
I have tested this with 4.* .Net Framework version. It might work with older versions but never tried.
In this blog I am trying to setup a deployment pipeline so, I make below assumptions

Assumptions/Prerequisites

Before we start on the yaml files we need to take care of couple of things

Step 1: Use the digital certificate to sign the ClickOnce application while deploying

When you publish as signed clickonce application the deployment agent should have access to the certificate. For security reasons it is not recommended to keep the certificate in the code repository. Instead, we can make the certificate available to the build agent by installing it in the server certification store.
  • Install the certificate on the server where the deployment agents run.
  • Either you can install it only for the account the pipeline agent run as or at machine level.
User specific is the secure option.

Step 2: Network share

  • Create a network share where we can deploy the ClickOnce application to.
  • Make sure the deployment agent user account has “write” permissions on the network share.

Step 3: Database migrations

To keep it simple I am assuming that database already exists and this pipeline just have to update database iteratively and I chose to use DbUp.
Please check https://dbup.readthedocs.io/en/latest/usage/ on different options to run DB migrations with DbUp.

Time to add pipeline files to the solution

I have added a solution folder, Pipelines, and added 4 yaml files.

DeployClickOnce.yml

  • As the name suggests this file has the steps to deploy our windows forms project as ClickOnce.
  • This is a pipeline template that know how to compile and deploy a windows application as ClickOnce. This lets us re-use the same template to deploy this with different configurations.
  • It conditionally runs database migrations.
1parameters:
2- name: buildConfiguration
3- name: publishUrl
4- name: environmentName
5- name: deployRootPath
6- name: solutionName
7- name: projectName
8- name: dbMigrations
9  type: object
10  default:
11  - name: runDbMigrations
12    default: false
13  - name: dbConnectionString
14    default: ''
15  - name: executableProjectName
16    default: ''
17# Above the it is required to set default value for the properties of dbMigrations. This way the caller can completely ignore passing values for dbMigrations in case of no migrations.
18
19stages:
20- stage: 'DeployClickOnce'
21  displayName: 'Deploy ClickOnce'
22  jobs:
23  - deployment: 'DeploayClickOnce'
24    displayName: 'Deploy ClickOnce'
25    variables:
26      solutionFolder: '$(Build.SourcesDirectory)\${{parameters.solutionName}}'
27    # Check https://docs.microsoft.com/en-us/azure/devops/pipelines/process/environments?view=azure-devops to learn about environments.
28    environment:
29      name: '${{parameters.environmentName}}'
30      resourceType: 'virtualMachine'
31      tags: 'Services'
32    strategy:
33      runOnce:
34        deploy:
35          steps:
36          - checkout: self
37
38          # Restore packages.
39          - task: NuGetCommand@2
40            displayName: 'Restore Packages'
41            inputs:
42              command: restore
43              restoreSolution: '**/*.sln'
44              feedsToUse: config
45              nugetConfigPath: '$(solutionFolder)\nuget.config'
46
47          # Publish windows forms projects as ClickOnce.
48          - task: PowerShell@2
49            displayName: 'Build and publish'
50            inputs:
51              targetType: 'inline'
52              script: |
53                Set-Location '$(solutionFolder)\${{parameters.projectName}}'
54                & 'C:\Program Files (x86)\Microsoft Visual Studio\2019\BuildTools\MSBuild\Current\Bin\MSBuild.exe' /target:publish /p:Configuration=${{parameters.buildConfiguration}} /p:PublishUrl='${{parameters.publishUrl}}'
55          
56          # Generate Publich.htm file. This is the file that end user access to install the ClickOnce. This file is generated automatically if we publish from Visual Studio.
57          - task: PowerShell@2
58            displayName: 'Generate Publish.htm'
59            inputs:
60              targetType: 'inline'
61              script: |
62                [xml]$projectFile = Get-Content '$(solutionFolder)\$${{parameters.projectName}}\${{parameters.projectName}}.vbproj'
63                [string]$version = $projectFile.Project.PropertyGroup.MinimumRequiredVersion
64                $publishHtm = Get-Content '$(solutionFolder)\publish.htm' -Raw
65                $updatePublishHtm = $publishHtm -replace '{VERSION}', $version.Trim()
66                $updatePublishHtm | Set-Content -Path '$(solutionFolder)\${{parameters.projectName}}\bin\${{parameters.buildConfiguration}}\app.publish\publish.htm'
67
68          # Copy the published application to the network share.
69          - task: CopyFiles@2
70            displayName: 'Copy files'
71            inputs:
72              sourceFolder: '$(solutionFolder)\${{parameters.projectName}}\bin\${{parameters.buildConfiguration}}\app.publish'
73              contents: '**'
74              targetFolder: '${{parameters.deployRootPath}}'
75              cleanTargetFolder: false
76              overWrite: true
77
78          # Execute the DBMigrations executable to apply the migrations, if any.
79          - ${{if eq(parameters.dbMigrations.runDbMigrations, true)}}:
80          - task: PowerShell@2
81            displayName: 'Run database migrations'
82            inputs:
83              targetType: 'inline'
84              script: |
85                $command = '${{parameters.solutionFolder}}\${{parameters.executableProjectName}}\bin\${{parameters.buildConfiguration}}\${{parameters.executableProjectName}}.exe ${{parameters.dbConnectionString}}'
86                & $command

CIPipeline.yml

This is the pipeline that will be triggered when you merge code into your ci branch.
1name: $(BuildDefinitionName)-PR.$(Build.SourceBranch)$(Rev:.r)
2
3# Triggers this pipeline when a new merge to ci branch.
4trigger:
5- ci
6   
7stages:
8
9# Use the template and pass ci environment configurations.
10- template: DeployClickOnce.yml
11  parameters:
12    buildConfiguration: 'Debug'
13    publishUrl: 'The URL the end users use to install the ClickOnce application '
14    environmentName: 'CI'
15    deployRootPath: 'The network share where the windows forms project will be deployed as ClickOnce'
16    dbMigrations: 
17     runDbMigrations: true
18     dbConnectionString: 'your database connection string here'
19     executableProjectName: 'Dhrutara.ClickOnce.DBMigrations'

MasterPipeline.yml

This is the pipeline that will be triggered when you merge code into your master branch.
1name: $(BuildDefinitionName)-PR.$(Build.SourceBranch)$(Rev:.r)
2
3# Triggers this pipeline when a new merge to master branch.
4trigger:
5- master
6
7stages:
8
9- template: DeployClickOnce.yml
10  parameters:
11    buildConfiguration: 'Release'
12    publishUrl: 'The URL the end users use to install the ClickOnce application '
13    environmentName: 'Production'
14    deployRootPath: 'The network share where the windows forms project will be deployed as ClickOnce'
15    dbMigrations: 
16     runDbMigrations: true
17     dbConnectionString: 'your database connection string here'
18     executableProjectName: 'Dhrutara.ClickOnce.DBMigrations'

Setup ci and master pipelines in Azure Devops

This is a very simple process, please follow the steps in the documentation, https://docs.microsoft.com/en-us/azure/devops/pipelines/customize-pipeline?view=azure-devops#pipeline-settings.
After the pipelines are setup for ci and master branch push code into ci and master branches to trigger these pipelines.
If you don’t have any DB migrations then just remove below lines.
1dbMigrations: 
2     runDbMigrations: true
3     dbConnectionString: 'your database connection string here'
4     executableProjectName: 'Dhrutara.ClickOnce.DBMigrations'
By the way
  • If you copy paste the yaml content then there might be some format issues. Both visual studio and visual studio code IDEs can validate the syntax and highlight errors.
  • You might need to tweak the configurations, parameter values and project paths based on your project and infrastructure setup.