Construct a condition in Azure DevOps using 'each' - azure-devops

In Azure DevOps (YAML pipeline), we have a stages that should be run only after another set of stages have been skipped.
In the example below, the parameter copyStages_UAT can be amended by users when triggering a manual run, meaning it's impossible to hard-code the dependsOn and condition properties, so necessitating the use of the directive each.
- template: ../Stages/stage--code--depoly-to-environment.yml
parameters:
name: Deploy_PRD_UKS
displayName: Deploy PRD - UK South
dependsOn:
- ${{ each uatStage in parameters.copyStages_UAT }}:
- Roll_Back_${{ uatStage.name }}
variables:
- template: ../Variables/variables--code--global.yml
- template: ../Variables/variables--code--prd.yml
environment: PRD
This stage above works in a pipeline, however because a successful run results in stages defined in dependsOn being skipped, sadly then Azure DevOps will also skip this stage.
To counter this, I'm trying to add a condition to check whether or not the previous stages were all skipped.
condition: >-
and(replace(
${{ each uatStage in parameters.copyStages_UAT }}:
eq(dependencies.Roll_Back_${{ uatStage.name }}.result, 'Skipped'),
), ', )', ' )')
Unfortunately though, it seems as though I cannot use the directive each in this context -
The directive 'each' is not allowed in this context. Directives are not supported for expressions that are embedded within a string. Directives are only supported when the entire value is an expression.
As condition can only be a string, how can I leverage expressions and/or directives to construct my desired condition?
Example of desired YAML
Assuming the following value was given for the parameter copyStages_UAT -
- name: UAT_UKS
displayName: UAT - UK South
- name: UAT_UKW
displayName: UAT - UK West
This is how the YAML should be compiled. I'm not worried out the format of the condition, as long as the relevant checks are included.
- template: ../Stages/stage--code--depoly-to-environment.yml
parameters:
name: Deploy_PRD_UKS
displayName: Deploy PRD - UK South
dependsOn:
- Roll_Back_UAT_UKS
- Roll_Back_UAT_UKW
condition: >-
and(
eq(dependencies.Roll_Back_UAT_UKS.result, 'Skipped'),
eq(dependencies.Roll_Back_UAT_UKW.result, 'Skipped')
)
variables:
- template: ../Variables/variables--code--global.yml
- template: ../Variables/variables--code--prd.yml
environment: PRD

Azure DevOps Pipelines does not have a particularly good way for solving this. However, and(...),
join(delimiter, ...) and 'Filtered Arrays' can be used to hackily accomplish this.
Observe that the following condition could be rearranged:
and(
eq(dependencies.Roll_Back_UAT_UKW.result, 'Skipped'),
eq(dependencies.Roll_Back_UAT_UKX.result, 'Skipped'),
eq(dependencies.Roll_Back_UAT_UKY.result, 'Skipped'),
eq(dependencies.Roll_Back_UAT_UKZ.result, 'Skipped')
)
and(
eq(dependencies.Roll_Back_
UAT_UKW
.result, 'Skipped'), eq(dependencies.Roll_Back_
UAT_UKX
.result, 'Skipped'), eq(dependencies.Roll_Back_
UAT_UKY
.result, 'Skipped'), eq(dependencies.Roll_Back_
UAT_UKZ
.result, 'Skipped')
)
Or more abstractly, where PREFIX=eq(dependencies.Roll_Back_ and SUFFIX=.result, 'Skipped'):
and(
<PREFIX>
UAT_UKW
<SUFFIX>, <PREFIX>
UAT_UKX
<SUFFIX>, <PREFIX>
UAT_UKY
<SUFFIX>, <PREFIX>
UAT_UKZ
<SUFFIX>
)
With filtered arrays (NAMES=parameters.parameterName.*.name) to extract the name, an aggregation could then be written:
and(<PREFIX>${{ join('<PREFIX>, <SUFFIX>', <NAMES>) }}<SUFFIX>)
Thus:
condition: |
and(
ne(dependencies.Roll_Back_${{ join('.result, ''Skipped''), ne(dependencies.Roll_Back_', parameters.copyStages_UAT.*.name) }}.result, 'Skipped')
)
But there are some obvious caveats with this:
If there is 0 elements in parameters.copyStages_UAT, then the expression would evaluate to and(dependencies.Roll_Back_.result, 'Skipped') which could be non-sensical.
and(...) requires a minimum of 2 arguments, so this could potentially fail if there is only 0 or 1 expression(s). To circumvent this, True can be supplied as the first and second argument such that the expression is always valid. If your logic instead requires or(...), then use False instead of True to keep the meaning consistent.
Therefore, you may need to protect against these scenarios occurring with a modified check:
${{ if eq(length(parameters.copyStages_UAT), 0) }}:
condition: false
${{ else }}:
condition: |
and(
True,
ne(dependencies.Roll_Back_${{ join('.result, ''Skipped''), ne(dependencies.Roll_Back_', parameters.copyStages_UAT.*.name) }}.result, 'Skipped')
)

