You shall not forget - reminding about pull requests in VSTS #2

In the previous post I presented you how easily we can determine whether a PR has been reviewed or not. Now we'll create a real solution, which will send a notification to a Slack channel containing information about waiting pull request.

Creating an Azure Function

To handle our solution we'll develop a function, which will do following:

  • call VSTS API to check whether there're active PRs
  • filter those PRs, which haven't been reviewed during a specific interval
  • send a message to a Slack channel with specific information regarding forgotten PRs

Now let's create a function. Go to Azure Portal and create a new Function App(if you don't have one). Once created add a function as a TimerTrigger(schedule doesn't matter in this moment). Once you're done, you should get following function:

Now - to authenticate our calls to the VSTS API we'll need a personal access token. To create one go to the instance of VSTS you're going to use and go to Security screen.

On the left you should see Personal access tokens tab. Go there and create a new personal access token, which we'll use in our function.

Once we have a PAT we can write add some code to our function - for this moment we'll try to check whether there're active PRs.

Fetching active PRs from VSTS

This is a simple task - we have to call an API with a generated PAT. Consider following code:

/
using System;
using System.Net;
using System.Net.Http.Headers;

public static async Task Run(TimerInfo myTimer, TraceWriter log)
{
    log.Info($"C# Timer trigger function executed at: {DateTime.Now}");

    try
    {
        var personalaccesstoken = "THIS_IS_YOUR_PAT";

        using (HttpClient client = new HttpClient())
        {
            client.DefaultRequestHeaders.Accept.Add(
                new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));

            client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic",
                Convert.ToBase64String(
                    System.Text.ASCIIEncoding.ASCII.GetBytes(
                        string.Format("{0}:{1}", "", personalaccesstoken))));

            using (HttpResponseMessage response = client.GetAsync(
                        "https://{instance}.visualstudio.com/{project}/_apis/git/repositories/{repository}/pullRequests?api-version=3.0").Result)
            {
                response.EnsureSuccessStatusCode();
                string responseBody = await response.Content.ReadAsStringAsync();
                log.Info(responseBody);
            }
        }
    }
    catch (Exception ex)
    {
        log.Info(ex.ToString());
    }
}

When you replace dummy values with values of your own, you should receive in the Logs window a serialized response containing active pull requests(of course if there's any). So far so good - let's try to find if any requires our attention.

Finding outdated PRs

To find outdate PRs we'll have to change our code a little - for now we have a raw string and are unable to query any of its properties. For the purpose of this post I decided to use a dynamic object, just not to trouble with creating a DTO for a response. To do so just create responseBody variable to:

/
dynamic data = await response.Content.ReadAsAsync<object>();

No we can easily query PRs and find those, which haven't been reviewed during a specific interval(let's say 24 hours). Let's add following code to our function:

/
using (HttpResponseMessage response = client.GetAsync(
			"https://{instance}.visualstudio.com/{project}/_apis/git/repositories/{project}/pullRequests?api-version=3.0").Result)
{
	response.EnsureSuccessStatusCode();
	dynamic data = await response.Content.ReadAsAsync<object>();

	foreach(var pr in data.value) {
		foreach(var reviewer in pr.reviewers) {
			if(reviewer.vote == 0 && (DateTime.Now - DateTime.Parse(pr.creationDate.ToString())).Hours > 24) {
				log.Info($"Reviewer {reviewer.displayName} still hasn't reviewed a PR!");
			}
		}
	}
}

With the above code we're able to find reviewers, which haven't got a chance to take a look and review a pull request. In the last part we'll send this information to a Slack channel.

Notifying in a Slack channel

To send something to a Slack channel we have to set up a webhook integration which is described here. Once you have added a webhook to your channel and got an endpoint, which can be used to send messages to a channel, we can extend our function. Here you have the complete code:

/
using System;
using System.Net;
using System.Net.Http.Headers;

public static async Task Run(TimerInfo myTimer, TraceWriter log)
{
    log.Info($"C# Timer trigger function executed at: {DateTime.Now}");

    try
    {
        var personalaccesstoken = "YOUR_PAT";

        using (HttpClient client = new HttpClient())
        {
            client.DefaultRequestHeaders.Accept.Add(
                new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));

            client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic",
                Convert.ToBase64String(
                    System.Text.ASCIIEncoding.ASCII.GetBytes(
                        string.Format("{0}:{1}", "", personalaccesstoken))));

            dynamic data = null;
            using (HttpResponseMessage response = await client.GetAsync(
                        "https://{instance}.visualstudio.com/{project}/_apis/git/repositories/{repository}/pullRequests?api-version=3.0"))
            {
                response.EnsureSuccessStatusCode();
                data = await response.Content.ReadAsAsync<object>();
            }

            foreach(var pr in data.value) {
                foreach(var reviewer in pr.reviewers) {
                    if(reviewer.vote == 0 && (DateTime.Now - DateTime.Parse(pr.creationDate.ToString())).Hours > 24) {
                        var textToSend = $"Reviewer {reviewer.displayName} still hasn't reviewed a PR!";

                        await client.PostAsync("https://hooks.slack.com/{...}", new StringContent("{\"text\":\"" + textToSend + "\"}"));
                    }
                }
            }
        }
    }
    catch (Exception ex)
    {
        log.Info(ex.ToString());
    }
}

