Customized pull request status policy using Azure Functions for semantic release PR titles (part 1)
Published: 2023-03-06 21:45:44

---
title: Customized pull request status policy using Azure Functions for semantic release PR titles (part 1)
published: true
date: 2023-03-05 17:50:03 UTC
tags: 
canonical_url: https://www.eriklieben.com/posts/semantic-release-status-check-part-1/
---

In my [previous blog post](https://www.eriklieben.com/posts/semantic-release-nuget/), I showed you how to automate releases using pull request titles for your NuGet packages.

When you use this process less often, or if you have new people onboarding your team, it is beneficial to give some additional help/ insights when they go through the process. This blog post will look at how to build an Azure Function to provide more information about the process during your regular pull request workflow.

<!-- Excerpt End -->
## What will we build

To extend your Azure DevOps Pull Request workflow, you can build a customized workflow by creating an HTTP endpoint. This endpoint will be notified whenever a pull request is created or updated. To set up Azure DevOps to notify the HTTP endpoint, we can use Service Hooks.

The logic behind the HTTP endpoint will perform the check and post back the status to Azure DevOps using a PAT token for authentication.

Let's start with creating the PAT token.

## Creating the PAT (Personal Access Token) token

A personal access token contains your security credentials for Azure DevOps. A PAT identifies you, your accessible organizations, and scopes of access. As such, they're as critical as passwords, so you should treat them the same way. That's why we will store this token in Azure KeyVault later on.

Let's create the token with the required scope. Go to your Azure DevOps environment and click the `user settings` icon next to your profile image in the top right of the window. Pick the option `Personal Access Token`.

![Go to personal access token management](https://www.eriklieben.com/img/posts/semantic-release-status-check/goto-pat-token-creation-light-mode.jpg)

This will open up the `User settings` page on the section `Security` with `Personal access tokens` selected. By default, the PAT tokens screen will show you active tokens and allow you to create a new token. Click the `+ New Token` button to generate the required token.

![Click the new personal access token button](https://www.eriklieben.com/img/posts/semantic-release-status-check/create-new-pat-token-light-mode.jpg)

This will open up the window to generate a personal access token window. Set the title to a new one you will remember; in this case, we will use `semantic-release title validation`. Select the expiration date for the token. This is the period the token will be active; after this date, the token will stop working, and you will need to extend or re-create it. Next, we will specify the scopes that can be accessed using the token. Select `Custom defined` to specify a subset of scopes. In the Section `Code` check the item `Status`. This will give the token access to read and write commit and pull request statuses.

![Create a new personal access token](https://www.eriklieben.com/img/posts/semantic-release-status-check/create-new-pat-token-window-light-mode.jpg)

Click the `Create` button on the bottom of the screen to create the token. A new token will be generated; copy this token, because it won't be shown again. Keep this safe for now, we will later on store this in Azure KeyVault.

![Copy personal access token](https://www.eriklieben.com/img/posts/semantic-release-status-check/copy-pat-light-mode.jpg)

## Creating the Azure Function

Open Visual Studio and create an Azure Function; in the project creation wizard, select `.NET 7.0 Isolated` as the Function worker option. This will generate a project for us with a default `Function1` class - rename this class to `SemanticReleaseConventionFunctions`.

### Ingestion of the PAT token secret

To send back status information to Azure DevOps we require access to the PAT token from our code. To supply this token, we will create a record called `ApplicationSettings` and add one property to called PatToken to the record.

```csharp
public record ApplicationSettings
{
    public string? PatToken { get; set; }
}
```

Next, we set up the startup logic to load the settings from `local.settings.json` file. Open up the `Program.cs` file and add the `ConfigureServices` method and the code to setup the application settings configuration (line 8 till 14 below).

```csharp
using AzureDevopsSemanticRelease;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

var host = new HostBuilder()
    .ConfigureFunctionsWorkerDefaults()
    .ConfigureServices(s =>
    {
        s.AddOptions<ApplicationSettings>().Configure<IConfiguration>((settings, configuration) =>
        {
            configuration.GetSection(nameof(ApplicationSettings)).Bind(settings);
        });
    })
    .Build();

host.Run();
```

Add the setting for the PAT token to your `local.settings.json` and paste the token you've generated above. **Your `local.settings.json` file should not be checked into source control, because it contains a secret.** We will temporarily keep the token in this file to simplify debugging our code.

```json
{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
    "ApplicationSettings:PatToken": "REPLACE_WITH_YOUR_PAT_TOKEN"
  }
}
```

We can now retrieve the application settings in our Function class by letting the dependency injection process inject it into our constructor. Create a constructor for your class and add the parameter `IOptions<ApplicationSettings> settings` and store it's value in a private field.

```csharp
public class SemanticReleaseConventionFunctions
{
    private readonly string pat;

    public SemanticReleaseConventionFunctions(IOptions<ApplicationSettings> settings)
    {
        pat = settings.Value.PatToken ?? throw new ConfigurationException("Unable to find setting 'ApplicationSettings:PatToken'");
    }
}

public class ConfigurationException : Exception
{
    public ConfigurationException(string message) : base(message) { }
}
```

### Create the HTTP trigger [#](https://www.eriklieben.com/posts/semantic-release-status-check-part-1/#create-the-http-trigger)

We're now ready to add the HTTP endpoint and the logic to validate the pull request title. Create an Azure Function for the route `validate-pr-title` that accepts POST requests in the `SemanticReleaseConventionFunctions` class.

- On line 5 and 6 we retrieve the body from the request and parse the json to our object structure to understand the message.
- Line 8 takes this object and generates the status message and submits it to the Azure DevOps rest API endpoint.
- On line 10 till 12 we create a valid response for the webhook.

```csharp
[Function(nameof(ValidatePRTitle))]
public async Task<HttpResponseData> ValidatePRTitle(
    [HttpTrigger(AuthorizationLevel.Function, "post", Route = "validate-pr-title")] HttpRequestData req)
{
    var body = await req.ReadAsStringAsync() ?? throw new MessageProcessingException("Unable to read body");
    var pr = JsonSerializer.Deserialize<Message>(body) ?? throw new MessageProcessingException("Unable to parse body");

    await PostStatusOnPullRequest(GenerateStatusMessage(pr.resource.title, pr.resource.repository.webUrl), pr.resource.url);

    var response = req.CreateResponse(HttpStatusCode.OK);
    response.Headers.Add("Content-Type", "text/plain; charset=utf-8");
    return response;
}

public class MessageProcessingException : Exception
{
    public MessageProcessingException(string message) : base(message) { }
}
```

In addition, we need a few objects to store the data structure in which Azure Devops sends the event.

```csharp
public record Message
{
    public PullRequestResource resource { get; set; } = new();
}

public record PullRequestResource
{
    public string title { get; set; }

    public int pullRequestId { get; set; }

    public string url { get; set; }

    public PullRequestRepository repository { get; set; } = new();

}

public record PullRequestRepository
{
    public string webUrl { get; set; }
}
```

### Create the GenerateStatusMessage method [#](https://www.eriklieben.com/posts/semantic-release-status-check-part-1/#create-the-generatestatusmessage-method)

In the generate status method, we will generate the message to post to Azure DevOps based on the title text. We return a `succeeded` state by default, and if it's not in the right format the `pending` state. In both cases, we return the generated description, either providing the error description or the type of release it will create.

```csharp
private static string GenerateStatusMessage(string pullRequestTitle, string repositoryUri)
{
    string state = "succeeded";
    var (valid, description) = ParseTitle(pullRequestTitle);
    if (!valid)
    {
        state = "pending";
        description = "PR title is not following PR title conventions (<type>(<scope>): or <type>:); click me for more information.";
    }

    return JsonSerializer.Serialize(
        new
        {
            State = state,
            Description = description,
            TargetUrl = $"{repositoryUri}?path=/docs/release-instructions.md&version=GBmain&_a=preview",

            Context = new
            {
                Name = "PullRequest-SemanticRelease-Commit-Title",
                Genre = "pr-azure-function-ci"
            }
        });
}
```

In the `ParseTitle` method we parse the title and process it to define the type of release. On line 13 to 24 you can define your rules around releases.

```csharp
private static (bool, string) ParseTitle(string title)
{
    Match m = SemanticReleaseRegex().Match(title);

    string message = "No release will be created.";

    if (m.Success)
    {
        var tag = m.Groups["tag"].Value;
        var scope = m.Groups["scope"].Value;

        string? releaseType = null;
        switch (tag)
        {
            case "feat":
                releaseType = "minor";
                break;
            case "docs" when scope == "README":
            case "perf":
            case "fix":
            case "deps":
                releaseType = "patch";
                break;
        }

        message = string.IsNullOrEmpty(releaseType) ?
              $"PR title is according to PR title conventions for semantic release and will _not_ perform a release."
            : $"PR title is according to PR title conventions for semantic release and will perform a {releaseType} release";
    }

    return (m.Success, message);
}

[GeneratedRegex("^(?<tag>docs|perf|fix|deps|feat|style|test|refactor)\\(?(?<scope>.*?)\\)?:(?<description>.*)", RegexOptions.IgnoreCase)]
private static partial Regex SemanticReleaseRegex();
```

We defined the following rules below: If the tag is `feat` we will expect a minor release. When the tag is `docs` and the scope is `README` we will expect a patch release and when it is in any other scope but uses the tag `docs` no release will be created. The tags `perf`, `fix`, and `deps` will expect a patch release. All the other tags or scopes will not expect a release.

Line 26 to 28 will generate the correct description based upon the release type.

Line 34 and 35 contain a regular expression parsing the title text. The `GeneratedRegex` was introduced in .NET 7 that uses Roslyn enabled source generators to provide the regex implementation; for more information see [.NET regular expression source generators](https://learn.microsoft.com/en-us/dotnet/standard/base-types/regular-expression-source-generators).

The regex will roughly perform the following:

We expect the title string to start with a tag:

- docs
- perf
- fix
- deps
- feat
- style
- test
- refactor

Then optionally, we would detect a scope and the rest as the description (in our sample, we don't use the description).

Next, we look at the last bit, sending the generated response to Azure DevOps in the `PostStatusOnPullRequest` method. 

We use the pat token previously retrieved from the settings, convert it to a base64 string, and add it as basic authorization header. Finally, we send the message generated in the `GenerateStatusMessage` method to the Azure DevOps REST API and validate the response (The `EnsureSuccessStatusCode` method throws an Exception if the status code is not a successful one).

```csharp
private async Task PostStatusOnPullRequest(string message, string prUri)
{
    using HttpClient client = new();
    client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
    client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes($":{pat}")));

    var request = new HttpRequestMessage(new HttpMethod("POST"), $"{prUri}/statuses?api-version=4.1")
    {
        Content = new StringContent(message, Encoding.UTF8, "application/json")
    };

    using HttpResponseMessage response = await client.SendAsync(request);
    response.EnsureSuccessStatusCode();
}
```

## Testing/ running your code locally using the public preview of dev tunnels [#](https://www.eriklieben.com/posts/semantic-release-status-check-part-1/#testing%2F-running-your-code-locally-using-the-public-preview-of-dev-tunnels)

It's always more fun and easier to understand first to see your code working on your machine and be able to debug it and step through your code. This is a bit difficult for most because our development machine is most likely not directly exposed to the internet/ publicly accessible. To still perform this action, we need a temporary tunnel. This can either be done using ngrok or a new tool in Visual Studio called dev tunnels. We will use the latter.

For this to work, you must ensure you have version 17.4 of Visual Studio. The first step is to enable the preview version; go to: `Tools`, `Options` in the Visual Studio toolbar. The options window will show; on the left side, open up the `Environment` section and select the subsection `Preview Features`. Ensure the checkbox before 'Enable dev tunnels for Web Applications' is checked.

![Enable dev tunnels preview](https://www.eriklieben.com/img/posts/semantic-release-status-check/enable-dev-tunnels-vs.jpg)

Next, we need to configure the Azure Function to use dev tunnels. In your solution explorer, open the folder `Properties` and open the file `launchSettings.json`. See the code below, and make sure the `launchBrowser` option is set to `true`; by default, this is set to `false` and add the two devTunnel properties on like 7 and 8.

```json
{
  "profiles": {
    "AzureDevopsSemanticRelease": {
      "commandName": "Project",
      "commandLineArgs": "--port 7068",
      "launchBrowser": true,
      "devTunnelEnabled": true,
      "devTunnelAccess": "public"
    }
  }
}
```

Start your Azure Function app by clicking play or F5 to run it. A new browser window will open, which contains the link to your dev tunnel. In the screenshot, the actual URL is masked with `*`'s; yours will have a random set of characters and is personal to you/ made at random for testing.

![Browser window with dev tunnel link](https://www.eriklieben.com/img/posts/semantic-release-status-check/dev-tunnel-open-browser-dark-mode.jpg)

Copy the URL you get, because we will use it in the next step to setup the webhook.

## Setting up the webhook in Azure DevOps

We need to set up a service hook to be made aware of new pull requests or changes to a pull request. A service hook is comparable to a webhook; when something occurs, it calls your HTTP endpoint to inform you of the change.

![Open the project settings](https://www.eriklieben.com/img/posts/semantic-release-status-check/open-project-settings-light-mode.jpg)

Next, go to the section `Service hooks` and click the `+ Create subscription` button.

![Add service hook](https://www.eriklieben.com/img/posts/semantic-release-status-check/add-service-hook-new-light-mode.jpg)

If you already have service hooks configured, the screen looks a bit different, clicking the `+` button will bring you to the same spot.

![Add service hook with existing service hooks](https://www.eriklieben.com/img/posts/semantic-release-status-check/add-service-hook-existing-light-mode.jpg)

A new window will open up, scroll down in the list on the left and select `Web Hooks` and click `Next`.

![Select web hooks](https://www.eriklieben.com/img/posts/semantic-release-status-check/new-service-hook-type-light-mode.jpg)

On the next screen, we must set the type of events we would like to receive. In the dropdown below `Trigger on this type of event` select `Pull request created` and click `Next`.

Optionally, you can filter on repository, target branch, or members. For example, if you use a lot of different branches, then set this to your main branch (the branch that triggers the release).

![Select type of web hook](https://www.eriklieben.com/img/posts/semantic-release-status-check/create-pull-request-web-hook-trigger-light-mode.jpg)

The last part of the wizard steps allows you to specify the target URL. For testing purpose and to see the code in action, we will add the URL we received above when creating the dev tunnel appended with `/api/validate-pr-title`.

_Note: once the system is deployed to production, we must replace this URL with the production URL._

![Provide webhook URL](https://www.eriklieben.com/img/posts/semantic-release-status-check/complete-adding-service-web-hook-light-mode.jpg)

Our first service hook/webhook is now configured.

Repeat the process above once more for pull request updates. In the event to trigger step, select `Pull request updated` for the field `Trigger on this type of event`. Once this is completed, you will have two service hooks, one that triggers the creation of pull requests and one that triggers changes/updates to pull requests.

We can now create a testing pull request and try to adjust the title when our Function App is running on our local machine. If you set a breakpoint, it will be hit, and you can step through the code.

![Add a breakpoint](https://www.eriklieben.com/img/posts/semantic-release-status-check/debug-the-azure-function-using-dev-tunnel-light-mode.jpg)

Be aware that your setup is now configured to temporarly work together with the local instance running on your machine. We don't want to keep this on all the time, so after confirming that everything works it might be good to remove the webhook from Azure DevOps so we can, once you completed the later on reconfigure it.

# Finally 

With the above step completed, we've seen that the code does what we expect it to do and also discovered how to debug and step through the code if we ever require that. Now, running the function locally on your machine isn't an option. In the second part of the blog post, we will look at how we can bring our code to production/ host it in Azure.

# Up next 

In part 2 of this blog post, we will cover how to create our IaC (Infrastructure as Code) a bicep template to deploy our infrastructure, setup our Azure build pipeline to release/deploy everything, and finally hook it all up to our production environment, so that it is ready for use.