Automate pull request descriptions in Azure DevOps with Azure OpenAI

In this blog, you will learn how to leverage Azure OpenAI’s large language models to generate pull request descriptions based on the changes in your code.

The goal is to create seamless automation that summarises the code changes in a pull request using natural language. The text generated by the large language model (LLM) is placed in the description field of the pull request.

To leverage large language models, we will use Azure OpenAI. For a frictionless experience, we will integrate Azure DevOps build validations. A build validation is an Azure Pipeline that runs when a pull request is created or updated. The only action required from the developer is to create a pull request, which triggers the process. The build validation will use predefined Azure DevOps variables to enable frictionless automation.

Below is a high-level overview of the process:

Overview of the process
  1. An actor creates a pull request.
  2. The pull request creation automatically triggers the configured build validation.
  3. The build validation Azure Pipeline calls the Chat Completion Azure OpenAI API with the data.
  4. The LLM creates the description based on the prompts from step 3. Human readable natural text is returned to the Azure Pipeline.
  5. The Azure Pipeline calls the Azure DevOps API to set the pull request description.

To update pull request descriptions, the service principal (service connection) requires the following permissions and configuration:

  • The service principal requires a Basic Azure DevOps licence, as it interacts with code (Azure Repos).
  • Contributor permissions within the project where the pull requests are created.

This blog does not cover the deployment of Azure OpenAI or the deployment of the large language model. If you are looking for an Azure Bicep template to deploy Azure OpenAI, see Azure Verified Modules.

Azure AI Foundry is the platform that enables the deployment of large language models, including OpenAI’s GPT-4o, o1, and many more. For this blog, the model used to generate pull request descriptions is GPT-4o.

In Azure AI Foundry, the deployment looks like this:

The AI Foundry portal, which shows that a model is deployed

An Azure OpenAI key is required to authenticate with the APIs. The key can be found in the Azure Portal:

Azure OpenAI key found at “keys and endpoint”

To automate pull request descriptions, three PowerShell functions and an orchestrator Azure Pipeline are used:

• Set-PullRequestDescription.ps1

• Invoke-PullRequestGeneration.ps1

• Get-Repository.ps1

• azure-pipelines.yml (build validation pipeline)

The function below gathers information about the repository by invoking Get-Repository, and it also calls Invoke-PullRequestGeneration. More details on these functions are covered later in this blog post.

Finally, it makes a PATCH API call to Azure DevOps with the newly generated description in natural language, produced by the Azure OpenAI model.

function Set-PullRequestDescription {
param (
[parameter(Mandatory)]
[string]
$SourceBranch,
[parameter(Mandatory)]
[string]
$TargetBranch,
[parameter(Mandatory)]
[string]
$PullRequestId,
[parameter(Mandatory)]
[string]
$ProjectName,
[parameter(Mandatory)]
[string]
$RepositoryName,
[parameter(Mandatory)]
[string]
$OrgName,
[parameter(Mandatory)]
[string]
$OpenAIKey
)
$apiUrl = "https://dev.azure.com/$OrgName/$ProjectName/_apis/git/repositories/$RepositoryName/pullRequests/$($PullRequestId)?api-version=7.1"
$formattedSourceBranchName = $SourceBranch -replace "refs/heads/", ""
Get-Repository -ProjectName $ProjectName -RepositoryName $RepositoryName -OrgName $OrgName -SourceBranch $formattedSourceBranchName
$newDescription = Invoke-PullRequestDescriptionGeneration -SourceBranch $formattedSourceBranchName -TargetBranch $TargetBranch -OpenAIKey $OpenAIKey
$body = @{
description = $newDescription
} | ConvertTo-Json
$headers = @{
Authorization = "Bearer $(Get-AzAccessToken | Select-Object -ExpandProperty Token)"
"Content-Type" = "application/json"
}
Invoke-RestMethod -Uri $apiUrl -Method PATCH -Headers $headers -Body $body
}

This function clones the Azure Repo where the pull request is created. When a checkout is performed during a pipeline run, it is in a “detached HEAD” state, meaning the checkout is done on a commit instead of a branch.

After cloning the Azure Repo, we gain access to both the main (target) branch and the feature (source) branch.

function Get-Repository {
param (
[parameter(Mandatory)]
[string]
$ProjectName,
[parameter(Mandatory)]
[string]
$RepositoryName,
[parameter(Mandatory)]
[string]
$OrgName,
[parameter(Mandatory)]
[string]
$SourceBranch
)
git clone "https://$(Get-AzAccessToken | Select-Object -ExpandProperty Token)@dev.azure.com/$($OrgName)/$($ProjectName)/_git/$($RepositoryName)"
Set-Location $RepositoryName
git checkout $SourceBranch
}

