Drew M
Drew M

Reputation: 11

Azure Pipeline steplist template: Referencing a parameter in another parameter

I am attempting to create an Azure Pipelines template that contains a list of steps. It contains a steps object and some parameters. The idea is that I can reference this template in another file's steps object, along with other templates, such that the list will flatten into a single steplist with steps from various templates. This is what I currently have:

parameters:
# The solution configuration to be used, e.g. Release, Debug, etc.
- name: solutionConfig
  type: string
  default: Release

steps:
# Build the target
- task: VSBuild@1
  displayName: Build the target
  inputs:
    configuration: ${{ parameters.solutionConfig }}

This works fine and I am able to use the step as a template in another file. The issue I am facing is trying to create another parameter that uses the value of solutionConfig, but is over-writable by the user of the template. Something like this:

parameters:
# The solution configuration to be used, e.g. Release, Debug, etc.
- name: solutionConfig
  type: string
  default: Release
# The path where binaries are created after compiling.
- name: binContentWildcard
  type: string
  default: '**/${{ parameters.solutionConfig }}/**/*.msi'

steps:
# Build the target
- task: VSBuild@1
  displayName: Build the target
  inputs:
    configuration: ${{ parameters.solutionConfig }}
# Copy binaries to staging directory
- task: CopyFiles@2
  displayName: Copy binary artifacts
  inputs:
    contents: ${{ parameters.binContentWildcard }}
    targetFolder: $(Build.ArtifactStagingDirectory)/bin

However, this results in a A template expression is not allowed in this context error in Azure Pipelines (referring to the default value supplied to binContentWildcard) and the pipeline is not able to run.

In the Microsoft docs, they indicate:

Parameters are only available at template parsing time. Parameters are expanded just before the pipeline runs so that values surrounded by ${{ }} are replaced with parameter values. Use variables if you need your values to be more widely available during your pipeline run.

I tried using a variable to provide runtime resolution, like the following:

parameters:
# The solution configuration to be used, e.g. Release, Debug, etc.
- name: solutionConfig
  type: string
  default: Release
# The path where binaries are created after compiling.
- name: binContentWildcard
  type: string
  default: ''

variables:
  ${{ if eq(parameters.binContentWildcard, '') }}:
    binContentWildcardVar: '**/${{ parameters.solutionConfig }}/**/*.msi'
  ${{ if ne(parameters.binContentWildcard, '') }}:
    binContentWildcardVar: ${{ parameters.binContentWildcard }}

steps:
# Build the target
- task: VSBuild@1
  displayName: Build the target
  inputs:
    configuration: ${{ parameters.solutionConfig }}
# Copy binaries to staging directory
- task: CopyFiles@2
  displayName: Copy binary artifacts
  inputs:
    contents: $(binContentWildcardVar)
    targetFolder: $(Build.ArtifactStagingDirectory)/bin

But this results in a Unexpected value error in Azure Pipelines with regards to defining variables in this scope. This is because the template is referenced from the context of a step, but variables can only be declared in the context of a pipeline, stage, or job.

I don't think this is supported, but I also tried to use the runtime expression syntax with the binContentWildcard parameter:

parameters:
# The solution configuration to be used, e.g. Release, Debug, etc.
- name: solutionConfig
  type: string
  default: Release
# The path where binaries are created after compiling.
- name: binContentWildcard
  type: string
  default: '**/$[ parameters.solutionConfig ]/**/*.msi'

steps:
# Build the target
- task: VSBuild@1
  displayName: Build the target
  inputs:
    configuration: ${{ parameters.solutionConfig }}
# Copy binaries to staging directory
- task: CopyFiles@2
  displayName: Copy binary artifacts
  inputs:
    contents: $[ parameters.binContentWildcard ]
    targetFolder: $(Build.ArtifactStagingDirectory)/bin

However, this results in the string literal "$[ parameters.binContentWildcard ]" being supplied to the contents input.

Is it possible to use the value of a parameter in another parameter within the context of a steplist template?

Upvotes: 1

Views: 1569

Answers (3)

Van Thoai Nguyen
Van Thoai Nguyen

Reputation: 1036

This works for me.

parameters:
- name: csProjectName
  type: string
- name: requiredProjects
  type: string
  default: ''  

steps:
- task: DotNetCoreCLI@2
  inputs:
    command: restore
    projects: ${{ coalesce(parameters.requiredProjects, format('src/{0}/*.csproj', parameters.csProjectName)) }}
    verbosityRestore: Minimal
    workingDirectory: ${{ parameters.workingDirectory }}
    feedsToUse: config
    nugetConfigPath: nuget.config
  displayName: "nuget restore"

Upvotes: 0

Drew M
Drew M

Reputation: 11

The solution suggested by DreadedFrost may work for some. However, having a template at the scope of a steplist is really what I wanted in my situation.

My solution is similar to my original attempt and DreadedFrost's answer, expect that the variables are created by the task.setvariable logging command, which allows the variables to be dynamically created by a step and be referenced by subsequent steps in the same job without needing a variables element in the template.

parameters:
# The solution configuration to be used, e.g. Release, Debug, etc.
- name: solutionConfig
  type: string
  default: Release
# The path where binaries are created after compiling.
- name: binContentWildcard
  type: string
  default: ''

steps:
# Create dynamic variables that depend on parameter inputs
- bash: |
    echo "##vso[task.setvariable variable=binContentWildcardVar]${{ coalesce(parameters.binContentWildcard, format('**/{0}/**/*.msi', parameters.solutionConfig)) }}"
# Build the target
- task: VSBuild@1
  displayName: Build the target
  inputs:
    configuration: ${{ parameters.solutionConfig }}
# Copy binaries to staging directory
- task: CopyFiles@2
  displayName: Copy binary artifacts
  inputs:
    contents: $(binContentWildcardVar)
    targetFolder: $(Build.ArtifactStagingDirectory)/bin

Upvotes: 0

DreadedFrost
DreadedFrost

Reputation: 2978

I believe in this situation the tasks should form a job template. To best scale templates it is usually recommend to have a task template, a job template calling task templates, and a stage calling stage templates.

In this situation I would recommend something like the following:

parameters:
# The solution configuration to be used, e.g. Release, Debug, etc.
- name: solutionConfig
  type: string
  default: Release
# The path where binaries are created after compiling.
- name: binContentWildcard
  type: string
  default: ''
- name: serviceName
  type: string
  default: ''

jobs:
- job: '{{ parameters.serviceName }}_job'
  variables:
  ${{ if eq(parameters.binContentWildcard, '') }}:
    binContentWildcardVar: '**/${{ parameters.solutionConfig }}/**/*.msi'
  ${{ if ne(parameters.binContentWildcard, '') }}:
    binContentWildcardVar: ${{ parameters.binContentWildcard }}

  steps:
      # Build the target
  - task: VSBuild@1
    displayName: Build the target
    inputs:
      configuration: ${{ parameters.solutionConfig }}
  # Copy binaries to staging directory
  - task: CopyFiles@2
    displayName: Copy binary artifacts
    inputs:
      contents: $(binContentWildcardVar)
      targetFolder: $(Build.ArtifactStagingDirectory)/bin

The serviceName has been included to denote that a job of the same name can only appear once per stage so making this flexible will help at scale.

This job template would be called in a stage via:

  jobs:
    - template: job_template.yml

Upvotes: 0

Related Questions