Code

Teams, function apps & webhooks.

Background

Since migrating off of Slack to Microsoft Teams, our developers have been missing out on the integrations that have allowed them to see their pull request notifications.

Although Microsoft teams and Azure DevOps do natively support integrations between each other, unfortunatly for us, our Azure DevOps and Microsoft Teams are in two different AAD tenants which excludes us from being able to use the native functionaliy.

Thankfully, Microsoft Teams provides the ability to post notifications to any team using webhook integrations. In addition, Azure DevOps also has the ability to invoke webhooks in place of Slack, Teams or <insert any other product here>.

To do this, we will use Azure Function apps to translate an Azure DevOps notification into a Microsoft Teams compliant message.

Create the function app

The below code is everything you will need for the translation layer.

How does it work
a) Azure DevOps sends a webhook notification to the Azure Function app.
b) The Azure function serialises the request from Azure DevOps.
c) The Azure function converts the request into a Microsoft Teams message.
d) The Azure function send a POST request to the Microsoft Teams webhook.

This code can be directly copied and pasted into a new Azure Function app, you just need to update the variable 'TeamsHookURI' with your Microsoft Teams webhook (see below to create your webhook url).

Before leaving the function app page, take a copy of the functions app URL. You can grab this by clicking on the 'Get Function URL' at the top of the functions app page.


#r "Newtonsoft.Json"      

using System.Net;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Primitives;
using Newtonsoft.Json;
using System.Collections.Generic;
using System.Net.Http;
using System.Text;


public static async Task<IActionResult> Run(HttpRequest req, ILogger log)
{
    log.LogInformation("C# HTTP trigger function processed a request.");

    log.LogInformation("Request Recieved");
    string name = req.Query["name"];
    const string TeamsHookURI = "## YOUR TEAMS WEBHOOK HERE ##";

    string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
    DevOps data = JsonConvert.DeserializeObject<DevOps>(requestBody);

    TeamsMessage message = new TeamsMessage();
    message.title = $"{data.resource.createdBy.displayName} has created a new pull request!";
    message.text = data.resource.title;
    message.potentialAction = new List<PotentialAction>()
            {
                new PotentialAction()
                {
                    targets = new List<Target>()
                    {
                        new Target()
                        {
                            uri = data.resource._links.web.href
                        }
                    }
                }
            };
    message.sections = new List<Section>()
            {
                new Section()
                {
                    Title = "**Details**",
                    facts = new List<Fact>()
                    {
                        new Fact(){name = "Title:", value = data.resource.title},
                        new Fact(){name = "Project:", value = data.resource.repository.project.name},
                        new Fact(){name = "Repository:", value = data.resource.repository.name},
                        new Fact(){name = "Source Branch:", value = data.resource.sourceRefName},
                        new Fact(){name = "Target Branch:", value = data.resource.targetRefName}
                    }
                }
            };
    string json = JsonConvert.SerializeObject(message, Formatting.Indented);

    bool successfulPost = sendRequest(json, TeamsHookURI);
    if(!successfulPost)
        return (ActionResult)new BadRequestObjectResult(new { message = "Post to Teams failed.", currentDate = DateTime.Now });;
    return (ActionResult)new OkObjectResult("Success");
}

public static bool sendRequest(string content, string url)
        {
            using (var client = new HttpClient())
            using (var request = new HttpRequestMessage(HttpMethod.Post, url))
            {
                using (var stringContent = new StringContent(content, Encoding.UTF8, "application/json"))
                {
                    request.Content = stringContent;

                    using (var response = client.SendAsync(request))
                    {
                        return response.Result.IsSuccessStatusCode;
                    }
                }
            }
        }

//Azure DevOps classes
public class DevOps
{
    public resource resource {get;set;}
}

public class resource{
    public repository repository {get;set;}    
    public createdBy createdBy {get;set;}    
    public string sourceRefName {get;set;}
    public string targetRefName {get;set;}
    public string url {get;set;}
    public string time {get;set;}
    public string title {get;set;}
    public string pullRequestId {get;set;}
    public Links _links {get;set;}

}

public class Links{
    public Link web {get;set;}
}

public class Link{
    public string href {get;set;}
}

public class repository{
    public string name {get;set;}
    public project project {get;set;}
}

public class project{
    public string name {get;set;}
}

public class createdBy{
    public string displayName {get;set;}
}

//Microsot Teams Classes
public class Target
{
    public string os = "default";
    public string uri { get; set; }
}

public class PotentialAction
{
    [JsonProperty("@type")]
    public string actiontype = "OpenUri";
    public string name = "Review";
    public List<Target> targets = new List<Target>();
}

public class Fact
{
    public string name { get; set; }
    public string value { get; set; }
}

public class Section
{
    public string Title = "**Details**";
    public List<Fact> facts = new List<Fact>();
}

public class TeamsMessage
{
    public string context = "https://schema.org/extensions";
    public string type = "MessageCard";
    public string themeColor = "0072C6";
    public string title { get; set; }
    public string text = " ";
    public List<PotentialAction> potentialAction = new List<PotentialAction>();
    public List<Section> sections = new List<Section>();
}
      

Create your Microsoft Teams Webhook

After you have created the function app, you will need to create a new webhook in Microsoft teams. These are OOB and require no authentication to create and use.
Open up your teams -> Select the channel -> Manage Connectors -> Configure Webhook -> Set a name, upload an image and click 'Create'. 

You will now be given your teams webhook url. Once you have saved it, click done.

Configure Azure DevOps

For this task, you will need to be a project administrator for the Azure DevOps project you want to configure.
Open your project -> Project Settings -> Service Hooks -> (Add New) Webhooks -> Choose 'Pull Request' trigger.
You will now be presented with the 'Actions' page. 
This page will ask you for an Action URL, you will now paste your Azure Function app url here.

From here, each time your service triggers an action, Azure DevOps will send a POST request to your function app for translation to Microsoft teams.

Final Result

And with that we now have realtime notifications coming from Azure DevOps into our Microsoft teams channels.

Gavin Albantow - © 2021
Contact Me