How to gain control over your Pull Request in Azure DevOps in 5 steps
Updated: Jul 15
By leveraging Azure Functions, webhooks, and pull request configurations, you can efficiently validate branches across multiple repositories without the need for separate pipelines. Let’s learn how.
Azure DevOps is a powerful cloud-based platform that offers a wide range of development tools and services. Nevertheless, when it comes to running pipelines across multiple repositories, Azure DevOps has certain limitations that make it cumbersome to perform build validations on specific branches existing in multiple repositories. But fear not! We have a solution that will save you time and effort.
In this guide, we'll take you through the steps to set up this solution. You'll learn:
Following these steps will streamline your build validation process and make your Azure DevOps workflows a breeze.
What you’ll need
Here you have the magic ingredients:
1 Azure DevOps Account.
1 Webhook.
1 Azure Function.
1 Token.
To achieve our goal successfully in this lab, we’ll follow a series of steps.
Step # 1: Create a token
First of all, we must create a Token to be used in an Azure Function. This function will be triggered whenever a Pull Request is created, thanks to the webhook that connects Azure Function and Azure DevOps. (Don't worry, it's much simpler than it sounds) Let's get started by following these instructions:
Log in to your Azure DevOps account.
Navigate to your profile settings by clicking on your profile picture or initials in the top-right corner of the screen.
From the dropdown menu, select "Security".
In the "Personal access tokens" section, click on "New Token".
Provide a name for your token to identify its purpose.
Choose the desired organization and set the expiration date for the token.
Under "Scope", select the appropriate level of access needed for your token. For example, if you only need to perform actions related to build and release pipelines, choose the relevant options.
Review and confirm the settings.
Once the token is created, make sure to copy and securely store it. Note that you won't be able to view the token again after leaving the page. So be careful!
You can now use this token in your Azure Function or other applications to authenticate and access Azure DevOps resources.
Step # 2: Prepare the Azure Function
Click on the "Create a resource" button (+) in the top-left corner of the portal.
In the search bar, type "Function App" and select "Function App" from the results.
Click on the "Create" button to start the creation process.
In the "Basics" tab, provide the necessary details:
Subscription: Select your desired subscription.
Resource Group: Select a name for the Resource Group.
Function App name: Enter a unique name for your function app.
Runtime stack: Choose .NET
Region: Select the region closest to your target audience.
Click on the "Next" button to proceed to the "Hosting" tab.
Configure the hosting settings:
Operating System: Windows
Plan type: Select the appropriate plan type (Consumption, Premium, or Dedicated).
Storage account: Create a new storage account or select an existing one.
Click on the "Review + Create" button to proceed.
Review the summary of your configuration, and if everything looks good, click on the "Create" button to create the Azure Function.
The deployment process may take a few minutes. Once it's completed, you'll see a notification indicating that the deployment was successful.
Navigate to the newly created and change the code for the following one (.NET code):
using System;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using Newtonsoft.Json;
# Add your PAT (Token)
private static string pat = "";
public static async Task<HttpResponseMessage> Run(HttpRequestMessage req, TraceWriter log)
{
try
{
log.Info("Service Hook Received.");
// Get request body
dynamic data = await req.Content.ReadAsAsync<object>();
log.Info("Data Received: " + data.ToString());
// Get the pull request object from the service hooks payload
dynamic jObject = JsonConvert.DeserializeObject(data.ToString());
// Get the pull request id
int pullRequestId;
if (!Int32.TryParse(jObject.resource.pullRequestId.ToString(), out pullRequestId))
{
log.Info("Failed to parse the pull request id from the service hooks payload.");
};
// Get the pull request title
string pullRequestTitle = jObject.resource.title;
// Get the branch source name
string branchSourcetRefName = jObject.resource.sourceRefName;
// Get the branch target name
string branchTargetRefName = jObject.resource.targetRefName;
// Get the repository name
string repositoryName = jObject.resource.repository.name;
// Get the project name
string projectName = jObject.resource.repository.project.name;
// Get the repository url
string url = jObject.resource.repository.url;
// Get the organization name
string organizationName = url.Split('/')[3];
log.Info("Service Hook Received for PR: " + pullRequestId + " " + branchTargetRefName);
PostStatusOnPullRequest(pullRequestId, ComputeStatus(branchTargetRefName, branchSourcetRefName), repositoryName, projectName, organizationName);
return req.CreateResponse(HttpStatusCode.OK);
}
catch (Exception ex)
{
log.Info(ex.ToString());
return req.CreateResponse(HttpStatusCode.InternalServerError);
}
}
private static void PostStatusOnPullRequest(int pullRequestId, string status, string repositoryName, string projectName, string organizationName )
{
string Url = string.Format(
@"https://dev.azure.com/{0}/{1}/_apis/git/repositories/{2}/pullrequests/{3}/statuses?api-version=4.1",
organizationName,
projectName,
repositoryName,
pullRequestId);
using (HttpClient client = new HttpClient())
{
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(
ASCIIEncoding.ASCII.GetBytes(
string.Format("{0}:{1}", "", pat))));
var method = new HttpMethod("POST");
var request = new HttpRequestMessage(method, Url)
{
Content = new StringContent(status, Encoding.UTF8, "application/json")
};
using (HttpResponseMessage response = client.SendAsync(request).Result)
{
response.EnsureSuccessStatusCode();
}
}
}
private static string ComputeStatus(string branchTargetRefName, string branchSourcetRefName)
{
string state = "succeeded";
string description = "Ready for review";
# Change the branches, in the source branch please indicate who will be the branch allowed to make PR and in the target to which target it can.
if (branchSourcetRefName.ToLower().Contains("ADD SOURCE BRANCH") && (branchTargetRefName.ToLower().Contains("ADD TARGET BRANCH")))
{
state = "failed";
description = "Invalid PullRequest SurceBranch.";
}
return JsonConvert.SerializeObject(
new
{
State = state,
Description = description,
TargetUrl = "https://dev.azure.com/",
# The context must be equal to the API check that we will see later.
Context = new
{
Name = "check",
Genre = "error"
}
});
}
Step # 3: Configure Azure DevOps
1. Open your Azure DevOps account and navigate to your project.
2. Go to the project settings by clicking on the gear icon in the bottom left corner.
3. In the project settings, select the "Service Hooks" option.
4. On the Service Hooks page, click on the "+ Add Subscription" button to create a new webhook.
5. Choose the service to connect with. In this case, it will be Webhook!
6. Select the event that will trigger the webhook. Created Pull Request is a good one for this case.
7. Configure the details of the webhook, we will take the URL of the Webhook from the Azure Function. I will leave a little snapshot here to see what I’m referring for:
8. The webhook will fail, but don’t worry, it’s an expected behavior. The Azure Function needs a real case of testing for work.
9. Save the subscription.
10. You now have a webhook set up in Azure DevOps! It will trigger whenever the specified event occurs, allowing you to integrate external services or perform custom actions based on those events.
Step # 4: Configure Pull Request Protection Policy
Open Azure DevOps and navigate to your project.
In the Left bottom, click on your profile picture and select "Project settings" from the dropdown menu.
In the project settings, click on "Repositories".
In the repository settings, go to the "Branch policies" tab.
Click on the "+ Add policy" button to add a new policy.
Select the branch that you want to protect.
From the list of available policies, choose "Status check".
Configure the settings for the status check policy (Remember that the name of our status is “check” and the genre is “error” If you select for the status check another name or genre, the solution will fail)
Step#5. Verify the Functionality
Create a Pull Request from a non-desire branch, and it will fail as you can see here:
Create a Pull Request from a desired Branch and It will succeed!
In case of failure, the Pull Request will not be allowed to continue, it will not only give advice, it will block the entire Pull Request.
Final Thoughts
Since Azure DevOps doesn’t allow us to use Build Validation in Cross Repositories because a pipeline has to be in the same repository that it runs, this solution is a great option to avoid overwork by creating a lot of YAML and pipelines in different repositories if you want to block the Pull Request. I hope you enjoyed it and hope it helps!
Mauro Meluso
Cloud Engineer
Teracloud
If you want to know more about security, we suggest checking Streamlining Security with Amazon Security Hub: A where to start Step-by-Step Guide If you are interested in learning more about our TeraTips or our blog's content, we invite you to see all the content entries that we have created for you and your needs.