Optional Arguments in Azure DevOps
Janne Kemppainen |Sometimes your Azure DevOps pipelines need to adapt to different situations with sane default values. On the other hand, optional arguments become especially handy when you want to create flexible and reusable templates.
In this post we’ll go through some useful tips that can really improve your pipelines!
Default values
Maybe your pipeline needs to install a specific version of a software but you want to give an opportunity to override it as needed. Using default values solves this problem.
parameters:
- name: helmVersion
type: string
default: '3.9.0'
steps:
- task: HelmInstaller@1
displayName: Install Helm ${{ parameters.helmVersion}}
inputs:
helmVersionToInstall: ${{ parameters.helmVersion }}
This way the user of the template (or pipeline) gets your intended version automatically. It can be especially useful if the version number has to be changed often since then you only need to change the value in one place. Users that rely on a specific version can still override it as needed.
Conditional steps
Sometimes you may want to be to enable or disable steps as needed. This can be done with if conditions.
Consider this runnerSetup.yaml
template file:
parameters:
- name: node
type: boolean
default: false
- name: nodeVersion
type: string
default: 16
- name: python
type: boolean
default: false
steps:
- ${{ if eq(parameters.node, true)}}:
- task: NodeTool@0
inputs:
versionSpec: ${{ parameters.nodeVersion }}
displayName: Use Node ${{ parameters.nodeVersion }}
- ${{ if eq(parameters.python, true) }}:
- task: UsePythonVersion@0
inputs:
versionSpec: '3.x'
displayName: Switch to latest Python 3
The if
syntax is a bit weird at first but as long as you remember that it should result in valid YAML you should be alright. Just remember these points when working with conditional steps:
- The if statement should start with a dash
-
just like a normal task step would. - The statement syntax is
${{ if <condition> }}
where the condition is any valid Azure DevOps condition. - The statement ends with a colon
:
. - Everything belonging to that if statement should be indented by one extra level.
This example contains two optional steps, one for installing Node.js and the other for installing the latest Python version. In this case the default configuration wouldn’t install anything. In fact, the steps that are not included by the if statements won’t appear in the step execution list for the pipeline at all. In comparison, steps with conditions that evaluate are shown with skipped status.
For example, this pipeline would only install Python:
pool:
vmImage: 'ubuntu-latest'
trigger: none
steps:
- template: runnerSetup.yaml
parameters:
python: true
With a template like this it would probably make sense to set all default values to false so that the template user needs to explicitly choose which components they want to include.
Pass conditional task arguments
Maybe your pipeline needs to be able to call tasks, or even templates, with different configurations. In this case, we can utilize if statements for template parameters or task inputs.
One cool trick that I’ve also demonstrated in my Send Teams Notifications from Azure DevOps post is to add a conditional condition to your template. In the case of a Teams notification you might want to send a different message based on the pipeline status.
In this example I’ve reduced the template to just echoing the message to the console instead of sending it to Teams. Check the linked article if you’re interested in the actual details. The sendMessage.yaml
file could look something like this:
parameters:
- name: message
- name: condition
default: ''
- name: displayName
default: Send message
steps:
- bash: echo "${{ parameters.message }}"
displayName: ${{ parameters.displayName }}
${{ if parameters.condition }}:
condition: ${{ parameters.condition }}
In this case the message
parameter is required as it doesn’t have a default value. The displayName
parameter is optional and it defaults to “Send message” if no other value is provided.
The condition
parameter is a string with an empty default value that evaluates to false in the if statement. This time the if statement doesn’t start with a hyphen, but the line still ends with a colon. Then the condition definition is indented inside the if block.
When Azure DevOps evaluates this template during pipeline initialization it receives the condition parameter as a string and places it to the generated end result. When the pipeline execution reaches the step with the condition it isn’t even aware that a template was used.
Here’s a simple pipeline to demonstrate this in practice:
pool:
vmImage: 'ubuntu-latest'
trigger: none
steps:
- bash: exit 1
- template: sendMessage.yaml
parameters:
message: Build succeeded!
displayName: Send success message
condition: succeeded()
- template: sendMessage.yaml
parameters:
message: Build failed!
displayName: Send failure message
condition: failed()
- template: sendMessage.yaml
parameters:
message: Build either succeeded or failed!
displayName: Send succeeded or failed message
condition: succeededOrFailed()
If you try to run this the Send success message
step will be skipped since the first step has failed due to a non-zero return code. The other two steps are going to run since their condition evaluates to true.
The success message step wouldn’t actually need a succeeded()
condition as it is implicitly assumed when one is not defined. In this case, defining the condition explicitly improves readability.
Optional arguments in Bash scripts
While the if conditions work nicely with templates, parameters and task inputs, we cannot use them inside the multiline strings for custom Bash scripts. In that case we need to do some Bash scripting instead.
This example might feel a little contrived, so please bear with me. The curl
command is a good example of a Linux command line application with lots of options. We could create a template called curl.yaml
that lets us invoke curl
with different configurations.
parameters:
- name: url
- name: method
default: ''
- name: verbose
type: boolean
default: false
- name: output
default: ''
- name: file
default: ''
- name: fail
type: boolean
default: false
- name: displayName
default: ''
steps:
- bash: |
[[ -n "${{ parameters.method }}" ]] && ARG_METHOD="-X ${{ parameters.method }}"
[[ "${{ parameters.verbose }}" = "true" ]] && ARG_VERBOSE="-v"
[[ -n "${{ parameters.output }}" ]] && ARG_OUTPUT="-o ${{ parameters.output }}"
[[ -n "${{ parameters.file }}" ]] && ARG_FILE="-data '@${{ parameters.file }}'"
[[ "${{ parameters.fail }}" = "true" ]] && ARG_FAIL="--fail-with-body"
curl \
$ARG_METHOD \
$ARG_VERBOSE \
$ARG_OUTPUT \
$ARG_FILE \
$ARG_FAIL \
"${{ parameters.url }}"
${{ if parameters.displayName }}:
displayName: ${{ parameters.displayName }}
The way this works is that first we run a Bash test command for each parameter and determine if we should store the parameter in a variable or not. In most cases we check if the input is an empty string (the default value) but with the Boolean parameters we check if the input matches the string “true” and add the flag as needed.
All argument values are expanded when we call curl
. Variables that haven’t been set are empty, so they don’t affect anything. The file
option uses the --data
input argument that normally takes the data as a string on the command line, but if you start the value with @
it will be interpreted as a filename.
Now we could use the template to do web requests. The following is just an example and it won’t actually work with these URLs, but you’ll get the idea.
pool:
vmImage: 'ubuntu-latest'
trigger: none
steps:
- template: curl.yaml
parameters:
url: example.com
output: output.html
displayName: Download example.com
- template: curl.yaml
parameters:
url: example.com/api/upload
file: output.html
fail: true
displayName: Send file
- template: curl.yaml
parameters:
url: example.com
method: DELETE
fail: true
verbose: true
displayName: Delete example.com
This example shows how you could use curl to download or upload files. If you don’t define the HTTP method curl will normally default to GET. If the request has data then the method is going to default to POST instead.
Conclusion
I hope these tips have helped you create better Azure DevOps templates! If you have any questions or feedback you can reach me on Twitter. See you in the next one!
Previous post
Loops in Azure DevOps Pipelines