Summary

VSTS and its API give you many flexibility and are really fun to play with. The solution presented here is not ideal - there's still a possibility to improve things. Anyway, I'd like to encourage you to your own experiments with VSTS and different integrations - you'll be surprised what you can achieve with a few lines of code.

 

You shall not forget - reminding about pull requests in VSTS #1

Code reviews are one of the best ways to make sure, that everyone is aligned with recent code changes and allow a team to actually keep the codebase clean and well-maintained. The is one "meh" however - they require a developer to take a look and review changes. This require time and many people tend to postpone and forget about PRs(which I consider a bad practice, which discourage people from selecting different team members as reviewers). I'll show you a really easy way to remind people about waiting pull requests using VSTS REST API and Azure Functions.

Prerequisites

I won't go into details regarding authenticating REST API in VSTS in this post. If you're not sure how can you query it, check my other posts from VSTS category or read VSTS REST API basics.

Finding active pull requests

We'll start from a very easy task - we'll find active pull requests, which might require our attention. To do so we can use following endpoint:

/
GET https://{instance}/DefaultCollection/{project}/_apis/git/repositories/{repository}/pullRequests?api-version={version}[&status={string}&creatorId={GUID}&reviewerId={GUID}&sourceRefName={string}&targetRefName={string}&$top={integer}&$skip={integer}]

It's very flexible and allow you to specify many different parameters to return exactly what you wany. In our case we can simply use following version of this request:

/
https://{instance}.visualstudio.com/{project}/_apis/git/repositories/{repository}/pullRequests?api-version=3.0

It returns either an empty array of PRs(if none is active) or an array of active pull requests. In my case it returns a following result:

/
{
	"value": [{
		"repository": {
			"id": "...",
			"name": "LicznikNET vNext",
			"url": "...",
			"project": {
				"id": "...",
				"name": "LicznikNET vNext",
				"state": "unchanged",
				"visibility": "unchanged"
			}
		},
		"pullRequestId": 1,
		"codeReviewId": 1,
		"status": "active",
		"createdBy": {
			"id": "...",
			"displayName": "Kamil Mrzygłód",
			"uniqueName": "",
			"url": "...",
			"imageUrl": "..."
		},
		"creationDate": "2017-07-12T07:18:36.1561898Z",
		"title": "Merge branch ",
		"description": "Merge branch 'feature/LNETVN-204-data-should-not-be-collected-for-non-existing-locations' into develop",
		"sourceRefName": "refs/heads/develop",
		"targetRefName": "refs/heads/feature/LNETVN-204-data-should-not-be-collected-for-non-existing-locations",
		"mergeStatus": "succeeded",
		"mergeId": "...",
		"lastMergeSourceCommit": {
			"commitId": "...",
			"url": "..."
		},
		"lastMergeTargetCommit": {
			"commitId": "...",
			"url": "..."
		},
		"lastMergeCommit": {
			"commitId": "...",
			"url": "..."
		},
		"reviewers": [{
			"reviewerUrl": "...",
			"vote": 0,
			"id": "...",
			"displayName": "[LicznikNET vNext]\\LicznikNET vNext Team",
			"uniqueName": "...",
			"url": "...",
			"imageUrl": "...",
			"isContainer": true
		}],
		"url": "...",
		"supportsIterations": true
	}],
	"count": 1
}

It provides some information but there's no way to check how long we're waiting for reviewers. Let's try to find something, which will help us here.

Checking votes

Ok, let's try following approach - what if we go to our PR and try to actually mark it as Waiting for the author, as in real scenario?

I changed my PR status to "Waiting for the author"

Now let's query API once more and check results. This is what I got(I removed lines, which were not changed):

/
"reviewers": [{
	"reviewerUrl": "...",
	"vote": -5,
	"id": "...",
	"displayName": "[LicznikNET vNext]\\LicznikNET vNext Team",
	"uniqueName": "...",
	"url": "...",
	"imageUrl": "...",
	"isContainer": true
}, {
	"reviewerUrl": "...",
	"vote": -5,
	"votedFor": [{
		"reviewerUrl": "...",
		"vote": 0,
		"id": "...",
		"displayName": "[LicznikNET vNext]\\LicznikNET vNext Team",
		"uniqueName": "...",
		"url": "...",
		"imageUrl": "...",
		"isContainer": true
	}],
	"id": "...",
	"displayName": "Kamil Mrzygłód",
	"uniqueName": "...",
	"url": "...",
	"imageUrl": "..."
}],

As you can see we got some additional values for reviewers property. What is more interesting here is the vote property, which has changed from to -5. This is what will help us in the next post to make a working solution notifying team members about waiting pull requests - we'll try to find PRs with no votes, which are older than 24 hours and try to send a message to people involved in it, encouraging them to take a look at proposed changes.