'Construct a condition in Azure DevOps using 'each'
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
Solution 1:[1]
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 ?
Solution 2:[2]
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 toand(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 requiresor(...)
, then useFalse
instead ofTrue
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')
)
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | |
Solution 2 |