Updated:
To summary your demand, you are looking for a expression that you can use it in condition while the dependsOn value are dynamic. And this stage should run only after another set of dependent stages are all skipped.
As far as I know and tested, this can not be achieved via each.
For further confirmation, I discussed this scenario with our pipeline PM who is more familiar with each and YAML pipeline.
Same as me, he also think this isn't possible to achieve. Because ${{ each }} expects to be the outermost part of a mapping key or value, so you can’t nest it into a condition string.
Work around:
You could fake it by having a hard-coded stage which depends on the dynamic list, figures out if they were all skipped, and sets an output variable. Then the real final job would only depend on that “decider” job, and its condition would depend on the contents of the output variable.
Figuring out that all upstream dependencies were skipped is something of an exercise for you. You might be able to dynamically construct one step per stage. Those steps map the stage’s status to a variable with a known name scheme. Then the final (hardcoded) step iterates the environment variables of the known name scheme and decides whether the next stage should proceed.
And... yes, I am aware how ugly that sounds ☹

Related

AzureDevops stage dependsOn stageDependencies

How to create a multi-stage pipeline depending on a stage/job-name derived from a parameter whereas stages run firstly in parallel and eventually one stage that waits for all previous stages?
Here's what I've tried so far:
A multi-stage pipeline runs for several stages depending on a tool parameter in parallel, whereas dependsOn is passed as parameter. Running it in parallel for each tool waiting for the previous stage for the said tool works smoothly.
Main template: all wait for for all
- ${{ each tool in parameters.Tools }}:
- template: ../stages/all-wait-for-all.yml
parameters:
Tool: ${{ tool }}
stages/all-wait-for-all.yml
parameters:
- name: Tool
type: string
stages:
- stage: ALL_WAIT_${{ parameters.Tool}}
dependsOn:
- PREPARE_STAGE
- OTHER_TEMPLATE_EXECUTED_FOR_ALL_TOOLS_${{ parameters.Tool }}
Now there should be one stage that should only run once and not per tool, but it should only run after the individual tool stages are done. It can't be hardcoded as there are various tools. So I hoped defining the individual wait-stages in a prepare job would work out:
Main template: prepare-stage
- script: |
toolJson=$(echo '${{ convertToJson(parameters.Tools) }}')
tools=$(echo "$toolJson" | jq '.[]' | xargs)
stage="ALL_WAIT"
for tool in $tools; do
stageName="${stage}_${tool }"
stageWaitArray+=($stageName)
done
echo "##vso[task.setvariable variable=WAIT_ON_STAGES]${stageWaitArray}"
echo "##vso[task.setvariable variable=WAIT_ON_STAGES;isOutput=true]${stageWaitArray}"
displayName: "Define wait stages"
name: WaitStage
stages/one-waits-for-all.yml
stages:
- stage: ONE_WAITS
dependsOn:
- $[ stageDependencies.PREPARE_STAGE.PREPARE_JOB.outputs['waitStage.WAIT_ON_STAGES'] ]
whereas below error is shown:
Stage ONE_WAITS depends on unknown stage $[ stageDependencies.PREPARE_STAGE.PREPARE_JOB.outputs['WaitStage.WAIT_ON_STAGES'] ].
As I understand depends on can not have dynamic $[] or macro $() expressions evaluated at runtime. You can use template expressions ${{}} which are evaluated at queue time.
Guess I was overthinking the solution as eventually it was pretty obvious.
So first template can be called within a loop from the main template whereas it's executed as many times as tools we got. Second template shall be called once waiting on previous stages for all tools, whereas the job/stage prefix is known, only the tool name as postfix was unknown. So just add them in a loop directly in dependsOn..
Here you go:
stages:
- stage: ONE_WAITS
dependsOn:
- PREPARE_STAGE
- ${{ each tool in parameters.Tools }}:
- OTHER_TEMPLATE_EXECUTED_FOR_ALL_TOOLS_${{ tool}}
- ALL_WAIT_${{ tool }}

What do double curly braces '{{}}' mean in YAML files as used in Azure DevOps Yaml pipelines?

