Get the changes since the last Release in Azure DevOps

A request came by me to:

Get all the commits associated to a specific release, based on the previous succesful release.

The fun thing is, we’re using Azure DevOps.
Easy right?

Well, that’s what I thought, because this information is readily available in the web interface of Azure DevOps.

The details of a Release Pipeline in Azure DevOps, showing the corresponding commits.

As the saying goes:

We do things not because it is easy, but because we thought it would be easy!

This phrase applies to the above request.

When starting out, I figured there must be some environment variable, or endpoint I can invoke to get these details. On the command-line, this is pretty git log command. Turns out, there isn’t. I did ask on Stack Overflow, but it appears the only way to get this data is to invoke several Azure DevOps REST API endpoints.
Not a big problem, but surely more work as expected.

Because of my earlier investigations on integrating with Azure DevOps, I was aware of the REST API service & documentation that is provided on the topic.

The initial implementation

With the documentation available I kind of knew the /_apis/release/releases would be my starting point.
From with the response of this endpoint you should be able to identify the current, and previous releases. In my case, I want to get the previous succesful release that had a specific stage in a succesful state. Quite possible with the response that you get.
Now that you know the previous release identifier, the changes endpoint can be invoked, taking the release id of the current and previous run.

Below is the script I have used originally. This script works fine, but needs a small nuance.

# When running this script locally, provide it with your own PAT.
$token = ""
$headers = @{}

if (-not $token) {
    # This block is using the Azure DevOps variables available in the pipeline
    $headers["Authorization"] = "Bearer $($env:SYSTEM_ACCESSTOKEN)"
    $adoReleaseManagerOrgEndpoint = "$($env:SYSTEM_COLLECTIONURI)"
    $projectName = "$($env:BUILD_PROJECTNAME)"
    $releaseDefinitionId = $($env:RELEASE_DEFINITIONID)
    $currentReleasedId = $($env:RELEASE_RELEASEID)
} else {
    # This block is used for local testing.
    # The $releaseDefinitionId and $currentReleasedId might need to be changed, depending on what you want to do.
    $encodedToken = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes(":$($token)"))
    $headers["Authorization"] = "Basic $encodedToken"

    $adoReleaseManagerOrgEndpoint = "https://vsrm.dev.azure.com/myOrganization/"   # SYSTEM_COLLECTIONURI
    $projectName = "myProject"                                                     # BUILD_PROJECTNAME
    $releaseDefinitionId = 123                                                     # RELEASE_DEFINITIONID
    $currentReleasedId = 45678                                                     # RELEASE_RELEASEID
}

# The Azure DevOps endpoint to retrieve all releases for a given release definition.
# https://learn.microsoft.com/en-us/rest/api/azure/devops/release/releases/list
$latestReleasesEndpoint = "$($adoReleaseManagerOrgEndpoint)$($projectName)/_apis/release/releases?api-version=6.0&`$expand=environments&definitionId=$($releaseDefinitionId)"
Write-Host "Invoking: $latestReleasesEndpoint"
$releasesResponse = Invoke-RestMethod -Uri $latestReleasesEndpoint -Headers $headers -Method Get
Write-Host "Found $($releasesResponse.count) releases."

# Filtering the releases to only include the ones that have a `MySpecificStage` environment and that have succeeded.
$filteredReleases = $releasesResponse.value | Where-Object {
    $_.environments -ne $null -and 
    ($_.environments | Where-Object {
        $_.name -eq "MySpecificStage" -and
        $_.status -eq "succeeded"
    })
}

Write-Host "Found $($filteredReleases.count) releases with a MySpecificStage environment that succeeded."
Write-Host "Current release - ID: $($currentReleasedId)"
$previousSuccesfulReleaseId = ""

# Take the identifier from the previous release.
$filteredReleases | Select-Object -First 1 -Skip 1 | ForEach-Object {
    $previousSuccesfulReleaseId = $_.id
    $name = $_.name
    Write-Host "Previous succeeded release - ID: $previousSuccesfulReleaseId, Name: $name"
}

# The Azure DevOps endpoint to retrieve all changes between two releases.
# This endpoint for releases specifically IS NOT documented, but the documentation for the builds endpoint is very similar.
# For Builds: https://learn.microsoft.com/en-us/rest/api/azure/devops/build/builds/get-changes-between-builds
$changesBetweenReleaseEndpoint = "$($adoReleaseManagerOrgEndpoint)$($projectName)/_apis/Release/releases/$($currentReleasedId)/changes?baseReleaseId=$($previousSuccesfulReleaseId)"
Write-Host "Invoking: $changesBetweenReleaseEndpoint"
$changesResponse = Invoke-RestMethod -Uri $changesBetweenReleaseEndpoint -Headers $headers -Method Get

Write-Host "Found $($changesResponse.count) changes."
$output = ""
# Retrieve all the titles from the changes
$changesResponse.value | ForEach-Object {
    $output += $_.message + " | "
    Write-Host $_.message
}

