title | ms.custom | description | ms.topic | ms.date | monikerRange |
---|---|---|---|---|---|
Template expressions |
seodec18 |
How to use expressions in templates |
conceptual |
06/30/2023 |
>=azure-devops-2020 |
Use template expressions to specify how values are dynamically resolved during pipeline initialization.
Wrap your template expression inside this syntax: ${{ }}
.
Template expressions can expand template parameters, and also variables.
You can use parameters to influence how a template is expanded.
The parameters
object works like the variables
object
in an expression. Only predefined variables can be used in template expressions.
Note
Expressions are only expanded for stages
, jobs
, steps
, and containers
(inside resources
).
You cannot, for example, use an expression inside trigger
or a resource like repositories
.
Additionally, on Azure DevOps 2020 RTW, you can't use template expressions inside containers
.
For example, you define a template:
# File: steps/msbuild.yml
parameters:
- name: 'solution'
default: '**/*.sln'
type: string
steps:
- task: msbuild@1
inputs:
solution: ${{ parameters['solution'] }} # index syntax
- task: vstest@2
inputs:
solution: ${{ parameters.solution }} # property dereference syntax
Then you reference the template and pass it the optional solution
parameter:
# File: azure-pipelines.yml
steps:
- template: steps/msbuild.yml
parameters:
solution: my.sln
Within a template expression, you have access to the parameters
context that contains the values of parameters passed in.
Additionally, you have access to the variables
context that contains all the variables specified in the YAML file plus
many of the predefined variables (noted on each variable in that article).
Importantly, it doesn't have runtime variables such as those stored on the pipeline or given when you start a run.
Template expansion happens early in the run, so those variables aren't available.
You can use general functions in your templates. You can also use a few template expression functions.
- Simple string token replacement
- Min parameters: 2. Max parameters: N
- Example:
${{ format('{0} Build', parameters.os) }}
→'Windows Build'
- Evaluates to the first non-empty, non-null string argument
- Min parameters: 2. Max parameters: N
- Example:
parameters:
- name: 'restoreProjects'
default: ''
type: string
- name: 'buildProjects'
default: ''
type: string
steps:
- script: echo ${{ coalesce(parameters.foo, parameters.bar, 'Nothing to see') }}
You can use template expressions to alter the structure of a YAML pipeline. For instance, to insert into a sequence:
# File: jobs/build.yml
parameters:
- name: 'preBuild'
type: stepList
default: []
- name: 'preTest'
type: stepList
default: []
- name: 'preSign'
type: stepList
default: []
jobs:
- job: Build
pool:
vmImage: 'windows-latest'
steps:
- script: cred-scan
- ${{ parameters.preBuild }}
- task: msbuild@1
- ${{ parameters.preTest }}
- task: vstest@2
- ${{ parameters.preSign }}
- script: sign
# File: .vsts.ci.yml
jobs:
- template: jobs/build.yml
parameters:
preBuild:
- script: echo hello from pre-build
preTest:
- script: echo hello from pre-test
When an array is inserted into an array, the nested array is flattened.
To insert into a mapping, use the special property ${{ insert }}
.
# Default values
parameters:
- name: 'additionalVariables'
type: object
default: {}
jobs:
- job: build
variables:
configuration: debug
arch: x86
${{ insert }}: ${{ parameters.additionalVariables }}
steps:
- task: msbuild@1
- task: vstest@2
jobs:
- template: jobs/build.yml
parameters:
additionalVariables:
TEST_SUITE: L0,L1
If you want to conditionally insert into a sequence or a mapping in a template, use insertions and expression evaluation. You can also use if
statements outside of templates as long as you use template syntax.
For example, to insert into a sequence in a template:
# File: steps/build.yml
parameters:
- name: 'toolset'
default: msbuild
type: string
values:
- msbuild
- dotnet
steps:
# msbuild
- ${{ if eq(parameters.toolset, 'msbuild') }}:
- task: msbuild@1
- task: vstest@2
# dotnet
- ${{ if eq(parameters.toolset, 'dotnet') }}:
- task: dotnet@1
inputs:
command: build
- task: dotnet@1
inputs:
command: test
# File: azure-pipelines.yml
steps:
- template: steps/build.yml
parameters:
toolset: dotnet
For example, to insert into a mapping in a template:
# File: steps/build.yml
parameters:
- name: 'debug'
type: boolean
default: false
steps:
- script: tool
env:
${{ if eq(parameters.debug, true) }}:
TOOL_DEBUG: true
TOOL_DEBUG_DIR: _dbg
steps:
- template: steps/build.yml
parameters:
debug: true
You can also use conditional insertion for variables. In this example, start
always prints and this is a test
only prints when the foo
variable equals test
.
variables:
- name: foo
value: test
pool:
vmImage: 'ubuntu-latest'
steps:
- script: echo "start" # always runs
- ${{ if eq(variables.foo, 'test') }}:
- script: echo "this is a test" # runs when foo=test
The each
directive allows iterative insertion based on a YAML sequence (array) or mapping (key-value pairs).
For example, you can wrap the steps of each job with other pre- and post-steps:
# job.yml
parameters:
- name: 'jobs'
type: jobList
default: []
jobs:
- ${{ each job in parameters.jobs }}: # Each job
- ${{ each pair in job }}: # Insert all properties other than "steps"
${{ if ne(pair.key, 'steps') }}:
${{ pair.key }}: ${{ pair.value }}
steps: # Wrap the steps
- task: SetupMyBuildTools@1 # Pre steps
- ${{ job.steps }} # Users steps
- task: PublishMyTelemetry@1 # Post steps
condition: always()
# azure-pipelines.yml
jobs:
- template: job.yml
parameters:
jobs:
- job: A
steps:
- script: echo This will get sandwiched between SetupMyBuildTools and PublishMyTelemetry.
- job: B
steps:
- script: echo So will this!
You can also manipulate the properties of whatever you're iterating over. For example, to add more dependencies:
# job.yml
parameters:
- name: 'jobs'
type: jobList
default: []
jobs:
- job: SomeSpecialTool # Run your special tool in its own job first
steps:
- task: RunSpecialTool@1
- ${{ each job in parameters.jobs }}: # Then do each job
- ${{ each pair in job }}: # Insert all properties other than "dependsOn"
${{ if ne(pair.key, 'dependsOn') }}:
${{ pair.key }}: ${{ pair.value }}
dependsOn: # Inject dependency
- SomeSpecialTool
- ${{ if job.dependsOn }}:
- ${{ job.dependsOn }}
# azure-pipelines.yml
jobs:
- template: job.yml
parameters:
jobs:
- job: A
steps:
- script: echo This job depends on SomeSpecialTool, even though it's not explicitly shown here.
- job: B
dependsOn:
- A
steps:
- script: echo This job depends on both Job A and on SomeSpecialTool.
If you need to escape a value that literally contains ${{
, then wrap the value in an expression string. For example, ${{ 'my${{value' }}
or ${{ 'my${{value with a '' single quote too' }}
Templates and template expressions can cause explosive growth to the size and complexity of a pipeline. To help prevent runaway growth, Azure Pipelines imposes the following limits:
- No more than 100 separate YAML files may be included (directly or indirectly)
- No more than 20 levels of template nesting (templates including other templates)
- No more than 10 megabytes of memory consumed while parsing the YAML (in practice, this is typically between 600 KB - 2 MB of on-disk YAML, depending on the specific features used)