Am working on Azure DevOps templates lately. And came across this double curly braces syntax. Just want to know how to use double curly braces in Azure DevOps.
Have seen some posts regarding the same.
Assign conditional value to variables in Azure DevOps
Curly braces in Yaml files
If else in Azure DevOps
Conditional Variable Assignment in Azure DevOps
So lets say, I have a variable defined in a group as follows.
Also we can define variables as follows in a yaml file.
variables:
- name: BuildConfiguration
value: 'Release'
- name: finalBuildArtifactName
value: 'TempFolderName'
When should we use the double curly brace syntax?
I have idea about and using the following ways to reference variables.
variables['MyVar']
variables.MyVar
What can we accomplish with double curly braces in contrast to the above two?
Its very difficult to get things working in a yaml pipeline. Do the change, checkin, run the pipeline, see the result and circle back again. This is highly time consuming, and not smooth to say the least.
${{}} syntax is used for expressions. What's more there is another $[ <expression> ]. And here is the difference:
# Two examples of expressions used to define variables
# The first one, a, is evaluated when the YAML file is compiled into a plan.
# The second one, b, is evaluated at runtime.
# Note the syntax ${{}} for compile time and $[] for runtime expressions.
variables:
a: ${{ <expression> }}
b: $[ <expression> ]
Please check documentation here.
And as you may see here
steps:
- task: PublishPipelineArtifact#1
inputs:
targetPath: '$(Pipeline.Workspace)'
${{ if eq(variables['Build.SourceBranchName'], 'main') }}:
artifact: 'prod'
${{ else }}:
artifact: 'dev'
publishLocation: 'pipeline'
you can use variables['Build.SourceBranchName'] syntax for accessing variables. But,
variables:
- name: foo
value: fabrikam # triggers else condition
pool:
vmImage: 'ubuntu-latest'
steps:
- script: echo "start"
- ${{ if eq(variables.foo, 'adaptum') }}:
- script: echo "this is adaptum"
- ${{ elseif eq(variables.foo, 'contoso') }}:
- script: echo "this is contoso"
- ${{ else }}:
- script: echo "the value is not adaptum or contoso"
You may also use variables.foo.
Expressons are often used for conditional evaluation, dynamic steps/jobs/stages configuration etc.

Azure Devops yml pipeline if else condition with variables

