Azure DevOps - Change Release variable during execution
azure-devops releases

Changing and persisting variable values in Releases can be done using DevOps REST API and PowerShell tasks. This post describes how to do it.
June 8, 2020

Azure DevOps (ADO) has nice hierarchy of variables and dynamic values - you can define high-level variable groups and then specific variables which can be modified before starting a release. But what about being a little more dynamic and changing pipeline variables during execution? To be available for all stages?

Turns out that sharing information between stages in release pipeline is not as simple as it could be.

This post talks about the "classic" definition of pipelines where Builds and Releases are separate. YAML pipelines have more options of sharing data between stages.

tl;dr

To persist variable changes in a Release run (not definition):

  1. Set Project Collection Build Service permissions to Manage releases.
  2. Check the Allow scripts to access the OAuth token box.
  3. Use in PowerShell:
$url = "$(System.TeamFoundationServerUri)$(System.TeamProjectId)/_apis/Release/releases/$(Release.ReleaseId)?api-version=6.0-preview.8"

Write-Host "URL: $url"
$pipeline = Invoke-RestMethod -Uri $url -Headers @{
	Authorization = "Bearer $(System.AccessToken)" # Provided by ADO thanks to OAuth checkbox
}

# Change the value of var2 to be persisted in the rest of the run.
$pipeline.variables.var2.value = "World"

# Alternatively create new variable in cases when it's not present.
# $pipeline.variables  | Add-Member -MemberType NoteProperty -Name $name -Value @{value=$env.Value} -Force

$json = @($pipeline) | ConvertTo-Json -Depth 99
Invoke-RestMethod -Uri $url -Method Put -Body $json -ContentType "application/json" -Headers @{Authorization = "Bearer $(System.AccessToken)"}

The problem

The idea is to change a pipeline variable value during release stage execution and then consume this new value in the following stages.

  1. Pipeline variable var2 is set to "Hello".
  2. pre-deployment stage sets var2 to "World".
  3. actual deployment stage reads var2 and gets "World".

Alternatively:

  1. Variable var2 doesn't exist on the pipeline.
  2. pre-deployment stage creates var2 and sets value to "World".
  3. actual deployment stage reads var2 and gets "World".

Before running the pipeline:

Release pipeline variables before execution

If var2 is changed/created in pre-deployment the value will not be available in actual deployment.

Release pipeline with two stages

The solution

Azure DevOps release pipelines offer a way to manipulate running release by using logging commands. One example is changing release name based on information available only during release - such as value of a pipeline variable.

Write-Host "##vso[release.updatereleasename]Deploy Infra - $("$(environment)".ToUpper()) - $(Release.ReleaseId)"

Running this in a PowerShell task will update the release name to something like this:

Relase name set to Deploy Infra - INT24 - 2578

Bash on Linux agents works exactly the same (just echo instead of Write-Hosting).

We initially wanted to solve our problem by using another logging command: ##vso[task.setvariable variable=var2;]World. That unfortunately doesn't work in this case, because variables set by a task are available only in the current stage - all subsequent tasks in "pre-deployment" can read it, but tasks in "actual deployment" can't.

There is a solution to this though: using the Azure DevOps REST API.

To set it up we first need to go to the Security page of the release definition.

Select Security from the release settings menu

And change the permissions of Project Collection Build Service to Allow - Manage releases:

Set Manage releases to Allow

To call the REST APIs we will need an access token. ADO can obtain and surface the token for us when we select the "Allow scripts to access the OAuth token" checkbox at the Agent job level in a particular stage (pre-deployment in this case):

Allow scripts to access the OAuth token check box

And then finally use it in a PowerShell script:

$url = "$(System.TeamFoundationServerUri)$(System.TeamProjectId)/_apis/Release/releases/$(Release.ReleaseId)?api-version=6.0-preview.8"

Write-Host "URL: $url"
$pipeline = Invoke-RestMethod -Uri $url -Headers @{
	Authorization = "Bearer $(System.AccessToken)" # Provided by ADO thanks to OAuth checkbox
}

# Change the value of var2 to be persisted in the rest of the run.
$pipeline.variables.var2.value = "World"

# Alternatively create new variable in cases when it's not present.
# $pipeline.variables  | Add-Member -MemberType NoteProperty -Name $name -Value @{value=$env.Value} -Force

$json = @($pipeline) | ConvertTo-Json -Depth 99
Invoke-RestMethod -Uri $url -Method Put -Body $json -ContentType "application/json" -Headers @{Authorization = "Bearer $(System.AccessToken)"}

Attribution: This script was inspired by a solution presented at StackOverflow :) My colleague Sebastian Bader modified it to work with pipeline runs instead of definitions and made it a bit more sophisticated.

Final result - var2 has the value of World

Found something inaccurate or plain wrong? Was this content helpful to you? Let me know!

šŸ“§ codez@deedx.cz