Write-Host "##vso[task.setvariable variable=changesBetweenReleases;]$output"

This returns a nice list of all commits, in my case squashed commits from PRs, in the output showing all the detail necessary for a report I was asked to create.

The follow-up implementation

As mentioned, the above script works fine and was used for a couple of weeks.
A change was made to this specific release pipeline. Where it first only had a Build-artifact, it now also contains Git-artifacts (to multiple repositories). Suddenly the script wasn’t working anymore and got the following error:

VS402866: Not able to compare releases of artifact types Build and Git.

It wasn’t very clear to me at first, but the big change in this pipeline was the addition of the new artifacts. Therefore, my conclusion is the changes endpoint doesn’t work when having multiple artifacts.

To get the script working again, I’m now retrieving the details of the current & previous release, using the API. In the list of artifacts, I’m searching for the artifact with an alias having the same name of our repository. This artifact contains the version id (Git SHA) of the source for the release. Doing this for both the current and previous release gets me the commits necessary to do a git log, so I did.

Function GetChangesWhenRepositoryArtifactIsUsed {
    $nameOfRepositoryArtifact = "_MyRepositoryArtifactName"
    $remoteName = "origin"
    $branchToGetChangesFrom = "main"

    # Get the Source Version of the release run
    $detailsForCurrentReleaseEndpoint = "$($adoReleaseManagerOrgEndpoint)$($projectName)/_apis/Release/releases/$($currentReleasedId)"
    Write-Host "Invoking: $detailsForCurrentReleaseEndpoint"
    $detailsForCurrentRelease = Invoke-RestMethod -Uri $detailsForCurrentReleaseEndpoint -Headers $headers -Method Get

    $sourceArtifactForCurrentRelease = $detailsForCurrentRelease.artifacts | Where-Object { $_ -and $_.alias -and $_.alias -eq $nameOfRepositoryArtifact } | Select-Object -First 1
    $sourceVersionForCurrentRelease = $sourceArtifactForCurrentRelease.definitionReference.version.id

    # Get the Source Version of the previous release run ($previousSuccesfulReleaseId)
    $detailsForPreviousSuccessfulReleaseEndpoint = "$($adoReleaseManagerOrgEndpoint)$($projectName)/_apis/Release/releases/$($previousSuccesfulReleaseId)"
    Write-Host "Invoking: $detailsForPreviousSuccessfulReleaseEndpoint"
    $detailsForPreviousSuccessfulRelease = Invoke-RestMethod -Uri $detailsForPreviousSuccessfulReleaseEndpoint -Headers $headers -Method Get

    $sourceArtifactForPreviousRelease = $detailsForPreviousSuccessfulRelease.artifacts | Where-Object { $_ -and $_.alias -and $_.alias -eq $nameOfRepositoryArtifact } | Select-Object -First 1
    $sourceVersionForPreviousRelease = $sourceArtifactForPreviousRelease.definitionReference.version.id
    
    # If the Git repository is not found in the artifacts, search for the Build with a matching name, it also contains a source version.
    if(-not $sourceVersionForPreviousRelease) {
        $nameOfBuild = "_MyReleaseBuild"
        $sourceArtifactForPreviousRelease = $detailsForPreviousSuccessfulRelease.artifacts | Where-Object { $_ -and $_.alias -and $_.alias -eq $nameOfBuild } | Select-Object -First 1
        $sourceVersionForPreviousRelease = $sourceArtifactForPreviousRelease.definitionReference.sourceVersion.id    
    }

    # Get the changes between the two source versions
    Write-Host "Searching for the changes between commit '$sourceVersionForPreviousRelease' and '$sourceVersionForCurrentRelease'."

    # Need to create a Git repository, before the git commands can be invoked over here.
    git init
    # The $repositoryUrl can be retrieved from an environment variable. The name of this variable differs on how you named the artifact.
    git -c http.extraheader="AUTHORIZATION: Bearer $($env:SYSTEM_ACCESSTOKEN)" remote add $remoteName $repositoryUrl
    # Use the `--filter` over here, so files aren't downloaded. This makes the call much more efficient (https://github.blog/2020-12-21-get-up-to-speed-with-partial-clone-and-shallow-clone/#user-content-partial-clone)
    git -c http.extraheader="AUTHORIZATION: Bearer $($env:SYSTEM_ACCESSTOKEN)" fetch --filter=blob:none
    $output = git log "$remoteName/$branchToGetChangesFrom" --pretty=oneline "$sourceVersionForPreviousRelease..$sourceVersionForCurrentRelease"

    Write-Host "Changes between releases: $output"
    Write-Host "##vso[task.setvariable variable=changesBetweenReleases;]$output"
}

That’s it, that’s what I came up with in the end.
If you think this can be done easier, or see some issues in my solution above, please let me know. Or if you’ve solved a similar request in a different fashion, I’m also interested in learning how it was done.


Share

comments powered by Disqus