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:

  1. The if statement should start with a dash - just like a normal task step would.
  2. The statement syntax is ${{ if <condition> }} where the condition is any valid Azure DevOps condition.
  3. The statement ends with a colon :.
  4. 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.

Note
The --fail-with-body argument is available starting from curl version 7.76.0 or newer. The --fail option is available in earlier versions and works mostly the same but it doesn’t wait for the response body.

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!

Subscribe to my newsletter

What’s new with PäksTech? Subscribe to receive occasional emails where I will sum up stuff that has happened at the blog and what may be coming next.

powered by TinyLetter | Privacy Policy