${{ if }} Syntax in Azure DevOps YAML

Viewed 10077

I have a pool of self hosted VMs (MyTestPool) half of which is dedicated to installing & testing a 'ON' build (that has few features turned on) and a 'OFF' build (that's a default installation). All my test agent VMs have 'ALL' as a user defined capability. Half of them are also tagged 'ON', and the other half 'OFF'.

Now I have 2 stages called DEPLOYOFF & DEPLOYON that can be skipped if a Variable Group variable 'skipDeployOffStage' or 'skipDeployOnStage' is set to true. What I would like to do is to use 'ALL' as an agent demand if only ON / OFF is to be tested. If both ON & OFF are to be tested, the appropriate stage would demand their respective 'ON' or 'OFF' VMs.

Question: The ${{ if }} DOES NOT WORK.

trigger: none
pr: none


pool: 'MyBuildPool'

variables:
  - group: TEST_IF_VARIABLE_GROUP
  - name: subPool
    value: 'ON'
  - name: useAllPool
    value: $[ or(eq( variables.skipDeployOnStage, true), eq( variables.skipDeployOffStage, true)) ]
    
stages:
- stage: DEPLOYOFF
  condition: ne(variables['skipDeployOffStage'], 'true')

  variables:
    # The test stage's subpool
    ${{ if variables.useAllPool }}:
      subPool: 'ALL'
    ${{ if not(variables.useAllPool)  }}:
      subPool: 'OFF'

  pool: 
    name: 'MyTestPool'
    demands: 
      - ${{ variables.subPool }}

  jobs:
  - job:

    steps:
    - checkout: none
    - pwsh: |
        Write-Host ("Value of useAllPool is: {0}" -f '$(useAllPool)')
        Write-Host ("Value of VG variable 'skipDeployOnStage' is {0} and 'skipDeployOffStage' is {1}" -f '$(skipDeployOnStage)', '$(skipDeployOffStage)')
        Write-Host ("Subpool is {0}" -f '$(subPool)')
      displayName: 'Determined SubPool'

The Output when one of the flags is false:

2020-08-02T18:39:05.5849160Z Value of useAllPool is: True
2020-08-02T18:39:05.5854283Z Value of VG variable 'skipDeployOnStage' is true and 'skipDeployOffStage' is false 
2020-08-02T18:39:05.5868711Z Subpool is ALL

The Output when both are false:

2020-08-02T18:56:40.5371875Z Value of useAllPool is: False
2020-08-02T18:56:40.5383258Z Value of VG variable 'skipDeployOnStage' is false and 'skipDeployOffStage' is false
2020-08-02T18:56:40.5386626Z Subpool is ALL

What am I missing?

2 Answers

There are two issues that cause your code to run incorrectly:

1.The ${{if}}: The way you write ${{if}} is incorrect, and the correct script is:

${{ if eq(variables['useAllPool'], true)}}:
   subPool: 'ALL'
${{ if ne(variables['useAllPool'], true)}}:
   subPool: 'OFF'

2.The definition of variables.useAllPool: You use a runtime expression ($[ <expression> ]), so when the ${{if}} is running, the value of variables.useAllPool is '$[ or(eq( variables.skipDeployOnStage, true), eq( variables.skipDeployOffStage, true)) ]' instead of true or false.

To solve this issue, you need to use compile time expression ${{ <expression> }}.

However, when using compile time expression, it cannot contain variables from variable groups. So you need to move the variables skipDeployOnStage and skipDeployOffStage from variable group to YAML file.

So, you can solve the issue by the following steps:

1.Delete the variables skipDeployOnStage and skipDeployOffStage from the variable group TEST_IF_VARIABLE_GROUP.

2.Modify the YAML file:

trigger: none
pr: none
 
 
pool: 'MyBuildPool'
 
variables:
  - group: TEST_IF_VARIABLE_GROUP
  - name: subPool
    value: 'ON'
  - name: skipDeployOnStage
    value: true
  - name: skipDeployOffStage
    value: false
  - name: useAllPool
value: ${{ or(eq( variables.skipDeployOnStage, true), eq( variables.skipDeployOffStage, true)) }}
    
stages:
- stage: DEPLOYOFF
  condition: ne(variables['skipDeployOffStage'], 'true')
 
  variables:
    # The test stage's subpool
    ${{ if eq(variables['useAllPool'], true)}}:
      subPool: 'ALL'
    ${{ if ne(variables['useAllPool'], true)}}:
      subPool: 'OFF'
  pool: 
    name: 'MyTestPool'
    demands: 
      - ${{ variables.subPool }}
 
  jobs:
  - job:
 
    steps:
    - checkout: none
    - pwsh: |
        Write-Host ("Value of useAllPool is: {0}" -f '$(useAllPool)')
        Write-Host ("Value of VG variable 'skipDeployOnStage' is {0} and 'skipDeployOffStage' is {1}" -f '$(skipDeployOnStage)', '$(skipDeployOffStage)')
        Write-Host ("Subpool is {0}" -f '$(subPool)')
      displayName: 'Determined SubPool'

You can modify the value of skipDeployOnStage and skipDeployOffStage in YAML file to test whether this solution works.

My requirement:

  • Have a self hosted VM pool of say, 20 machines
  • 10 of them have "OFF" as a User Capability (Name as OFF & value as blank) 10 of them have
  • "ON" as a User capability (Name as ON & value as blank)
  • All of them have a "ALL" as an additional user capability
  • The pipeline basically installs 2 variations of the products and runs tests on them on the designated machines in the pool (ON / OFF)
  • When the user wants to run both OFF & ON deployments, I have stages that demand OFF or ON run
  • What I want to do is when the user wants only ON or OFF deployment, to save time I want to use all 20 of the machines, deploy and test so I can reduce overall testing time.

I was trying vainly, to replace the pool demand at run time as I did not want to update user capabilities for 20-50 VMs just prior to the run every time. That's what I have to do if use the traditional demand syntax:

  pool: 
    name: 'MyTestPool'
    demands: 
      # There's no OR or other syntax supported here
      # LVALUE must be a built-in variable such as Agent.Name OR a User capability
      # I will have to manually change the DeploymentType before the pipeline is run
      - DeploymentType -equals $(subPool)  

So, I was trying to ascertain the value of the subPool at run time and use the below syntax so I don't have to manually configure the user capabilities before run.

pool:
  name: ${{ parameters.pool }}
  demands:
    - ${{ parameters.subPool}}

Here's my research:

You can definitely mix compile & run time expressions. The compile time expression evaluates to whatever the value was at the compile time. If it's an expression or a variable (and not a constant), the entire expression or variable is substituted as Jane Ma-MSFT notes.

For example, I have been able to use queue time variables in compile time expressions without issues. For example, I've used which pool to use as a queue time variable and then pass on the same to a template which uses a compile time syntax for the value.

parameters:
  - name: pool
    type: string  

jobs:
  - job: ExecAllPrerequisitesJob
    displayName: 'Run Stage prerequisites one time from a Single agent'

    pool:
      name: ${{ parameters.pool }}

However, the real issue is where you're using the compile time expression. Essentially, in the above, the entire ${{ parameters.pool }} gets replaced by $(buildPool), a queue time variable at compile time. But, pool supports using a variable for the pool name. This is so convoluted and undocumented where you can use expressions, variables (compile or run) and where you must use constants.

One such example:

jobs:
- job: SliceItFourWays
  strategy:
    parallel: 4  # $[ variables['noOfVMs'] ] is the ONLY syntax that works to replace 4

In some places, such as Pool demands, Microsoft's YAML parser dutifully replaces the variable. However, the run time doesn't support custom variables as LVALUE.

It only supports evaluation of custom run time variables in the RVALUE portion of the demand.

  pool: 
    name: ${{ parameters.buildPool }} # Run time supports variables here
    demands: 
      - ${{ parameters.subPool }}  # LVALUE: Must be resolved to a CONSTANT at compile time
      - poolToRunOn -equals '${{ parameters.subPool }}'  # RVALUE... custom user capability.  This evaluates and applies the demand correctly.

Conclusion:

MICROSOFT'S YAML IMPLEMENTATION SUCKS!

Related