be9inn3r
be9inn3r

Reputation: 113

use output variables from another stage in a different stage

I have the next code snippets:

- task: AzureCLI@2
  name: A
  displayName: "Run tf ${{ parameters.action }}"
  inputs:
    azureSubscription: '***-${{ parameters.env }}' 
    scriptType: 'bash'
    scriptLocation: 'inlineScript'
    inlineScript: |
      if [ ${{ parameters.action }} = "plan" ]; then
          echo "Running terraform plan..."
          timeout -s SIGINT 58m terraform plan out=terraform.plan
          changes=$(terraform show -no-color terraform.plan | grep -E "No changes.")
          echo "Checking for Changes variable [$changes]"
          if [ -n "$changes" ]; then
            echo "No changes detected. Exiting without further actions."
            echo '##vso[task.setvariable variable=PlanChanges; isOutput=true]'notdetected
          else
            echo '##vso[task.setvariable variable=PlanChanges; isOutput=true]'changesdetected
          fi
      else
          echo "Running terraform apply..."
          timeout -s SIGINT 58m terraform apply input=false auto-approve
      fi
    addSpnToEnvironment: true
    workingDirectory: '${{ parameters.workingDirectory }}'    

These tasks are part of a template called script.yml and it is used in another template called stages.yml following the next structure in the next comment:

parameters:
- name: workingDirectory
  type: string
- name: environments
  type: object
  default:
  - dev
  - qa


stages:
- ${{ each env in parameters.environments }}:
    - stage: Plan_${{ env }}
      condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest'))
      jobs:
        - job: Plan
          steps:
          - template: script.yml
            parameters:
              env: ${{ env }}
              action: plan
              workingDirectory: '${{ parameters.workingDirectory }}'

    - stage: Apply_${{ env }}
      dependsOn:
        - Plan_${{ env }}
      jobs: 
      - deployment: 
        displayName: Deploy
        environment: ${{ env }}
        strategy:
          runOnce:
            deploy:
              steps:            
              - template: script.yml
                parameters:
                  env: ${{ env }}
                  action: apply
                  workingDirectory: '${{ parameters.workingDirectory }}'

Now, I have isOutput=true option for (echo '##vso[task.setvariable variable=PlanChanges; isOutput=true]'notdetected), I need to consume this output variable in my Apply stage which is a different one. I need to set a condition based on this output for the Apply Stage to be skipped/to run only the variable output is "changesdetected"... but I tried first to consume that variable to see if works.

I have tried referencing this variable in the stages.yml: adding variables: varPlan: $[ stageDependencies.Plan_${{ env }}.Plan.outputs['PlanOverview.PlanChanges'] ] or varPlan: $[ stageDependencies.Plan_${{ env }}.outputs['Plan.PlanOverview.PlanChanges'] ] when I run the script echo $(varPlan) not showing the output of variable.

According to: https://learn.microsoft.com/en-us/azure/devops/pipelines/process/expressions?view=azure-devops&branch=pr-en-us-1603#job-to-job-dependencies-across-stages:~:text=Dependency%20syntax%20overview

how to reference that variable output to my condition in the apply stage?

Tried like these: varPlan: $[ stageDependencies.Plan_${{ env }}.Plan.outputs['[email protected]'] ]

varPlan: $[ stageDependencies.Plan_${{ env }}.Plan.outputs['A.PlanChanges'] ]

varPlan: $[ dependencies.Plan_${{ env }}.outputs['[email protected]'] ]

varPlan: $[ stageDependencies.Plan_${{ env }}.outputs['Plan.A.PlanChanges'] ]

varPlan: $[ stageDependencies.Plan_${{ env }}.Plan.A.outputs['PlanChanges'] ]

varPlan: $[ stageDependencies.Plan_${{ env }}.Plan.outputs['PlanOverview.PlanChanges'] ]

varPlan: $[ stageDependencies.Plan_${{ env }}.PlanOverview.outputs['PlanChanges'] ]

varPlan: $[ stageDependencies.Plan_${{ env }}.Plan.outputs['PlanOverview.PlanChanges'] ]

Upvotes: 2

Views: 957

Answers (2)

Bright Ran-MSFT
Bright Ran-MSFT

Reputation: 13834

On the stage-level of all the "Apply_xxx" stages, you can set the condition like as below.

- ${{ each env in parameters.environments }}:
  - stage: Plan_${{ env }}
    . . .
  
  - stage: Apply_${{ env }}
    dependsOn: Plan_${{ env }}
    condition: and(succeeded(), eq(stageDependencies.Plan_${{ env }}.outputs['Plan.A.PlanChanges'], 'changesdetected'))

With this condition, the "Apply_xxx" stages will run when the value of output variable "PlanChanges" is "changesdetected".


In addition, another important thing you need to know is that based on the current definition in your main YAML (stages.yml), all the stages will run in a single line in sequence based on their order of defining in the YAML file. So, they will look like as below in a pipeline run.

enter image description here

