How to pass complex DevOps pipeline template parameter to script

GGirard picture GGirard · Jan 29, 2020 · Viewed 11.7k times · Source

In an Azure DevOps pipeline template, I am declaring a parameter as an array/sequence

parameters:
  mySubscription: ''
  myArray: []

steps:
- AzureCLI@2
  inputs:
    azureSubscription: ${{ parameters.mySubscription }}
    scriptType: pscore
    scriptPath: $(Build.SourcesDirectory)/script.ps1
    arguments: '-MyYAMLArgument ${{ parameters.myArray }}'

Value for the parameter is then passed from pipeline definition as

steps:
- template: myTemplate.yml
  parameters:
    mySubscription: 'azure-connection'
    myArray:
    - field1: 'a'
      field2: 'b'
    - field1: 'aa'
      field2: 'bb'

My problem is I can't pass that array as-is in YAML syntax (kind of ToString()) to be able to consume and treat that array from PowerShell in my template. When trying to run this pipeline, I get the following error: /myTemplate.yml (Line: X, Col: X): Unable to convert from Array to String. Value: Array. The line/column referenced in the error message correspond to arguments: '-MyYAMLArgument ${{ parameters.myArray }}' from my template.

I also tried to map the parameter as an environment for my script

- AzureCLI@2
  inputs:
    azureSubscription: ${{ parameters.mySubscription }}
    scriptType: pscore
    scriptPath: $(Build.SourcesDirectory)/script.ps1
    arguments: '-MyYAMLArgument $Env:MY_ENV_VAR'
  env:
    MY_ENV_VAR: ${{ parameters.myArray }}

This does not work too: /myTemplate.yml (Line: X, Col: Y): A sequence was not expected. That time line/column refers to MY_ENV_VAR: ${{ parameters.myArray }}.

Does anyone ever faced a similar requirement to pass complex types (here an array/sequence of object) defined from the pipeline definition to a PowerShell script? If so, how did you achieve it?

Answer

leoniDEV picture leoniDEV · Mar 25, 2020

I'm also facing a similar problem, my workaround is to flatten the array in a string using different separator for different dimensions.

For example I want to make some parameters required and fail the build if these parameters are not passed, instead of add a task for every parameter to check, I want to do this in a single task.

To do this I first pass, as a parameter (to another template, called check-required-params.yml which hold the task responsible for check the parameters), an array where each element is a string of the type name:value which is a concatenation (using the format expression) of the name and the value of required parameters separated by a colon:

# templates/pipeline-template.yml
parameters:
- name: endpoint
  type: string
  default: ''
- name: rootDirectory
  type: string
  default: $(Pipeline.Workspace)
- name: remoteDirectory
  type: string
  default: '/'
- name: archiveName
  type: string
  default: ''

#other stuff

      - template: check-required-params.yml
        parameters:
          requiredParams:
          - ${{ format('endpoint:{0}', parameters.endpont) }}
          - ${{ format('archiveName:{0}', parameters.archiveName) }}

Then in check-required-params.yml I join the array separating the elements with a semicolon using the expression ${{ join(';', parameters.requiredParams) }}, this create a string of the type endpoint:value;archiveName:value and pass this as an environmental variable.

At this point, using a little of string manipulations, in a script I can split the string using the semicolon as separator so I will get an array of strings like name:value which I can further split but this time using colon as separator. My check-required-params.yml looks like:

# templates/check-required-params.yml
parameters:
- name: requiredParams
  type: object
  default: []    

steps:
- task: PowerShell@2
  inputs:
  script: |
    $params = $env:REQURED_PARAMS -split ";"
    foreach($param in $params) {
      if ([string]::IsNullOrEmpty($param.Split(":")[1])) {
        Write-Host "##vso[task.logissue type=error;]Missing template parameter $($param.Split(":")[0])"
        Write-Host "##vso[task.complete result=Failed;]"
      }
    }
  targetType: inline
  pwsh: true
  env:
    REQURED_PARAMS: ${{ join(';', parameters.requiredParams) }}
  displayName: Check for required parameters

Then in my azure-pipelines.yml I can do:

#other stuff
- template: templates/pipeline-template.yml
  parameters:
    endpoint: 'myEndpoint'
    rootDirectory: $(Pipeline.Workspace)/mycode

In this example the build will fail because i don't pass the parameter archiveName

You can add some flexibility by using variables also for defining the separators instead of hardcoding in the scripts and in the expressions