DevOps: Productivity Boost with Azure Devops Templates (DevOps 2019)

Rob
6 min readJan 27, 2023

--

With CI/CD templates in Azure DevOps, recurring build and release tasks can be created and reused once so that a pipeline doesn’t have to be created and maintained over and over again. This has the advantage that the code is changed in a central location and thus the effort is significantly reduced with increasing pipelines and projects.

To whom the article is addressed

The article is addressed to every developer, IT project manager and infrastructure manager who uses DevOps 2019 and has multiple source code management repositories (GIT, TFS) and projects with the same SDK (.NET 4.8, .NET 6, Angular 11) and would like to significantly reduce the effort required to create new pipelines.

I cannot recommend to use these templates for newer versions of DevOps.

About Azure Devops CI/CD

Azure DevOps is a cloud solution from Mircrosoft that is used by development teams to achieve the following requirements:

1. Managing source code in an audit-proof manner so that code is not lost and can be recovered.
2. Build and test source code to ensure code quality.
3. Delivering applications to customers and distributing development resources to developers in the form of NUGET packages.

How to Setup the DevOps template

To create a template yourself, the following steps must be performed:
1. Create a repository for the templates (here: TemplateRepo)
2. Add a YAML file to the repository (template.yml)
(3). Copy one of the following examples for the template. They are based on .NET 6 and .NET 4.8. If you are using an other target you will need to define different task and build them for your own purposes.

Template .NET 6

parameters:
buildConfiguration: 'Release'
runtime: 'win-x64'
createNugetPackage: false
pool: 'DEV'
testProjects: '**/Tests/*.csproj'
projects: '**\*.csproj;!**\*.Tests.csproj;!**\*.Test.csproj'
solution: '**/.sln'
testFilterCriteria: 'TestCategory!=Manual'
selfContained: true

jobs:
- job: Build
pool: ${{ parameters.pool }}
steps:

# Validate Parameters
- task: PowerShell@2
displayName: 'Validate Parameter'
inputs:
targetType: 'inline'
script: |
$buildConfiguration = '${{ parameters.buildConfiguration }}'
if(($buildConfiguration -eq '')
{
echo '##vso[task.logissue type=error;]Missing parameter buildConfiguration'
}

# Install SDK
- task: UseDotNet@2
displayName: 'Install SDK'
condition: 'succeeded()'
inputs:
packageType: 'sdk'
version: '6.x'

# Build Application
- task: DotNetCoreCLI@2
displayName: 'Build Application'
condition: 'succeeded()'
inputs:
command: build
projects: ${{ parameters.solution }}
arguments: '--configuration ${{ parameters.buildConfiguration }}'

# Test Application
- task: DotNetCoreCLI@2
displayName: 'Test Application'
condition: 'succeeded()'
inputs:
command: test
projects: ${{ parameters.testprojects }}
arguments: '--configuration ${{ parameters.buildConfiguration }} --filter ${{ parameters.testFilterCriteria }} --collect "Code coverage"'

# Prepare publish artifacts
- task: DotNetCoreCLI@2
displayName: 'Prepare publish artifacts'
condition: 'succeeded()'
inputs:
command: publish
publishWebProjects: false #Note: Change, if required
zipAfterPublish: false #Note: Change, if required
projects: {{ parameters.projects }}
arguments: '--configuration ${{ parameters.buildConfiguration }} --output ${{ Build.ArtifactStagingDirectory }}\Binaries --runtime ${{ parameters.runtime }} --self contained ${{ parameters.selfContained }}'

# Provide Parameter for Release Pipeline
- task: PowerShell@2
displayName: 'Provide parameter for release pipeline'
condition: 'succeeded()'
inputs:
targetType: inline
script: |
$variable = @{ Values = @{
PublishNuget = '${{ parameters.createNugetPackage }}'
}} $variable | ConvertTo-Json | Out-File $(Build.ArtifactStagingDirectory\parameters.txt
Get-Content $(Build.ArtifactStagingDirectory)\parameters.txt

# Package Nuget
- task: PowerShell@2
displayName: 'Create Nuget Package'
condition: 'and(succeeded(), eq( ${{ parameters.createNugetPackage }}, true))'
inputs:
command: pack
packagesToPack: ${{ parameters.projects }}

# Publish Artifacts
- task: PublishBuildArtifacts@1
displayName: 'Publish Artifacts'
condition: 'succeeded()'
inputs:
pathToPublish: $(Build.ArtifactStagingDirectory)
artifactName: drop

# Delete all files of build directory
- task: DeleteFiles@1
displayName: 'Delete Binaries'
condition: 'succeeded()'
inputs:
SourceFolder: '$(Build.BinariesDirectory)'
Contents: '**/*'

# Delete all files of source directory
- task: DeleteFiles@1
displayName: 'Delete Sources'
condition: 'succeeded()'
inputs:
SourceFolder: '$(Build.SourcesDirectory)'
Contents: |
**/*
.gitignore

Template .NET 4.8

parameters:
buildConfiguration: 'Release'
buildPlatform: 'any cpu'
runtime: 'win-x64'
createNugetPackage: false
pool: 'DEV'
testProjects: '**/Tests/*.csproj'
projects: '**\*.csproj;!**\*.Tests.csproj;!**\*.Test.csproj'
solution: '**/.sln'
testFilterCriteria: 'TestCategory!=Manual'

jobs:
- job: Build
pool: ${{ parameters.pool }}
steps:

# Validate Parameters
- task: PowerShell@2
displayName: 'Validate Parameter'
inputs:
targetType: 'inline'
script: |
$buildConfiguration = '${{ parameters.buildConfiguration }}'
if(($buildConfiguration -eq '')
{
echo '##vso[task.logissue type=error;]Missing parameter buildConfiguration'
}

# Install SDK
- task: UseDotNet@2
displayName: 'Install SDK'
condition: 'succeeded()'
inputs:
packageType: 'sdk'
version: '5.x'

# Restore NuGet
- task: NuGetCommand@2
displayName: 'Restore NuGet'
condition: 'succeeded()'
inputs:
command: restore
restoreSolution: '{{ parameters.solution }}'
feedsToUse: 'select'
vstsFeed: '/34234234-23d3-...' #TODO: Set your NuGet feed here
includeNugetOrg: true
noCache: true

# Build Application
- task: MSBuild@1
displayName: 'Build Application'
condition: 'succeeded()'
inputs:
solution: '{{ parameters.solution }}'
configuration: '${{ parameters.buildConfiguration }}'
clean: true
msbuildArguments: '--output=$(Build.BinariesDirectory)'

# Test Application
- task: VSTest@2
displayName: 'Test Application'
condition: 'succeeded()'
inputs:
testSelector: 'testAssemblies' #NOTE: Check the filter criteria
testAssemblyVer2: |
**\*test*.dll
**\*Test.dll
!**\*TestAdapter.dll
!**\obj\**
searchFolder: '$(System.DefaultWorkingDirectory)'
testFiltercriteria: '${{ parameters.testFilterCriteria }}'
codeCoverageEnabled: true #NOTE: change if you don't want to use code coverage
testRunTitle: 'Tests'
platform: '${{ parameters.buildPlatform }}'
configuration: '${{ parameters.buildConfiguration }}'

# Prepare publish artifacts
- task: VSTest@2
displayName: 'Prepare publish artifacts'
condition: 'succeeded()'
inputs:
SourceFolder: '$(Build.BinariesDirectory)' #NOTE: A filter on .xml files is included for excluding debug informations
Contents: |
**
!*.xml
!*.pdb
!*Test*
TargetFolder: '$Build.ArtifactStagingDirectory)\Binaries'

# Provide Parameter for Release Pipeline
- task: PowerShell@2
displayName: 'Provide parameter for release pipeline'
condition: 'succeeded()'
inputs:
targetType: inline
script: |
$variable = @{ Values = @{
PublishNuget = '${{ parameters.createNugetPackage }}'
}} $variable | ConvertTo-Json | Out-File $(Build.ArtifactStagingDirectory\parameters.txt
Get-Content $(Build.ArtifactStagingDirectory)\parameters.txt

# Package Nuget
- task: NuGetCommand@2
displayName: 'Create Nuget Package'
condition: 'and(succeeded(), eq( ${{ parameters.createNugetPackage }}, true))'
inputs:
command: pack
versionScheme: 'off'
includeReferenceProjects: true
packagesToPack: ${{ parameters.projects }}
configuartion: ${{ parameters.buildConfiguration }}

# Publish Artifacts
- task: PublishBuildArtifacts@1
displayName: 'Publish Artifacts'
condition: 'succeeded()'
inputs:
pathToPublish: $(Build.ArtifactStagingDirectory)
artifactName: drop

# Delete all files of build directory
- task: DeleteFiles@1
displayName: 'Delete Binaries'
condition: 'succeeded()'
inputs:
SourceFolder: '$(Build.BinariesDirectory)'
Contents: '**/*'

# Delete all files of source directory
- task: DeleteFiles@1
displayName: 'Delete Sources'
condition: 'succeeded()'
inputs:
SourceFolder: '$(Build.SourcesDirectory)'
Contents: |
**/*
.gitignore

4. Set the pipeline as a template
Menu: Pipeline / Builds / 3 Dots Button / Set as template

How to use the Azure Devops Template

The templates are now located within a specific branch (dev, master, …) under which the .yml file was created in the repository. This must be taken into account when executing the pipeline afterwards, as execution under a different branch will not work if the .yml file was not created there.

To use a template for a project, the following steps must be performed:

  1. Create a .yml file in the repository of the project where you want to use the template.
  2. Copy the following content into the newly created .yml file
trigger:
- master
- dev

resources:
repositories:
- repository: templates
type: git
name: ProjetCollection/TemplateRepo
jobs:
- template: net60.yml@templates
parameters:
createNugetPackage: true
pool: 'MyAgentPool'

Parameter Description

Each parameter has a default value, so the template could work without setting the parameters if the agent pool name equals DEV. If the name of your pool differs, you need at least set the name of the agent pool.

buildConfiguration
- Descr.: The build configuration of the application
- Values: Release, Debug

runtime:
- Descr.: The runtime of the application
- Values: any cpu, x64, Win32, x86

createNugetPackage:
- Descr.: Should the pipeline create a new NuGet package
- Values: true, false

pool:
- Descr.: Set your particular application pool. You can find you pool in the project settings menu, agent pools
- Values: Default, …, …

testProjects:
- Descr.: The projects which should be tested.
- Values: AppTests.csproj, …, …

projects:
- Descr.: The projects which binaries should be deployed.
- Values: App.csproj, …, …

solution:
- Descr.: The solution to build.
- Values: App.csproj, …, …

testFilterCriteria:
- Descr.: Set FilterCriteria for your tests so that some tests would be skipped by the pipeline
- Values: TestCategory != Manual, …

selfContained (.NET 6):
- Descr.: Should the application deployed as selfContained, so the application can run on it’s own without configuring the host in advance
- Values: true, false

Release Pipeline — Parameter from Build Pipeline

In DevOps 2019, there are release pipelines that can be used to deploy artifacts.
To use the artifacts of the build pipeline, we need to reference the artifacts and read the file with the parameters.

- powershell : |
$parameters = Get-Content -Path $(System.ArtifactsDirectory)\**\**parameters.txt -Raw | ConvertFrom-Json
$publishNuget = $parameters.Values.PublishNuget
Write-Host "##vso[task.setvariable variable=PublishNuget]$publishNuget

After that we can use the parameter from the build pipeline like an ordinary release variable: $(PublishNuget)

Additional Notes

Some parts of the template may vary from your requirements and environment, so it will be neccessary to change them. They are marked with TODO and NOTE.

Feel free to ask! There are many parts not covered here in the article, cause you can do a lot of stuff with Azure DevOps. Hence, don’t hesitate to comment or contact me via LinkedIn for further support.

--

--

Rob
Rob

Written by Rob

Unleash .NET - Providing simple dev solutions to enhance .NET productivity. Custom support: linkedin.com/in/robert-heckel

Responses (2)