In this situation, a stage will be skipped if any of the previous stages is not succeeded. For example, on above image, in the expected result, the "Apply_prod" stage should run but it was skipped due to the previous "Apply_qa" stage was skipped.

For your case, the ideal results should be:

  • "Plan_dev" and "Apply_dev" are in a separate line, and "Apply_dev" only depends on "Plan_dev".
  • "Plan_qa" and "Apply_qa" are in a separate line, and "Apply_qa" only depends on "Plan_qa".
  • "Plan_prod" and "Apply_prod" are in a separate line, and "Apply_prod" only depends on "Plan_prod".

To reach this, you can update the main YAML (stages.yml) like as below:

  1. Add a "main" stage as the parent node of all the separate lines. In this "main" stage, you can let it do nothing. So, it will be always succeeded.
  2. Set the "Plan_xxx" stage to depend on the "main" stage.
  3. Set the "Apply_xxx" stage to depend on the "Plan_xxx" stage.
stages:
- stage: main
  jobs:
  - job: main
    steps:
    - checkout: none

- ${{ each env in parameters.environments }}:
  - stage: Plan_${{ env }}
    dependsOn: main
    condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest'))
    jobs:
    . . .
  
  - stage: Apply_${{ env }}
    dependsOn: Plan_${{ env }}
    condition: and(succeeded(), eq(stageDependencies.Plan_${{ env }}.outputs['Plan.A.PlanChanges'], 'changesdetected'))
    jobs:
    . . .

enter image description here


EDIT:

  • For the question in your first reply blow:

    If I did not misnderstand your demands, when the value of output variable (PlanChanges) generated from 'Plan_${{ env }}' stage is 'changesdetected', then the corresponding 'Apply_${{ env }}' stage should run and not skip, and when the output value is 'notdetected', the 'Apply_${{ env }}' stage should skip.

    So, if 'Plan_prod' stage outputs 'changesdetected', the 'Apply_prod' stage should run. On the first pipeline image I posted above, the 'Plan_prod' stage was outputting 'changesdetected', the 'Apply_prod' stage should not skip.

  • For the question in your second reply below:

    You seem have wrong syntax when use the setvariable command to set the output variable using Bash.

    Change the command line like as the following:

    echo "##vso[task.setvariable variable=PlanChanges;isoutput=true]notdetected"
    echo "##vso[task.setvariable variable=PlanChanges;isoutput=true]changesdetected"
    

EDIT_2:

Below I will share you with a sample that I attempted on my side. In this sample, I set the "Apply_qa" stage will be skipped based on the condition below.

condition: and(succeeded(), eq(stageDependencies.Plan_${{ env }}.outputs['Plan.A.PlanChanges'], 'changesdetected'))

You can reference this sample to check and update the code in your YAML files.

  • The main YAML file: azure-pipelines.yml
parameters:
- name: workingDirectory
  type: string
- name: environments
  type: object
  default:
  - dev
  - qa
  - prod

stages:
- stage: main
  jobs:
  - job: main
    steps:
    - checkout: none

- ${{ each env in parameters.environments }}:
  - stage: Plan_${{ env }}
    dependsOn: main
    condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest'))
    jobs:
    - job: Plan
      steps:
      - template: script.yml
        parameters:
          env: ${{ env }}
          action: plan
          workingDirectory: '${{ parameters.workingDirectory }}'
  
  - stage: Apply_${{ env }}
    dependsOn: Plan_${{ env }}
    condition: and(succeeded(), eq(stageDependencies.Plan_${{ env }}.outputs['Plan.A.PlanChanges'], 'changesdetected'))
    jobs:
    - deployment: Deploy
      environment: ${{ env }}
      variables:
      # The variable can be used by the steps (include script.yml) within this job. 
        varPlanChanges: $[ stageDependencies.Plan_${{ env }}.Plan.outputs['A.PlanChanges'] ]
      strategy:
        runOnce:
          deploy:
            steps:
            - template: script.yml
              parameters:
                env: ${{ env }}
                action: apply
                workingDirectory: '${{ parameters.workingDirectory }}'
  • The template YAML file: script.yml
steps:
- task: Bash@3
  name: A
  displayName: 'Set Output'
  inputs:
    targetType: inline
    script: |
      if [ ${{ parameters.action }} = "plan" ]; then
        echo "The action is ${{ parameters.action }}."
        echo "The environment is ${{ parameters.env }}."
        if [ ${{ parameters.env }} = "qa" ]; then
          echo "##vso[task.setvariable variable=PlanChanges;isoutput=true]notdetected"
        else
          echo "##vso[task.setvariable variable=PlanChanges;isoutput=true]changesdetected"
        fi
      else
        echo "The action is not plan. It is ${{ parameters.action }}."
        echo "The environment is ${{ parameters.env }}."
      fi

