All Articles

List active (pending) Pull Requests on Microsoft Teams chat room

As a pricipal engineer, I move to a new team routinely. In the new team I recently joined, I noticed that Pull Requests stayed pending longer than we liked. There’s a out-of-the-box app that shows Pull Requests from Azure Devops but it keeps sending the same PR when anyone push a new commit. So, I wanted to create a simple custom app that lists down all Pull Requets from the team and shows them in our shared chatroom once a day.

My Acceptance Criteria

  • List down all Pull Requests
  • Send the message once a day, at 9 AM
  • Send the message only from Monday to Friday, not in the weekend
  • The message has the list of all pending Pull Requests and associated link to the page where you can review the PR
  • Each team can have their own customised list of Pull Requests

Azure Function

My chosen app service was Azure Function. I used webjobs before but it seemed not relevant any more in .NET Core era. Azure Function supports CRON expression, so it’s easy to execute the function daily. I’m not very familiar with CRON expression, so used this cheatsheet.

[FunctionName("RetrievePullRequests")]
public static async Task Run([TimerTrigger("0 0 9 * * MON,TUE,WED,THU,FRI")]TimerInfo myTimer, ILogger log)
{
    log.LogInformation($"Monring News function executed at: {DateTime.Now}");

    var pat = "<your personal access token>";
    var credentials = new VssBasicCredential(string.Empty, pat);
    var connection = new VssConnection(new Uri("https://dev.azure.com/<your organization>"), credentials);

    var gitClient = await connection.GetClientAsync<GitHttpClient>();
    var project = "<your project>";
    var repositoryIds = new[] {"your_repository_1",
        "your_repository_2"
    };

    var pullRequests = new List<GitPullRequest>();
    foreach (var repositoryId in repositoryIds)
    {
        pullRequests.AddRange(await gitClient.GetPullRequestsAsync(project, repositoryId, new GitPullRequestSearchCriteria
        {
            Status = PullRequestStatus.Active
        }));
    }


    var facts = "";
    foreach (var pullRequest in pullRequests)
    {
        var repositoryName = pullRequest.Repository.Name;
        var title = pullRequest.Title;
        var repositoryUri =
            $"https://dev.azure.com/<your_organization>/<your_project>/_git/{repositoryName}/pullrequest/{pullRequest.PullRequestId}";
        var prSubmitter = pullRequest.CreatedBy.DisplayName;

        facts +=
            "      {\n"
            + $"      \"name\": \"{repositoryName}\",\n"
            + $"      \"value\": \"[{title}]({repositoryUri}) by {prSubmitter}\"\n"
            + "    },\n";
    }

    var generalChatRoom =
        "https://outlook.office.com/webhook/<your_teams_webhook_uri>";

    var card = GetBody(facts);
    Console.WriteLine(card);

    var client = new HttpClient();
    var response = await client.PostAsync(
        generalChatRoom,
        new StringContent(card));
}

private static string GetBody(string facts)
{
    return
        "{\n"
        + "  \"@type\": \"MessageCard\",\n"
        + "  \"@context\": \"http://schema.org/extensions\",\n"
        + "  \"themeColor\": \"0076D7\",\n"
        + "  \"summary\": \"Morning News\",\n"
        + "  \"sections\": [{\n"
        + "    \"markdown\": true,\n"
        + "    \"facts\": [\n"
        + $"{facts}"
        + "      ]\n"
        + "  }]\n"
        + "}";
}

Continuous delivery by using Azure DevOps

Create a file, .azure-pipelines-ci.yaml, in the root directory of your solution / project.

Build your app

pool:
      vmImage: 'VS2017-Win2016'
steps:
- task: DotNetCoreCLI@2
  displayName: NuGet Restore
  inputs:
    command: restore
    projects: '**/*.csproj'
    noCache: true
- task: DotNetCoreCLI@2
  inputs:
    command: 'build'    
- task: DotNetCoreCLI@2
  inputs:
    command: publish
    arguments: '--configuration Release --output publish_output'
    projects: '**/*.csproj'
    publishWebProjects: false
    modifyOutputPath: false
    zipAfterPublish: false
- task: ArchiveFiles@2
  displayName: "Archive files"
  inputs:
    rootFolderOrFile: "$(System.DefaultWorkingDirectory)/publish_output"
    includeRootFolder: false
    archiveFile: "$(System.DefaultWorkingDirectory)/build$(Build.BuildId).zip"
- task: PublishBuildArtifacts@1
  inputs:
    PathtoPublish: '$(System.DefaultWorkingDirectory)/build$(Build.BuildId).zip'
    artifactName: 'drop'

Deoloy your app

- task: AzureFunctionApp@1
  inputs:
    azureSubscription: '<your subscription>'
    appType: functionApp
    appName: '<your app name>'