Migrating Classic Azure DevOps Releases to YAML Pipelines
April 17, 2024 路 (updated May 2, 2024) 路 4 min 路 843 words 路 Mart de Graaf
In a previous blog post, we tackled the issue of pushing files and creating pull requests using Powershell. In this blog post, we are going to tackle the issue of migrating classic releases to YAML pipelines in Azure DevOps. We are going to use a script to fetch current classic releases and make yaml files out of it.
This thing is not a one-size-fits-all solution. This script fetches all variables and puts them in variable files for you and puts a yaml file that extends an existing template. The template is let out of scope. The script is a starting point for you to migrate your classic releases to YAML pipelines and might give you ideas and inspiration.
As visualized in the chart above we can easily calculate the time saved by automating the process. When the time of the automation is smaller than the number of repositories times the time to do it manually, we should automate. In this case, we have 60 repositories and the time to do it manually is 1 hour per repository. I set the time to automate to 32 hours. This is the time I spent on the script. The time to do it manually would be 60 hours. So we saved 28 hours by automating the process. And we can reuse the script for future migrations. Or now you can too 😉.
In this script, a release is fetched from Azure DevOps using the Azure DevOps REST API. The release is then exported to a yaml file and moved to an archive folder. We can use the output of this script as input for our script to push files and create a pull request.
# Description: This script moves a release definition to a target folder in Azure DevOps# it depends on you being signed in in the Azure cli, that can be done by `az login`# Usage: ReleaseExportAndMoveToArchive.ps1 -releaseDefinitionName "MartService Release" -serviceName "MartService"param([Parameter(Mandatory=$true)][string]$releaseDefinitionName,[Parameter(Mandatory=$true)][string]$serviceName)# Prompt the user to login and get the access token# see https://learn.microsoft.com/en-us/azure/devops/integrate/get-started/authentication/service-principal-managed-identity?toc=%2Fazure%2Fdevops%2Forganizations%2Fsecurity%2Ftoc.json&view=azure-devops#q-can-i-use-a-service-principal-or-managed-identity-with-azure-cli$accessToken=azaccountget-access-token--resource499b84ac-1321-427f-aa17-267ca6975798--query"accessToken"--outputtsvif($accessToken-eq$null){exit1}$orgUrl="https://vsrm.dev.azure.com/MartOrg"$project="MartProject"$targetFolderId="/Archive"$headers=@{"Authorization"=("Bearer {0}"-f$accessToken)"Accept"="application/json"}# Get the release definitions matching the given name$uri="$orgUrl/$project/_apis/release/definitions?api-version=7.0"$definitionsResponse=Invoke-RestMethod-Uri$uri-Headers$headers-MethodGet-ContentType"application/json"Write-Host"Found $($definitionsResponse.count) release definitions"$definition=$definitionsResponse.value|Where-Object{$_.name-eq$releaseDefinitionName}# Move the release definition to the target folder$definition.id=$definition.id-replace":","%3A"# Escape the colon in the definition ID$uri="$($definition.url)?api-version=7.0"Write-Host"$uri"# get the current release definition$pipeline=Invoke-RestMethod-Uri$uri-Headers$headers-MethodGet-ContentType"application/json"$pipeline.path="$targetFolderId"$json=@($pipeline)|ConvertTo-Json-Depth99# create serviceName folderNew-Item-ItemTypeDirectory-Force-Path"tmp\$($serviceName.ToLower())"FunctionConvertTo-Yml{param([Parameter(Mandatory=$true)][object]$object,[Parameter(Mandatory=$true)][string]$Path)$yml='variables:'foreach($variablein$object.PsObject.Properties){$yml+="`n$($variable.Name): $($variable.Value.Value)"}Write-Host$yml$yml|Out-File-FilePath$Path}ConvertTo-Yml$pipeline.variables"tmp\$($serviceName.ToLower())\variables.yml"foreach($environmentin$pipeline.environments){#transform $environment.name to o, t, a or p#Possible inputs: Development, Test, Acceptance, Production$environmentName=$environment.name.ToLower().Substring(0,1)$environmentSuffix=switch($environmentName){"d"{"o"}"t"{"t"}"a"{"a"}"p"{"p"}}ConvertTo-Yml$environment.variables"tmp\$($serviceName.ToLower())\variables-$($environmentSuffix).yml"}$yml="
trigger:
branches:
include:
- main
pool: 'default'
resources:
repositories:
- repository: Pipelines
type: git
name: Pipelines
ref: refs/heads/main
name: `$(Build.DefinitionName)_`$(SourceBranchName)_`$(date:yyyyMd).`$(Rev:r)
variables:
- template: variables.yml
extends:
template: Yml/service.pipeline.yml@Pipelines
parameters:"$yml|Out-File-FilePath"tmp\$($serviceName.ToLower())\$($serviceName.ToLower())-pipeline.yml"# update the release definition with the new path$response=Invoke-RestMethod-Uri$uri-Headers$headers-MethodPut-Body$json-ContentType"application/json"Write-Host"Release definition '$releaseDefinitionName' moved to folder '$targetFolderId'"
We have now migrated our classic releases to YAML pipelines. We can now use the output of this script to push files and create a pull request. This way we can automate the process of updating all our repositories with the same type of change. We have saved time and can now focus on browsing/chilling/netflixing other tasks.