- ${{ if eq(parameters.action, 'plan') }}:
  - task: Bash@3
    displayName: 'Variables for plan action'
    inputs:
      targetType: inline
      script: |
        echo "env = ${{ parameters.env }}"
        echo "action = ${{ parameters.action }}"
        echo "workingDirectory = ${{ parameters.workingDirectory }}"
        echo "PlanChanges = $(A.PlanChanges)"

- ${{ if eq(parameters.action, 'apply') }}:
  - task: Bash@3
    displayName: 'Variables for apply action'
    inputs:
      targetType: inline
      script: |
        echo "env = ${{ parameters.env }}"
        echo "action = ${{ parameters.action }}"
        echo "workingDirectory = ${{ parameters.workingDirectory }}"
        echo "varPlanChanges = $(varPlanChanges)"
  • The result of running the pipeline, see the second image I posted above.

Upvotes: 1

Scott Richards
Scott Richards

Reputation: 766

Two options for you to try:


Option 1:

When using isOutput=true, you have to redefine the variable in the job or stage that it needs to be used in.

The syntax to reference a stage dependency is as follows:

$[ stageDependencies.StageName.JobName.outputs['StepName.VariableName'] ]

I have refactored your yaml file a bit to flatten it into one script for simplicity, and also renamed some fields. Here is a full example that may work for you:

parameters:
- name: workingDirectory
  type: string
  
- name: environments
  type: object
  default:
  - dev
  - qa
  - prod 

stages:
- ${{ each env in parameters.environments }}:
    - stage: Plan_${{ env }}
      condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest'))
      jobs:
        - job: Plan
          steps:
          - task: AzureCLI@2
            name: PlanScript
            displayName: "Run tf plan"
            inputs:
              azureSubscription: '***-${{ parameters.env }}' 
              scriptType: 'bash'
              scriptLocation: 'inlineScript'
              inlineScript: |
                echo "Running terraform plan..."
                timeout -s SIGINT 58m tfmake azure plan out=terraform.plan
                changes=$(terraform show -no-color terraform.plan | grep -E "No changes.")
                echo "Checking for Changes variable [$changes]"
                if [ -n "$changes" ]; then
                  echo "No changes detected. Exiting without further actions."
                  echo '##vso[task.setvariable variable=planChanges; isOutput=true]'notdetected
                else
                  echo '##vso[task.setvariable variable=planChanges; isOutput=true]'changesdetected
                fi
              addSpnToEnvironment: true
              workingDirectory: '${{ parameters.workingDirectory }}'

          - script: |
              terraform show terraform.plan 
              echo $(planChanges)
            workingDirectory: '${{ parameters.workingDirectory }}'
            displayName: "Plan overview"
            name: PlanOverview    

    - stage: Apply_${{ env }}
      dependsOn:
        - Plan_${{ env }}
      jobs: 
      - deployment: 
        displayName: Deploy
        environment: ${{ env }}
        variables:
          varPlan: $[ stageDependencies.Plan_${{ env }}.Plan.outputs['PlanScript.planChanges'] ]
        strategy:
          runOnce:
            deploy:
              steps:            
              - task: AzureCLI@2
                displayName: "Run tf apply"
                inputs:
                  azureSubscription: '***-${{ parameters.env }}' 
                  scriptType: 'bash'
                  scriptLocation: 'inlineScript'
                  inlineScript: |
                    if [ "$(varPlan)" = "changesdetected" ]; then
                      echo "Running terraform apply..."
                      timeout -s SIGINT 58m tfmake apply input=false auto-approve
                    else
                      echo "Plan changes is: $(varPlan)"" 
                    fi
                  addSpnToEnvironment: true
                  workingDirectory: '${{ parameters.workingDirectory }}'

Option 2:

You may be able to simplify your yaml a hole lot by having all your scripts run in a single job, that way you don't need to worry about parsing variables between stages/jobs.

parameters:
- name: workingDirectory
  type: string

- name: environments
  type: object
  default:
  - dev
  - qa
  - prod 

stages:
- ${{ each env in parameters.environments }}:
  - stage: Plan_and_Apply_${{ env }}
    condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest'))
    jobs: 
    - deployment: 
      displayName: Deploy
      environment: ${{ env }}
      strategy:
        runOnce:
          deploy:
            steps:            
            - task: AzureCLI@2
              displayName: "Run tf plan"
              inputs:
                azureSubscription: '***-${{ parameters.env }}' 
                scriptType: 'bash'
                scriptLocation: 'inlineScript'
                inlineScript: |
                  echo "Running terraform plan..."
                  timeout -s SIGINT 58m tfmake azure plan out=terraform.plan

                  changes=$(terraform show -no-color terraform.plan | grep -E "No changes.")
                  echo "Checking for Changes variable [$changes]"

                  if [ -n "$changes" ]; then
                    echo "No changes detected. Exiting without further actions."
                  else               
                    echo "Running terraform apply..."
                    timeout -s SIGINT 58m tfmake apply input=false auto-approve
                  fi
                addSpnToEnvironment: true
                workingDirectory: '${{ parameters.workingDirectory }}'

Upvotes: 0

Related Questions