This function calls the Chat Completion API in Azure OpenAI. However, some configuration is required before making the API call.

function Invoke-PullRequestDescriptionGeneration {
param (
[parameter(Mandatory)]
[string]
$SourceBranch,
[parameter(Mandatory)]
[string]
$TargetBranch,
[parameter(Mandatory)]
[string]
$OpenAIKey
)
$openai = @{
api_key = $OpenAIKey
api_base = 'https://openai-devops-jlo.openai.azure.com/'
api_version = '2024-08-01-preview'
name = 'openai-devops-jlo-gpt4o'
}
# Completion text
$messages = @()
$messages += @{
role = 'system'
content = 'Start with an overall summary of the changes before summarizing the git log content using bullet points. Combine the commits into one set of bulletpoints. Make sure the summarization is clear and easy to read. '
}
$messages += @{
role = 'user'
content = $(git log "$TargetBranch..$SourceBranch" -p)
}
# Header for authentication
$headers = [ordered]@{
'api-key' = $openai.api_key
}
# Adjust these values to fine-tune completions
$body = [ordered]@{
messages = $messages
} | ConvertTo-Json
$url = "$($openai.api_base)/openai/deployments/$($openai.name)/chat/completions?api-version=$($openai.api_version)"
$response = Invoke-RestMethod -Uri $url -Headers $headers -Body $body -Method Post -ContentType 'application/json'
return $response.choices.message.content
}

The $openai variable stores key configuration details:

ParameterDescription
API_KEYThe secret key used to authenticate with Azure OpenAI
API_BASEThe URL of Azure OpenAI
API_VERSIONThe version of the deployment model
nameThe name of the deployment model, which can be found in the Azure AI Foundry portal

Completion text

The completion text contains two different roles:

RoleDescription
System roleThe system role is used to provide initial instructions to the model. For example, you can give a brief description on how the assistant should behave.
User roleThe user role is for sending questions or context to the assistant.

In this case, the system message is:

Start with an overall summary of the changes before summarizing the git log content using bullet points. Combine the commits into one set of bulletpoints. Make sure the summarization is clear and easy to read.

For the user message, the following command is used:

git log "$TargetBranch..$SourceBranch" -p

This Git command displays the commit log history between two branches. The -p option includes the code changes, which is useful as additional context for the large language model. This allows the model to access both the commit messages and the actual code changes.

The command output looks like this:

Output of the git log "$TargetBranch..$SourceBranch" -p command

Finally, the headers for authentication and the message body with the prompts are set, and the Chat Completion API is called. The output is then returned and used in the Set-PullRequestDescription function to update the Azure DevOps pull request description.

The Azure Pipeline uses the AzurePowerShell task, leveraging a service connection with the necessary permissions to make API calls to Azure DevOps. The task calls the Set-PullRequestDescription function, which uses predefined Azure DevOps variables as input parameters. The OpenAI key is loaded from an Azure DevOps library.

trigger: none
pool:
vmImage: ubuntu-latest
variables:
- group: vg-openai
steps:
- task: AzurePowerShell@5
displayName: 'Generate PR Description using GPT4o'
inputs:
azureSubscription: sc-devops-openai
ScriptType: 'InlineScript'
azurePowerShellVersion: LatestVersion
pwsh: true
Inline: |
. ./Invoke-PullRequestDescriptionGeneration.ps1
. ./Get-Repository.ps1
. ./Set-PullRequestDescription.ps1
Set-PullRequestDescription `
-SourceBranch $(System.PullRequest.SourceBranch) `
-TargetBranch $(System.PullRequest.TargetBranchName) `
-PullRequestId $(System.PullRequest.PullRequestId) `
-ProjectName $(System.TeamProject) `
-RepositoryName $(Build.Repository.Name) `
-OrgName ("$(System.CollectionUri)" -split "/" | Select-Object -index 3) `
-OpenAIKey $(openai-key)

When a pull request is created, the build validation will automatically run. During this run, the code changes are analysed, and the model is instructed to start with a summary of the changes. After that, a summary of the Git log must be generated in bulletpoints:

The description is automated via the PR Description Automation build validation

Let’s take a look at some code changes. The pull request description states: ‘Previous sample content replaced with a motivational message: “You are awesome!”’ which is correct. The README has been updated as follows:

README change

Another change is that scripts have been moved, and their references are now in the azure-pipelines.yml file:

Scripts moved and references updated

This is just one possible format for automating pull request descriptions. If you want to adjust the format, you can customise the prompt to your preference. Keep in mind that it may take a few attempts to find the perfect summary format.

By leveraging Azure OpenAI, Azure DevOps, and PowerShell, you can automate the generation of pull request descriptions, saving time and reducing the manual effort involved in documenting changes.

Leave a comment