I’m a strong fan of Azure DevOps templates for pipelines because it is a really good feature to both simplify Pipeline authoring and avoid proliferation of too many way to do the same things. In some of my previous examples I’ve always used a template that contains full Multi Stage pipeline definition , this allows you to create a new pipeline with easy, reference repository with the template, choose right template, set parameters and you are ready to go.
Using a template file that contains all stages allows you to define in a single place an entire pipeline to reuse with simple parameters definition
Today my dear friend Giulio Vian told me that he was investigating the use of a cool feature of.NET core 3.0, called Local Tools . Basically with Local Tools you are able to create a special file called dotnet-tools.json that contains all the tools you need for your project. Since I’m an heavy fan of GitVersion, it seems to me standard to include in every project such file with the actual version of GitVersion used in my project. Here is an example of the file.
1
2
3
4
5
6
7
8
9
10
11
12
| {
"version": 1,
"isRoot": true,
"tools": {
"gitversion.tool": {
"version": "5.2.4",
"commands": [
"dotnet-gitversion"
]
}
}
}
|
Once you have this file in place, you can simply issue a dotnet tool restore command and all referenced tools will be automatically installed locally and ready to use. This makes me extra simple to use GitVersion in my pipelines, because a simple dotnet tool restore will make GitVersion available on my pipeline (given that I’ve previously created a dotnet-tools.json in my project).
To experiment this new feature I want to give you another approach in using Azure DevOps templates, shared steps. Lets have a look at this template file:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
| parameters:
buildName: 'Specify name'
dotNetCoreVersion: '2.2.301'
steps:
- task: DotNetCoreInstaller@2
displayName: 'Use.NET Core sdk ${{parameters.dotNetCoreVersion}}'
inputs:
version: ${{parameters.dotNetCoreVersion}}
- task: DotNetCoreCLI@2
displayName: 'install if needed dotnet gitversion tool'
inputs:
command: 'custom'
custom: 'tool'
arguments: 'restore'
- script: |
dotnet tool run dotnet-gitversion $(Build.Repository.LocalPath) /output buildserver
name: Run_dotnet_gitversion
- powershell: |
Write-Host "##vso[build.updatebuildnumber]${{parameters.buildName}}-$env:GITVERSION_FULLSEMVER"
$var = (gci env:*).GetEnumerator() | Sort-Object Name
$out = ""
Foreach ($v in $var) {$out = $out + "`t{0,-28} = {1,-28}`n" -f $v.Name, $v.Value}
write-output "dump variables on $env:BUILD_ARTIFACTSTAGINGDIRECTORY\test.md"
$fileName = "$env:BUILD_ARTIFACTSTAGINGDIRECTORY\test.md"
set-content $fileName $out
write-output "##vso[task.addattachment type=Distributedtask.Core.Summary;name=Environment Variables;]$fileName"
name: Update_build_number
- powershell: |
echo "[task.setvariable variable=GITVERSION_ASSEMBLYSEMVER;isOutput=true]$(GITVERSION.ASSEMBLYSEMVER)"
echo "[task.setvariable variable=GITVERSION_ASSEMBLYSEMFILEVER;isOutput=true]$(GITVERSION.ASSEMBLYSEMFILEVER)"
echo "[task.setvariable variable=GITVERSION_SHA;isOutput=true]$(GITVERSION.SHA)"
echo "[task.setvariable variable=GITVERSION_FULLSEMVER;isOutput=true]$(GITVERSION.FULLSEMVER)"
echo "##vso[task.setvariable variable=GITVERSION_ASSEMBLYSEMVER;isOutput=true]$(GITVERSION.ASSEMBLYSEMVER)"
echo "##vso[task.setvariable variable=GITVERSION_ASSEMBLYSEMFILEVER;isOutput=true]$(GITVERSION.ASSEMBLYSEMFILEVER)"
echo "##vso[task.setvariable variable=GITVERSION_SHA;isOutput=true]$(GITVERSION.SHA)"
echo "##vso[task.setvariable variable=GITVERSION_FULLSEMVER;isOutput=true]$(GITVERSION.FULLSEMVER)"
name: 'SetGitVersionVariables'
|
It is a quite long template but basically it begins with usual parameters section, followed by a series of steps, not an entire multi stage definition. The sequence of steps are used to: runs dotnet tool restore, run GitVersion and finally does some PowerShell dumping of the variables. This kind of template was more similar to a Task Group because it is basically just a sequence of steps with parameters.
With this approach you are defining small pieces of an entire pipeline, allowing for a more granular reuse. This other template contains steps to build and run tests for a solution in.NET core.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
| parameters:
dotNetCoreVersion: '3.1.201'
buildConfiguration: release
solution: ''
continueOnTestErrors: true
GitVersionFullSemVer: ''
GitVersionAssemblyVer: ''
GitVersionAssemblySemFileVer: ''
SkipInstallDotNetCore: false
steps:
- task: DotNetCoreInstaller@2
displayName: 'Use.NET Core sdk ${{parameters.dotNetCoreVersion}}'
inputs:
version: ${{parameters.dotNetCoreVersion}}
condition: ne(${{parameters.SkipInstallDotNetCore}}, 'true')
- task: DotNetCoreCLI@2
displayName: 'dotnet restore'
inputs:
command: restore
projects: '${{parameters.solution}}'
feedsToUse: config
nugetConfigPath: src/NuGet.Config
- task: DotNetCoreCLI@2
displayName: 'dotnet build'
inputs:
command: build
projects: '${{parameters.solution}}'
configuration: '${{parameters.buildConfiguration}}'
arguments: /p:AssemblyVersion=${{parameters.GitVersionAssemblyVer}} /p:FileVersion=${{parameters.GitVersionAssemblySemFileVer}} /p:InformationalVersion=${{parameters.GitVersionAssemblyVer}}_$(Build.SourceVersion)
- task: DotNetCoreCLI@2
displayName: 'dotnet test'
inputs:
command: test
nobuild: true
projects: '${{parameters.solution}}'
arguments: '--configuration ${{parameters.buildConfiguration}} --collect "Code coverage" --logger trx'
continueOnError: ${{parameters.continueOnTestErrors}}
|
This steps declare as parameters some of semVer numbers extracted by GitVersion, so this sequence of steps are based on the fact that you should have run GitVersion in some preceding steps. Finally I’ve the last template that is used to publish with NuGet.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| parameters:
dotNetCoreVersion: '3.1.201'
buildConfiguration: release
nugetProject: ''
GitVersionFullSemVer: ''
GitVersionAssemblyVer: ''
GitVersionAssemblySemFileVer: ''
SkipInstallDotNetCore: false
steps:
- task: DotNetCoreInstaller@2
displayName: 'Use.NET Core sdk ${{parameters.dotNetCoreVersion}}'
inputs:
version: ${{parameters.dotNetCoreVersion}}
condition: ne(${{parameters.SkipInstallDotNetCore}}, 'true')
- task: DotNetCoreCLI@2
displayName: NuGet Pack
inputs:
command: custom
custom: pack
projects: ${{parameters.nugetProject}}
arguments: -o "$(Build.ArtifactStagingDirectory)\NuGet" -c ${{parameters.buildConfiguration}} /p:PackageVersion=${{parameters.GitVersionFullSemVer}} /p:AssemblyVersion=${{parameters.GitVersionAssemblyVer}} /p:FileVersion=${{parameters.GitVersionAssemblySemFileVer}} /p:InformationalVersion=${{parameters.GitVersionAssemblyVer}}_$(Build.SourceVersion)
- task: NuGetCommand@2
displayName: NuGet Push
inputs:
command: push
packagesToPush: '$(Build.ArtifactStagingDirectory)\NuGet\*.nupkg'
nuGetFeedType: internal
publishVstsFeed: '95a01998-aa90-433c-8077-41da981289aa'
continueOnError: true
|
Now that I have these three distinct template files in a repository of my Azure DevOps account I can refer them to pipelines in another repository.
Here is the real pipeline file of final project that is actually using all those three steps template defined in another repository.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
| trigger:
branches:
include:
- master
- develop
- release/*
- hotfix/*
- feature/*
resources:
repositories:
- repository: templatesRepository
type: git
name: Jarvis/BuildScripts
ref: refs/heads/develop
jobs:
- job: net_build_test
pool:
name: '$(Pool)'
demands:
- vstest
- msbuild
- visualstudio
steps:
- template: 'steps/GitVersionDotnetCoreLocal.yml@templatesRepository' # Template reference
parameters:
dotNetCoreVersion: '3.1.201'
buildName: 'License Manager'
- template: 'steps/BuildAndTestCore.yml@templatesRepository' # Template reference
parameters:
dotNetCoreVersion: '3.1.201'
solution: 'src/LicenseManager.sln'
SkipInstallDotNetCore: true
GitVersionFullSemVer: '$(GITVERSION.FULLSEMVER)'
GitVersionAssemblyVer: '$(GITVERSION.ASSEMBLYSEMVER)'
GitVersionAssemblySemFileVer: '$(GITVERSION.ASSEMBLYSEMFILEVER)'
- template: 'steps/PublishNuget.yml@templatesRepository' # Template reference
parameters:
dotNetCoreVersion: '3.1.201'
nugetProject: 'src/LicenseManager.Client/LicenseManager.Client.csproj'
GitVersionFullSemVer: '$(GITVERSION.FULLSEMVER)'
GitVersionAssemblyVer: '$(GITVERSION.ASSEMBLYSEMVER)'
GitVersionAssemblySemFileVer: '$(GITVERSION.ASSEMBLYSEMFILEVER)'
SkipInstallDotNetCore: true
|
As you can see, with steps template I decide on final build how many stage I need, in this example I’m perfectly confortable with a single stage. The cool part of this approach is that I can mix standard steps and steps template files, giving me more flexibility in how the pipeline is constructed. Clearly this pipeline is more complex than one that use a full template file, because we need to pass parameter to every template. Running the pipeline gives you a standard run.
Figure 1: Steps are expanded during execution.
As you can verify from Figure 1 all steps templates are expanded in basic steps, allowing you to verify the output of every single step. Thanks to local tool feature I can simply run dotnet tool restore to have GitVersion automatically installed
Figure 2: Restoring tooling with dotnet tool restore automatically restore gitversion
This will greatly simplify agent requirements, because all requirements are automatically restored by the pipeline.
Gian Maria.