In this post, we are going to refactor our sample Azure DevOps Pipeline to move some of the redundant YAML to a new file and replace the redundant parts of our main YAML file. This post is going to build on the Azure DevOps project created in previous posts. If you are just joining this series check out the previous posts to find out how the project has progressed.
Getting Started with Azure DevOps
Pipeline Creation in Azure DevOps
Azure DevOps Publish Artifacts for ASP.NET Core
Azure DevOps Pipelines: Multiple Jobs in YAML
Starting YAML
The following is the YAML for our current pipeline that builds two different web applications using two different jobs. Looking at the two jobs you will notice that they both have the same steps. The only difference in the steps is which project to build (WebApp1.csproj or WebApp2.csproj) and what to call the published artifact (WebApp1 or WebApp2). When developing applications we would never stand for this level of duplication and the same should apply to our pipelines.
trigger: none variables: buildConfiguration: 'Release' jobs: - job: WebApp1 displayName: 'Build WebApp1' pool: vmImage: 'ubuntu-latest' steps: - task: UseDotNet@2 displayName: 'Use .NET 3.1.x' inputs: packageType: 'sdk' version: '3.1.x' - task: DotNetCoreCLI@2 displayName: 'Build' inputs: command: 'build' projects: '**/WebApp1.csproj' arguments: '--configuration $(buildConfiguration)' - task: DotNetCoreCLI@2 displayName: 'Publish Application' inputs: command: 'publish' publishWebProjects: false projects: '**/WebApp1.csproj' arguments: '--configuration $(buildConfiguration) --output $(Build.ArtifactStagingDirectory)' - task: PublishPipelineArtifact@1 displayName: 'Publish Artifacts' inputs: targetPath: '$(Build.ArtifactStagingDirectory)' artifact: 'WebApp1' publishLocation: 'pipeline' - job: WebApp2 displayName: 'Build WebApp2' pool: vmImage: 'ubuntu-latest' steps: - task: UseDotNet@2 displayName: 'Use .NET 3.1.x' inputs: packageType: 'sdk' version: '3.1.x' - task: DotNetCoreCLI@2 displayName: 'Build' inputs: command: 'build' projects: '**/WebApp2.csproj' arguments: '--configuration $(buildConfiguration)' - task: DotNetCoreCLI@2 displayName: 'Publish Application' inputs: command: 'publish' publishWebProjects: false projects: '**/WebApp2.csproj' arguments: '--configuration $(buildConfiguration) --output $(Build.ArtifactStagingDirectory)' - task: PublishPipelineArtifact@1 displayName: 'Publish Artifacts' inputs: targetPath: '$(Build.ArtifactStagingDirectory)' artifact: 'WebApp2' publishLocation: 'pipeline'
Add a New File
To attack the duplication above we need to take the shared steps from above and move them somewhere they can be reused. We will be walking through the steps using the Azure DevOps web site and committing directly to the master branch, but these same steps could be performed locally or on the web on any branch. First, from the Repos section of the site we need to add a new file by clicking the three dots at the level we want the file added. In this case, we are adding to the root of the repo but the same option is available on any folder.
A dialog will show where you can enter the New file name, we are going to use build.yml in this case. Next, click Create to continue.
Shared YAML
Now that we have a new file we can start building the new YAML that will handle the repeated steps from the original jobs. The first thing we are going to do is define a set of parameters that this set of steps can be called with. We are going to use this to pass what project to build, which build configuration to use, and what name the published artifact. The following is the definition of our parameters.
parameters: - name: buildConfiguration type: string default: 'Release' - name: project type: string default: '' - name: artifactName type: string default: ''
We can then use these parameters in the rest of the file using the ${{ parameterName }} syntax. Note that any pipeline variables are also available using the $(variableName) syntax. The following bit of YAML shows both types in the arguments line.
- task: DotNetCoreCLI@2 displayName: 'Publish Application' inputs: command: 'publish' publishWebProjects: false projects: '**/${{ parameters.project }}' arguments: '--configuration ${{ parameters.buildConfiguration }} --output $(Build.ArtifactStagingDirectory)'
While you can use pipeline variables I recommend passing all the values you need via parameters for the same reason that we try to avoid global variables when doing general programming. I’m using both here to show the usage of each. The following is the full YAML in our new file.
parameters: - name: buildConfiguration type: string default: 'Release' - name: project type: string default: '' - name: artifactName type: string default: '' steps: - task: UseDotNet@2 displayName: 'Use .NET 3.1.x' inputs: packageType: 'sdk' version: '3.1.x' - task: DotNetCoreCLI@2 displayName: 'Build' inputs: command: 'build' projects: '**/${{ parameters.project }}' arguments: '--configuration ${{ parameters.buildConfiguration }}' - task: DotNetCoreCLI@2 displayName: 'Publish Application' inputs: command: 'publish' publishWebProjects: false projects: '**/${{ parameters.project }}' arguments: '--configuration ${{ parameters.buildConfiguration }} --output $(Build.ArtifactStagingDirectory)' - task: PublishPipelineArtifact@1 displayName: 'Publish Artifacts' inputs: targetPath: '$(Build.ArtifactStagingDirectory)' artifact: ${{ parameters.artifactName }} publishLocation: 'pipeline'
Finally, commit the changes to the new file.
Using Shared YAML
Not that we have the YAML that is the same between our two build jobs we can switch back over to our main YAML file, azure-pipelines.yml in the sample, and remove the steps we are wanting to replace. While the jobs will both have a steps section the only thing we will have left in them is a template call to our other YAML file, build.yml for the sample, that passes the parameters to run the other file with. The following is the resulting YAML file with the call to the shared file in both jobs highlighted.
trigger: none variables: buildConfiguration: 'Release' jobs: - job: WebApp1 displayName: 'Build WebApp1' pool: vmImage: 'ubuntu-latest' steps: - template: build.yml parameters: buildConFiguration: $(buildConfiguration) project: WebApp1.csproj artifactName: WebApp1 - job: WebApp2 displayName: 'Build WebApp2' pool: vmImage: 'ubuntu-latest' steps: - template: build.yml parameters: buildConFiguration: $(buildConfiguration) project: WebApp2.csproj artifactName: WebApp2
Wrapping Up
Being able to remove duplication from your YAML files should help improve the maintainability of your pipelines. I know the samples don’t show it, but the template is just a step and you could have other steps before or after it just like you would with normal tasks.
Also published on Medium.
This is a GREAT blog series! You did an awesome job keeping it as simple as possible. I’m new to the Azure Devops and this is really helping me out.
Thank you, J!
It would probably be worth pointing out that parameters and variables have important differences – parameters are available only at parsing time.https://docs.microsoft.com/en-us/azure/devops/pipelines/process/runtime-parameters?view=azure-devops&tabs=script
There are several types of variable syntax (macro, template expression and runtime):https://docs.microsoft.com/en-us/azure/devops/pipelines/process/variables?view=azure-devops&tabs=yaml%2Cbatch
Hi,
When you have this reusable yaml as a file in repository, are you still somehow able to edit it in the ‘pipeline editor’?
It seems to have a few features which make writing the yaml pipelines less painful, but it seems not accessible anymore.