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
- We have a .Net framework solution, Dhrutara.ClickOnce, with 1 project, Dhrutara.WindowsForms.
- You can find the code at https://github.com/Dhrutara/blogs.dhrutara.com.blogs/tree/main/Dhrutara.ClickOnce.
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.
- 3: Run database migrations.
- 4: ClickOnce application is digitally signed. (https://docs.microsoft.com/en-us/visualstudio/deployment/clickonce-and-authenticode?view=vs-2022)
- 5: Trigger a build validation pipeline when pull requests are created on ci and master branches.
- 6: Trigger a deployment pipeline when code merged into CI or master branches.
In this blog I am trying to setup a deployment pipeline so, I make below assumptions
Assumptions/Prerequisites
- Your code is in Azure Devops Git repository.
- Your pipelines run by self-hosted agents. https://docs.microsoft.com/en-us/azure/devops/pipelines/agents/agents?view=azure-devops&tabs=browser#install.
- The windows application is digitally signed.
- You have network share where the windows application will be deployed as ClickOnce.
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.