Using task output variables in template parameters - azure

The first stage in my pipeline checks for what services have actually changed. This is in an effort to speed up the pipeline by avoiding rebuilding, retesting, redeploying services if there have been no changes.
This is the changed.yaml for that stage:
parameters:
- name: comparedTo
default: ''
stages:
- stage: Changed
displayName: Check for changes in services and configs...
jobs:
- job: Changes
displayName: Checking for changes in services and configs...
steps:
- bash: |
mapfile -t changed < <(git diff HEAD ${{ parameters.comparedTo }} --name-only | awk -F'/' 'NF!=1{print $1}' | sort -u)
servicesChanged=()
configChanged=()
echo ""
echo "Total Changed: ${#changed[#]}"
for i in "${changed[#]}"
do
echo $i
if [[ $i == 'admin' ]]; then
echo "##vso[task.setvariable variable=adminChanged;isOutput=True]true"
servicesChanged+=("admin")
elif [[ $i == 'admin-v2' ]]; then
echo "##vso[task.setvariable variable=adminV2Changed;isOutput=True]true"
servicesChanged+=("admin-v2")
elif [[ $i == 'api' ]]; then
echo "##vso[task.setvariable variable=apiChanged;isOutput=True]true"
servicesChanged+=("api")
elif [[ $i == 'client' ]]; then
echo "##vso[task.setvariable variable=clientChanged;isOutput=True]true"
servicesChanged+=("client")
elif [[ $i == 'k8s' ]]; then
echo "##vso[task.setvariable variable=k8sChanged;isOutput=True]true"
configsChanged+=("k8s")
elif [[ $i == 'pipelines' ]]; then
echo "##vso[task.setvariable variable=pipelineChanged;isOutput=True]true"
configsChanged+=("pipelines")
fi
done
echo ""
echo "Services Changed: ${#servicesChanged[#]}"
for i in "${servicesChanged[#]}"
do
echo $i
done
echo ""
echo "Configs Changed: ${#configsChanged[#]}"
for i in "${configsChanged[#]}"
do
echo $i
done
if [[ ${#servicesChanged[#]} > 0 ]]; then
echo ""
echo "Any services changed: True"
echo "##vso[task.setvariable variable=anyServicesChanged;isOutput=true]true"
echo "##vso[task.setvariable variable=servicesChanged;isOutput=true]${servicesChanged[#]}"
fi
if [[ ${#configsChanged[#]} > 0 ]]; then
echo ""
echo "Any configs changed: True"
echo "##vso[task.setvariable variable=anyConfigsChanged;isOutput=true]true"
echo "##vso[task.setvariable variable=configsChanged;isOutput=true]${configsChanged[#]}"
fi
echo ""
name: detectChanges
As you can see, it creates a number of task output variables:
# This just indicates if the service has changed: true/false
echo "##vso[task.setvariable variable=<service-name>;isOutput=True]true"
# This should be creating a an output variable that is an array of the services that have changed
echo "##vso[task.setvariable variable=servicesChanged;isOutput=true]${servicesChanged[#]}"
So I gave myself two options: just a true/false for each service or iterating (somewhow) through an array of the services that have changed.
Each stage basically has the following form:
# pr.yaml
...
- template: templates/unitTests.yaml
parameters:
services:
- api
- admin
- admin-v2
- client
...
parameters:
- name: services
type: object
default: []
stages:
- stage: UnitTests
displayName: Run unit tests on service...
dependsOn: Changed
condition: succeeded()
jobs:
- job: UnitTests
condition: or(eq(stageDependencies.Changed.Changes.outputs['detectChanges.anyServicesChanged'], true), eq(variables['Build.Reason'], 'Manual'))
displayName: Running unit tests...
steps:
- ${{ each service in parameters.services }}:
- bash: |
echo "Now running ${{ service }} unit tests..."
Here is what I've tried so far and the errors I get:
Adding each service conditionally to services array or adding the array of changed services:
- template: templates/changed.yaml
parameters:
comparedTo: origin/production
- template: templates/unitTests.yaml
dependsOn: Changed
parameters:
services:
- ${{ if eq(stageDependencies.Changed.Changes.outputs['detectChanges.apiChanged'], true) }}
- api
- ${{ if eq(stageDependencies.Changed.Changes.outputs['detectChanges.adminChanged'], true) }}
- admin
- ${{ if eq(stageDependencies.Changed.Changes.outputs['detectChanges.adminV2Changed'], true) }}
- admin-v2
- ${{ if eq(stageDependencies.Changed.Changes.outputs['detectChanges.clientChanged'], true) }}
- client
Or...
- template: templates/changed.yaml
parameters:
comparedTo: origin/production
- template: templates/unitTests.yaml
dependsOn: Changed
parameters:
services:
- ${{ if eq(dependencies.Changed.outputs['Changes.detectChanges.apiChanged'], true) }}
- api
- ${{ if eq(dependencies.Changed.outputs['Changes.detectChanges.adminChanged'], true) }}
- admin
- ${{ if eq(dependencies.Changed.outputs['Changes.detectChanges.adminV2Changed'], true) }}
- admin-v2
- ${{ if eq(dependencies.Changed.outputs['Changes.detectChanges.clientChanged'], true) }}
- client
Or...
- template: templates/changed.yaml
parameters:
comparedTo: origin/production
- template: templates/unitTests.yaml
dependsOn: Changed
parameters:
services:
- $[ stageDependencies.Changed.Changes.outputs['detectChanges.servicesChanged'] ]
This results in:
An error occurred while loading the YAML build pipeline. Object reference not set to an instance of an object.
I know variables: will only take strings and not arrays.
One solution would be to have a variables: for each of the true/false variables and then conditions based on the parameters.services and whether the task output variables is true.
Any suggestions?
Ref:
Task Output Variables: https://learn.microsoft.com/en-us/azure/devops/pipelines/process/variables?view=azure-devops&tabs=yaml%2Cbatch#set-variables-in-scripts
Parameters: https://learn.microsoft.com/en-us/azure/devops/pipelines/process/templates?view=azure-devops#parameters
Expressions: https://learn.microsoft.com/en-us/azure/devops/pipelines/process/expressions?view=azure-devops

The template expression ${{}} is evaluated at compile-time(before the jobs run), which means it cannot access to the variables that are dynamically set at the runtime(after the jobs start). So you cannot use template expression ${{}} in above scenario. See below description from here.
Within a template expression, you have access to the parameters context that contains the values of parameters passed in. Additionally, you have access to the variables context that contains all the variables specified in the YAML file plus many of the predefined variables (noted on each variable in that topic). Importantly, it doesn't have runtime variables such as those stored on the pipeline or given when you start a run. Template expansion happens very early in the run, so those variables aren't available
You can use conditions as a workaround. You need to add multiple tasks to be executed on the conditions. See below example:
- template: templates/changed.yaml
parameters:
comparedTo: origin/production
- template: templates/unitTests.yaml
dependsOn: Changed
#unitTests.yaml
stages:
- stage: UnitTests
displayName: Run unit tests on service...
dependsOn: Changed
condition: succeeded()
jobs:
- job: UnitTests
condition: or(eq(stageDependencies.Changed.Changes.outputs['detectChanges.anyServicesChanged'], true), eq(variables['Build.Reason'], 'Manual'))
displayName: Running unit tests...
variables:
changedServices: $[stageDependencies.Changed.Changes.outputs['detectChanges.servicesChanged']]
steps:
- bash: |
echo "Now running api unit tests..."
name: Api-unit-test
conidtion: contains(variables['changedServices'], 'api')
- bash: |
echo "Now running admin unit tests..."
name: admin-unit-test
conidtion: contains(variables['changedServices'], 'admin')
- bash: |
echo "Now running client unit tests..."
name: client-unit-test
conidtion: contains(variables['changedServices'], 'client')
Another workaround is to separate your pipeline into two pipelines. The fist pipeline to run the Changed stage. And Then call rest api in a script task to trigger the second pipeline and pass the variables in the request body. See this similar thread.

Related

Azure DevOps Azure PowerShell task output variable - Unable to pass value to third stage

I am trying to pass variables between stages. I was successfully able to pass values to immediate next stage, but after that the variables are blank. I tried all the ways to get this, but failing
stages:
- stage: InitialStage
jobs:
- job: StandAlone
displayName: Required Stand Alone Job
steps:
- bash: |
echo "##vso[task.setvariable variable=doThing;isOutput=true]Yes"
name: bash_test
- stage: SecondStage
jobs:
- job:
displayName: SecondStage
variables:
variable1: $[ stageDependencies.InitialStage.StandAlone.outputs['bash_test.doThing'] ]
steps:
- task: CmdLine#2
name: test
inputs:
script: |
echo "$(variable1)"
- stage: ThirdStage
jobs:
- job:
displayName: ThirdStage
variables:
variable1: $[ stageDependencies.InitialStage.StandAlone.outputs['bash_test.doThing'] ]
steps:
- task: CmdLine#2
name: test
inputs:
script: |
echo "$(variable1)"
The echo at stage SecondStage will give value, but ThirdStage it is blank, I am having a doubt either we can access a variable only in immediate stage or else I am doing something wrong, any help on this will be much appreciated.
Option 1:
In order to pass variable to the another stage you need to add isOutput=true
See the example as shown below;
- task: Bash#3
name: setprojectName
inputs:
targetType: 'inline'
script: |
echo "##vso[task.setvariable variable=projectNameGlobal;isOutput=true]$(echo $(Build.Repository.Name)| sed 's|.*/||')"
echo "##vso[task.setvariable variable=projectName]$(echo $(Build.Repository.Name)| sed 's|.*/||')"
projectName is only available the stage where variable populated but projectNameGlobal is available in next stage also.
Also see the documentation
Option 2:
## azure-pipelines.yml
stages:
- stage: one
jobs:
- job: A
steps:
- task: Bash#3
inputs:
filePath: 'script-a.sh'
name: setvar
- bash: |
echo "##vso[task.setvariable variable=skipsubsequent;isOutput=true]true"
name: skipstep
- stage: two
jobs:
- job: B
variables:
- name: StageSauce
value: $[ stageDependencies.one.A.outputs['setvar.sauce'] ]
- name: skipMe
value: $[ stageDependencies.one.A.outputs['skipstep.skipsubsequent'] ]
steps:
- task: Bash#3
inputs:
filePath: 'script-b.sh'
name: fileversion
env:
StageSauce: $(StageSauce) # predefined in variables section
skipMe: $(skipMe) # predefined in variables section
- task: Bash#3
inputs:
targetType: 'inline'
script: |
echo 'Hello inline version'
echo $(skipMe)
echo $(StageSauce)
In above option you can see how to use one variable where populated by any of the prior stages

need replace alias value of predefined variables in azure devops pipelines

I have enabled pipeline resource triggers between two pipelines. would like to replace alias value dynamically with triggering pipeline resource. below is the pipeline code
resources:
pipelines:
- pipeline: pipeline1
project: onecom
source: pipeline1-api
trigger:
branches:
- develop
- feat/*
- pipeline: pipeline2
project: onecom
source: pipeline2-api
trigger:
branches:
- develop
- feat
variables:
- name: apiname
value: $(resources.pipeline.<Alias>.pipelineName)
- name: dockertag
value: $(resources.pipeline.<Alias>.sourceCommit)
- name: runname
value: $(resources.pipeline.<Alias>.runName)
stages:
- stage: ScanImage
jobs:
- job: ScanImage
pool:
vmImage: 'ubuntu-16.04'
steps:
- script: echo $(apiname)
- script: echo $(runname)
I would like to replace Alias value in $(resources.pipeline..pipelineName) with value pipeline1 if build comes from source: pipeline1-api and with pipeline2 if build comes from source: pipeline2-api dynamically.
I would like to replace Alias value in $(resources.pipeline..pipelineName) with value pipeline1 if build comes from source: pipeline1-api and with pipeline2 if build comes from source: pipeline2-api dynamically.
Since the value of nested variables (like $(resources.pipeline.$(alias).pipelineName)) are not yet supported in the build/release pipelines. So we could not use it in the variable directly:
variables:
- name: apiname
value: $(resources.pipeline.$(alias).pipelineName)
To resolve this issue, we need add a inline powershell to set the variable resources.pipeline.<Alias>.pipelineName based on the value of the $(resources.triggeringAlias):
variables:
- name: alias
value: $(resources.triggeringAlias)
- task: InlinePowershell#1
inputs:
script: |
if ("$(alias)" -eq "PipelineA")
{
Write-Host ("##vso[task.setvariable variable=dockertag]$(resources.pipeline.PipelineA.sourceCommit) | cut -c -7")
}
elseif ("$(alias)" -eq "PipelineB")
{
Write-Host ("##vso[task.setvariable variable=dockertag]$(resources.pipeline.PipelineB.sourceCommit) | cut -c -7")
}
Update:
could you please help me same config in bash as we are using these
task in linux machines
- task: PowerShell#2
displayName: 'Inline Powershell'
inputs:
TargetType: inline
Script: |
if ("$(alias)" -eq "PipelineA")
{
Write-Host ("##vso[task.setvariable variable=dockertag]$(resources.pipeline.PipelineA.sourceCommit) | cut -c -7")
}
elseif ("$(alias)" -eq "PipelineB")
{
Write-Host ("##vso[task.setvariable variable=dockertag]$(resources.pipeline.PipelineB.sourceCommit) | cut -c -7")
}
pwsh: true
please try this
variables:
- name: alias
value: $(resources.triggeringAlias)
then you can try replace it as below
$(resources.pipeline.$(alias).pipelineName)

Tagging docker containers with azure pipelines

I have a multistage YAML Azure pipeline. I tag my images based on the build ID. This works well until a job fails and I need to rerun it.
On the rerun I get a newly incremented build Id so it no longer references same docker image used in the original run.
$(Build.BuildId)
Is there anyway around this?
Re-running job or stage doesn't change Build.BuildId. I checked this using below pipeline. However if you want to run whole pipeline for the same build id you my try to use runtime paramaters and provide a tagName on your own (like below):
parameters:
- name: tagName
type: string
default: ' '
trigger: none
pr: none
pool:
vmImage: 'ubuntu-latest'
stages:
- stage: A
jobs:
- job: A
steps:
- pwsh: |
$tagName = '$(Build.BuildId)'
if('${{ parameters.tagName }}' -ne ' ') {
$tagName = '${{ parameters.tagName }}'
}
echo $tagName
- job: B
steps:
- bash: echo "B"
- stage: B
jobs:
- job: A
steps:
- pwsh: |
$tagName = '$(Build.BuildId)'
if('${{ parameters.tagName }}' -ne ' ') {
$tagName = '${{ parameters.tagName }}'
}
echo $tagName
- bash: exit 1
- job: B
steps:
- bash: echo "B"
You could expand stage and rerun the stage, this won't change the buildId:

Azure DevOps pipelines - get build number of previous stage

I have a yml pipeline with 2 stages:
- stage:
- stage:
My second stage needs to refer to the $(Build.BuildNumber) of the previous stage. How can this be achieved? My understanding is output variables are scoped to the same stage and can't be used across-stages.
Trying to pull from stageDependencies:
stages:
- stage: BuildPublish
displayName: "Build & Publish"
jobs:
- job: BuildPublishJob
displayName: "Build & Publish"
steps:
- script: |
echo "Recording MSI version"
echo "##vso[task.setvariable variable=msiVersion;isOutput=true]$(Build.BuildNumber)"
name: MsiVersion
- script: echo $(MsiVersion.msiVersion)
name: echovar
- stage: DeployInstallerTest
displayName: "Deploy Installer Test"
jobs:
- job:
displayName: "Deploy Installer Test"
steps:
- task: AzurePowerShell#5
inputs:
azureSubscription: 'Spektrix Non-Prod'
ScriptType: 'InlineScript'
Inline: |
$msiVersion = stageDependencies.BuildPublish.BuildPublishJob.outputs['MsiVersion.msiVersion']
azurePowerShellVersion: 'LatestVersion'
Fails with:
##[error]The term 'stageDependencies.BuildPublish.BuildPublishJob.outputs[MsiVersion.msiVersion]' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try again.
It changed recently:
stages:
- stage: A
jobs:
- job: JA
steps:
- script: |
echo "This is job Foo."
echo "##vso[task.setvariable variable=doThing;isOutput=true]Yes" #The variable doThing is set to true
name: DetermineResult
- script: echo $(DetermineResult.doThing)
name: echovar
- job: JA_2
dependsOn: JA
condition: eq(dependencies.JA.outputs['DetermineResult.doThing'], 'Yes')
steps:
- script: |
echo "This is job Bar."
#stage B runs if DetermineResult task set doThing variable n stage A
- stage: B
dependsOn: A
jobs:
- job: JB
condition: eq(stageDependencies.A.JA.outputs['DetermineResult.doThing'], 'Yes') #map doThing and check if true
variables:
varFromStageA: $[ stageDependencies.A.JA.outputs['DetermineResult.doThing'] ]
steps:
- bash: echo "Hello world stage B first job"
- script: echo $(varFromStageA)
However, please be aware that stageDependencies is not available in condition at stage level. Of course you can use stageDependencies not only in condition.

Azure Pipeline Need to pass the power shell output to Kubernetes config map seems like Kubernetes task is not reading the variable at all

- task: PowerShell#2
displayName: Save Storage account Secrets to Build Variables
inputs:
azureSubscription:
targetType: 'inline'
script: '$outputs = ConvertFrom-Json $($env:STORAGE); foreach ($output in $outputs.PSObject.Properties) { echo $output.Name; echo $output.Value.value; Write-Host ("##vso[task.setvariable variable=$($output.Name);]$($output.Value.value)");}'
- phase: DEVRelease
dependsOn: Build
queue: Hosted Ubuntu 1604
steps:
- task: Kubernetes#1
displayName: Apply Kubernetes Deployment
inputs:
kubernetesServiceEndpoint:
arguments: "-f conf/deploy_local.yaml"
command: apply
azureSubscription:
azureContainerRegistry:
configMapName: myconfig
forceUpdateConfigMap: true
configMapArguments: --from-literal=myname=$($env:STORAGEACCOUNTNAME1)
Never reads the $env:STORAGEACCOUNTNAME variable
Since the PowerShell task to set the variables is in phase build. You need to add isOutput=true to the setvariable statement. Please check Set a multi-job output variable
"##vso[task.setvariable variable=$($output.name);isOutput=true]$($output.Value)"
I made a little bit changes to your yaml for testing. Please check it out. I have the env variable STORAGE = {'tags':[{'name':'A', 'Value':'1' }, { 'name':'B', 'Value':'2'}]}
phases:
- phase: build
queue: Hosted Ubuntu 1604
steps:
- powershell: |
$outputs = ConvertFrom-Json $($env:STORAGE)
foreach ($output in $outputs.tags) { echo $output.name; echo $output.Value; Write-Host ("##vso[task.setvariable variable=$($output.name);isOutput=true]$($output.Value)");}
name: myvariables
- powershell: |
echo "$(myvariables.A)"
echo "$(myvariables.A)"
- phase: DEVRelease
dependsOn: Build
queue: Hosted Ubuntu 1604
variables:
Da: $[ dependencies.build.outputs['myvariables.A'] ]
Db: $[ dependencies.build.outputs['myvariables.B'] ]
steps:
- powershell: |
echo $(Da)
echo $(Db)
In above script I output variable in phase build by adding isOutput=true to the statement, and give my powershell task a name name: myvariables.
And I refer to the output variable in the next phase DEVRelease by using statement $[ dependencies.{dependent phase name}.outputs['{task name}.{variable name}'] ] and assign it to the Variables.
Then i can successfully get the value in powershell task in phase DEVRelease.

Resources