Loops in Azure DevOps Pipelines
Janne Kemppainen |If you need to do the same thing over and over again, why repeat yourself? In this post we’ll discover how looping works in Azure DevOps pipelines.
The basics
The Azure DevOps documentation briefly mentions the each keyword with a sample usage:
parameters:
- name: listOfStrings
type: object
default:
- one
- two
steps:
- ${{ each value in parameters.listOfStrings }}:
- script: echo ${{ value }}
The each
keyword works the same way you’d expect from your typical programming language. It goes over an iterable one item at a time and stores the value in a variable of your choice, in this example: value
.
The loop needs to follow the YAML syntax. Since steps
is a list of items, the each
statement must also start with a hyphen -
. The statement itself is contained inside ${{ }}
and the line ends with a colon character. Everything that you want to be included in the loop needs to be indented one level further than you’d normally do.
There are many parameter data types in Azure DevOps, but there is no separate type for a list of strings. In this case we need to use the object
type which accepts any YAML structure. The default value is used when no values are provided.
It is important to understand that the loop is evaluated when the pipeline is parsed, so it generates all steps in advance. You can’t change them during the pipeline runtime. That’s why the parameter values need to be defined before the pipeline execution starts.
You can only use parameters in each loops since variables in Azure DevOps pipelines are always strings. Parameters that are defined at the top level of the pipeline can be changed at startup. Template parameters need to be passed when calling the template.
Looping a map
Maps can also be looped, so you don’t need to use lists for everything. Each item in a map contains a key and a value that you need to reference instead of the object itself.
The parameters object itself is also a map, and can be looped through:
parameters:
- name: environment
type: string
default: test
values:
- production
- staging
- test
- name: runTests
type: boolean
default: true
steps:
- ${{ each parameter in parameters }}:
- script: echo "${{ parameter.value }}"
displayName: Print ${{ parameter.key }}
This pipeline creates a step for each parameter and uses echo
to print the value to the console. See how the key and value need to be accessed.
Example: Authenticate multiple .npmrc files
Let’s take a look at a practical example.
The npm Authenticate task works on the local .npmrc
file from the repository to fetch and store credentials that are used for private registry authentication. Unlike your typical developer authentication methods, npmAuthenticate doesn’t edit the global configuration file in the user’s home directory, but it adds the credentials directly to the file inside the repository instead.
In a monorepo structure each component may have its own .npmrc file. In this case it’s not enough to only authenticate the repository top level file.
We can solve the issue by using the each loop in a template:
parameters:
- name: npmrcFiles
type: object
default: [".npmrc"]
steps:
- ${{ each workingFile in parameters.npmrcFiles }}:
- task: npmAuthenticate@0
inputs:
workingFile: ${{ workingFile }}
displayName: Authenticate ${{ workingFile }}
It goes through each item in the npmrcFiles
parameter and calls the npmAuthenticate@0
task that many times. If the parameter is omitted it defaults to .npmrc
at the root of your project.
If you’ve stored the template as templates/authenticateNpm.yaml
, the main pipeline could look something like this:
pool:
vmImage: "ubuntu-latest"
steps:
- template: templates/authenticateNpm.yaml
parameters:
- contact-form/.npmrc
- store/.npmrc
- user-api/.npmrc
- bash: cd contact-form && npm ci && npm test
- bash: cd store && npm ci && npm test
condition: succeededOrFailed()
- bash: cd user-api && npm ci && npm test
condition: succeededOrFailed()
The example pipeline authenticates three components, installs their dependencies and runs the unit tests.
Previous post
Replace Docker with Podman and BuildahNext post
Optional Arguments in Azure DevOps