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.

馃 Check out my previous blog to get more context: Foreach repositories push and create a pull request.

One script to rule them all

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.

Why automate

Should we automate?
Automation has benefits over manual work when exceeding a number of repositories.

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 馃槈.

Export release and archive it

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.

ReleaseExportAndMoveToArchive.ps1
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
# 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 = az account get-access-token --resource 499b84ac-1321-427f-aa17-267ca6975798 --query "accessToken" --output tsv
if ($accessToken -eq $null) {
    exit 1
}
$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 -Method Get -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 -Method Get -ContentType "application/json"
$pipeline.path = "$targetFolderId"

$json = @($pipeline) | ConvertTo-Json -Depth 99

# create serviceName folder
New-Item -ItemType Directory -Force -Path "tmp\$($serviceName.ToLower())"

Function ConvertTo-Yml {
    param (
        [Parameter(Mandatory=$true)]
        [object]$object,
        [Parameter(Mandatory=$true)]
        [string]$Path
    )
    $yml = 'variables:'
    foreach ($variable in $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 ($environment in $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 -Method Put -Body $json -ContentType "application/json"

Write-Host "Release definition '$releaseDefinitionName' moved to folder '$targetFolderId'"

Conclusion

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.