I am trying to use if else conditions in Azure Devops yml pipeline with variable groups. I am trying to implement it as per latest Azure Devops yaml pipeline build.
Following is the sample code for the if else condition in my scenario. test is a variable inside my-global variable group.
variables:
- group: my-global
- name: fileName
${{ if eq(variables['test'], 'true') }}:
value: 'product.js'
${{ elseif eq(variables['test'], false) }}:
value: 'productCost.js'
jobs:
- job:
steps:
- bash:
echo test variable value $(fileName)
When the above code is executed, in echo statement we don't see any value for filename, i.e. it empty, meaning none of the above if else condition was executed, however when I test the if else condition with the following condition.
- name: fileName
${{ if eq('true', 'true') }}:
value: 'product.js'
Filename did echo the correct value, i.e. product.js. So my conclusion is that I am not able to refer the variables from the variable group correctly. So any suggestion will be helpful and appreciated.
Thanks!
Unfortunately there is no ternary operator in Azure DevOps Pipelines. And it seems unlikely considering the state of https://github.com/microsoft/azure-pipelines-yaml/issues/256 and https://github.com/microsoft/azure-pipelines-yaml/issues/278. So for the time being the only choices are :
conditional insertion : it works with parameters, and should work with variables according to the documentation (but it is difficult to use properly),
or the hacks you can find in this Stack Overflow question.
Another work-around has been posted by Simon Alling on GitHub (https://github.com/microsoft/azure-pipelines-yaml/issues/256#issuecomment-1077684972) :
format(
replace(replace(condition, True, '{0}'), False, '{1}'),
valueIfTrue,
valueIfFalse
)
It is similar to the solution provided by Tejas Nagchandi, but I find it a little bit better because the syntax looks closer to what it would be if there was a ternary operator.
I was able to achieve the goal using some dirty work-around, but I do agree that using parameters would be much better way unless ternary operators are available for Azure DevOps YAML pipeline.
The issue is that ${{ if condition }}: is compile time expression, thus the variables under variable group are not available.
I was able to use runtime expressions $[<expression>]
Reference: https://learn.microsoft.com/en-us/azure/devops/pipelines/process/expressions?view=azure-devops
My pipeline:
trigger:
- none
variables:
- group: Temp-group-for-testing
- name: fileName
value: $[replace(replace('True',eq(variables['test'], 'True'), 'value1'),'True','value2')]
stages:
- stage: test
jobs:
- job: testvar
continueOnError: false
steps:
- bash: echo $(fileName)
displayName: "echo variable"
Results are available on github
After detailed investigation I realized that if else doesnt work with variables in Az Devop yaml pipelines, it only works with parameters. However the solution posted by #Tejas Nagchandi is a workaround and might be able to accomplish the same logic of if else setting variable value with replace commands. Hats off to TN.

Azure DevOps YAML Variables Evaluation Order

I have an issue with a variable evaluating to Null on a Condition.
This is the pipeline:
I have a variable template that is pulled in by the main yml file:
variables:
- ${{ if startsWith(variables['Build.SourceBranch'], 'refs/heads/feature/') }}:
- name: is_sandbox
value: true
- ${{ if eq(variables['system.pullRequest.targetBranch'], 'refs/heads/master') }}:
- name: is_sandbox
value: true
- name: bob
value: a
Note that is_sandbox is not set anywhere else.
I then have a condition on job:
condition: or (eq(variables.is_sandbox, true), eq(variables.bob, 'a'))
But this fails to evaluate. In the log is see:
system.pullRequest.targetBranch : refs/heads/master
and:
Expanded: or(eq(Null, True), eq(Null, 'a'))
Result: False
So it appears that the variables template has not set correctly. Why would that be?
Stage variables will only be available to activities within the stage. This is beneficial when reusing variables with different values across different stages. Think potentially stage is tied to an environment and the environment values are different so thus can pass in different variables at the stage level.
I believe you are looking to define these at the root level so variable values will be available to anything in the pipelines unless it is overwritten by a lower level scope.

Fill runtime azure pipeline parameters from external source

We looking to create a pipeline to update our multi-tenant azure environment. We need to perform some actions during the update per tenant. To accomplish this, we would like to create a job per tenant, so we can process tenants in parallel. To accomplish this, I want to use a runtime parameter to pass the tenants to update to my pipeline as follows:
parameters:
- name: tenants
type: object
the value of the tenants parameter might look like something like this:
- Name: "customer1"
Someotherproperty: "some value"
- Name: "customer2"
Someotherproperty: "some other value"
to generate the jobs, we do something like this:
stages:
- stage:
jobs:
- job: Update_Tenant
strategy:
matrix:
${{ each tenant in parameters.Tenants }}:
${{ tenant.tenantName }}:
name: ${{ tenant.tenantName }}
someproperty: ${{ tenant.otherProperty }}
maxParallel: 2
steps:
- checkout: none
- script: echo $(name).$(someproperty)
Now what we need, is some way to fill this tenants parameter. Now I tried a few solutions:
Ideally I would like to put a build stage before the Update_Tenants stage to call a REST api to get the tenants, and expand the tenants parameter when the Update_Tenants stage starts, but this is not supported AFAIK, since parameter expansion is done when the pipeline starts.
A less ideal but still workable option would have been to create a variable group yaml file containing the tenants, and include this variable group in my pipeline, and use the ${{ variables.Tenants }} syntax to reference them. However, for some reason, variables can only be strings.
The only solution I can currently think of, is to create a pipeline that calls a REST api to get the tenants to update, and then uses the azure devops api to queue the actual update process with the correct parameter value. But this feels like a bit of a clunky workaround to accomplish this.
Now my question is, are there any (better?) alternatives to accomplish what I want to do?
Maybe this can help. I was able to use external source (.txt file) to fill array variable in azure pipelines.
Working example
# Create a variable
- bash: |
arrVar=()
for images in `cat my_images.txt`;do
arrVar+=$images
arrVar+=","
done;
echo "##vso[task.setvariable variable=list_images]$arrVar"
# Use the variable
# "$(list_images)" is replaced by the contents of the `list_images` variable by Azure Pipelines
# before handing the body of the script to the shell.
- bash: |
echo my pipeline variable is $(list_images)
Sources (there is also example for matrix)
https://learn.microsoft.com/en-us/azure/devops/pipelines/process/variables?view=azure-devops&tabs=yaml%2Cbatch#set-a-job-scoped-variable-from-a-script
Other sources
https://learn.microsoft.com/en-us/azure/devops/pipelines/process/runtime-parameters?view=azure-devops&tabs=script
To accomplish this, we would like to create a job per tenant, so we
can process tenants in parallel.
Apart from rolling deployment strategy, you can also check Strategies and Matrix.
You can try something like this unless you have to use Runtime parameters:
jobs:
- job: Update
strategy:
matrix:
tenant1:
Someotherproperty1: '1.1'
Someotherproperty2: '1.2'
tenant2:
Someotherproperty1: '2.1'
Someotherproperty2: '2.2'
tenant3:
Someotherproperty1: '3.1'
Someotherproperty2: '3.2'
maxParallel: 3
steps:
- checkout: none
- script: echo $(Someotherproperty1).$(Someotherproperty2)
displayName: 'Echo something'