Search code examples
powershellazure-devopsyamlazure-pipelinescicd

Azure DevOps: Unable to Access PowerShell Output Variable in Template - Variable Not Resolving in Template


I'm having trouble accessing an output variable from a PowerShell task in my Azure DevOps pipeline and passing it into a deployment template. When I set the variable in a PowerShell script and try to pass it into my template, it doesn't resolve as expected.

The issue is that the variable $(filteredWebsitesPath) in my template is not resolving to the expected value. Instead, it just prints as $(filteredWebsitesPath) instead of the actual paths or values I need.

trigger:
- main

pool:
  vmImage: 'windows-latest'

stages:

- stage: Build
  displayName: 'Build Stage'
  jobs:
  - job: BuildJob
    steps:
      - task: Npm@1
      - task: Npm@1
      - task: PublishBuildArtifacts@1
        inputs:
          PathtoPublish: 'build'
          ArtifactName: 'drop'


- stage: Deploy
  displayName: 'Deploy to Production'
  dependsOn: Build
  condition: succeeded()

  jobs:
  - deployment: PreDeployJob
    displayName: 'PreDeployJob'
    environment: 
      name: 'Production'
      resourceType: VirtualMachine

    strategy:
      runOnce:
        deploy:
          steps:
          - task: PowerShellOnTargetMachines@3
            displayName: 'Get Website Names'
            name: GetWebsites
            inputs:
              Machines: 'ip:port'
              InlineScript: |
                  $websites = Get-ChildItem -Directory -Path 'C:\myPath\' | Select-Object -ExpandProperty Name
                  $filteredWebsites = $websites -join ','
                  Write-Host "##vso[task.setvariable variable=filteredWebsitesNew;isOutput=true]$filteredWebsites"
          - script: echo "Output: $(GetWebsites.filteredWebsitesNew)"
            name: echovar

  - job: SetVariableJob
    displayName: "Set Variable for Template"
    dependsOn: PreDeployJob
    variables:
      filteredWebsitesPath: $[ dependencies.PreDeployJob.outputs['PreDeployJob.GetWebsites.filteredWebsitesNew'] ]
    steps:
    - script: echo "Filtered Websites Path $(filteredWebsitesPath)"      

  - template: templates/deploy-template.yml
    parameters:
      filteredWebsitesNew: $(filteredWebsitesPath)

And here’s the template, where I’m trying to use filteredWebsitesNew:

parameters:
  filteredWebsitesNew: ''

jobs:
- ${{ each website in split(parameters.filteredWebsitesNew, ',') }}:
  - deployment: DeployJob
    displayName: 'Deploy to ${{ website }}'
    environment: 
      name: 'Production'
      resourceType: VirtualMachine
    strategy:
      runOnce:
        deploy:
          steps:
          - script: echo "Deploying to website ${{ website }}"

Despite setting the variable in the SetVariableJob, it isn’t being resolved when used in the deploy-template.yml template. Instead, I see $folderPath = "C:\myPath$(filteredWebsitesPath)\images" in the logs, where $(filteredWebsitesPath) doesn’t resolve to the actual variable value.

What am I missing, or is there an alternative way to ensure the variable resolves correctly in the template?

I tried setting the output variable filteredWebsitesNew in a PowerShell task within the PreDeployJob and expected it to resolve in the deploy-template.yml template. In SetVariableJob, I referenced the output variable using dependencies syntax and passed it to the template as $(filteredWebsitesPath). I expected the template to receive the resolved paths, but instead, it printed the literal variable name $(filteredWebsitesPath) without resolving it to the actual paths.


Solution

  • As mentioned in the comments, template expressions such as ${{ each ... }} are compiled before the pipeline starts and before the output variable is set.

    If you want to run each deployment in parallel I suggest you create a dedicated pipeline for the deployments, that is triggered after the output variable is set.

    For example, consider a pipeline named deploySomething:

    parameters:
      - name: filteredWebsitesNew
        displayName: 'A comma separated list of websites to deploy'
        type: string
    
    pool:
      vmImage: 'ubuntu-latest'
    
    # Disable CI-CD trigger
    trigger: none
    
    jobs:
    - ${{ each website in split(parameters.filteredWebsitesNew, ',') }}:
      - job: DeployJob_${{ website }}
        displayName: 'Deploy to ${{ website }}'
        environment: 
          name: 'Production'
          resourceType: VirtualMachine
        steps:
        - script: echo "Deploying to website ${{ website }}"
          displayName: 'Deploy to ${{ website }}'
    

    After setting the output variable in the original pipeline you can configure a job to trigger the deploySomething pipeline as follows:

    jobs:
      - deployment: PreDeployJob
        displayName: 'PreDeployJob'
        environment: 
          name: A
        strategy:
          runOnce:
            deploy:
              steps:
                - pwsh: |
                    $filteredWebsites='foo,bar'
                    Write-Host "##vso[task.setvariable variable=filteredWebsitesNew;isOutput=true]$filteredWebsites"
                  name: GetWebsites
                - script: |
                    echo "Output: $(GetWebsites.filteredWebsitesNew)"
                  name: echovar
    
      - job: TriggerBuild
        displayName: 'Trigger Build'
        dependsOn: PreDeployJob
        # don't run this job if the output variable 'filteredWebsitesNew' is not set / is empty
        condition: >-
          and(
            succeeded(),
            ne(dependencies.PreDeployJob.outputs['PreDeployJob.GetWebsites.filteredWebsitesNew'], '')
          )
        variables:
          filteredWebsitesPath: $[ dependencies.PreDeployJob.outputs['PreDeployJob.GetWebsites.filteredWebsitesNew'] ]
        timeoutInMinutes: 3
        cancelTimeoutInMinutes: 1
        steps:
          - checkout: none
          - script: |
              echo "Filtered Websites Path: $(filteredWebsitesPath)"
            displayName: 'Show variable'
          - pwsh: >-
              $run=$(
              az pipelines run
              --name "deploySomething"
              --organization $(System.CollectionUri)
              --project $(System.TeamProject)
              --branch $(Build.SourceBranchName)
              --parameters filteredWebsitesNew="$(filteredWebsitesPath)"
              --output json) | ConvertFrom-Json
    
              $pipelineUrl = "$(System.CollectionUri)$(System.TeamProject)/_build/results?buildId=$($run.id)"
    
              Write-Host "##vso[task.logissue type=warning]Pipeline was triggered. Web URL: $pipelineUrl"
            displayName: 'Trigger the deployment build'
            env:
              AZURE_DEVOPS_EXT_PAT: $(System.AccessToken)
    

    The script uses the azure pipeline run command of the Azure DevOps CLI. As an alternative, you can use the Trigger Build Task for more advanced scenarios.

    Please note you might need to configure the new pipeline's permissions for the user/group that is running the build:

    Set build queue permissions