DevOps: Productivity Boost with Azure Devops Templates (DevOps 2019)
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:
- Create a .yml file in the repository of the project where you want to use the template.
- 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.