Developing Azure DevOps Pull Request Comments Task Extension

Developing Azure DevOps Pull Request Comments Task Extension
Simple task to add comments from pipeline to Azure DevOps Pull Request

TL;DR: Coded a small Azure DevOps Task extension that can be used to publish comments on Pull Requests. See and install extension from Marketplace or browse the open-source code from GitHub azure-devops-pr-comment-extension.

Task itself is easy to use in your pipeline yaml script:

- task: PullRequestComment@1
  inputs:
    comment: |
      This is **sample** _text_ 🎉
      [This is link](https://microsoft.com)
      Build ID is $(Build.BuildId)
      | Table |
      |---|
      | Cell |

The example shows that you can use multiline Markdown formatting as well as all variables that you may have available in the pipeline. When task executes, it will show up as a comment like this in your pull request comments:

Comment in Azure DevOps pull request created by PR Comment Task extension

How to create Azure DevOps extensions?

It's easy to create new extensions as Microsoft provides npx tooling to create baseline for your extension. Extension consists of main three parts:

  1. Extension manifest file (vss-extension.json)
  2. Discovery assets (screenshots, markdown), things you see in Marketplace
  3. Static files, like in this case build task Typescript code

Following tutorials show this quite well so I don't duplicate them here:

How to add comment on PR in your extension with TypeScript?

So how to utilize and interact with Azure DevOps objects? Microsoft provides a couple of handy modules that make it easy to interact with the Azure DevOps API and especially Git API for Pull Request commenting. So, install these to your task script:

After this the comment thread can be created with the following task code:

import tl = require('azure-pipelines-task-lib/task')
import azdev = require('azure-devops-node-api')
import { CommentThreadStatus, CommentType } from 'azure-devops-node-api/interfaces/GitInterfaces'

async function run() {
  try{
    const comment: string | undefined = tl.getInput('comment', true)
    if(comment == '' || comment == undefined) {
      console.log(`Empty comment given - skipping PR comment`)
      return
    }
    const pullRequestId = parseInt(tl.getVariable('System.PullRequest.PullRequestId') ?? '-1')
    if(pullRequestId < 0 ) {
      console.log(`No pull request id - skipping PR comment`)
      return
    }
    
    // This is especially nice for task extensions as you
    // don't need to pass access tokens as parameters but you can get
    // the access token to az api with just the following:
    const accessToken = tl.getEndpointAuthorizationParameter('SystemVssConnection', 'AccessToken', false) ?? ''
    const authHandler = azdev.getPersonalAccessTokenHandler(accessToken) ?? ''
    
    // All the system/build variables can be accessed in similar
    // way as you access those from pipelines
    const collectionUri = tl.getVariable('System.CollectionUri') ?? ''
    const repositoryId = tl.getVariable('Build.Repository.ID') ?? ''
    const connection = new azdev.WebApi(collectionUri, authHandler)
    const gitApi = await connection.getGitApi()
    
    // Thread and comment schema wasn't explained well anywhere
    // but this minimalistic version was constructed from various
    // Stackoverflow posts
    const thread : any = {
      comments: [{
        commentType: CommentType.Text,
        content: comment,
      }],
      lastUpdatedDate: new Date(),
      publishedDate: new Date(),
      status: CommentThreadStatus.Closed,
    }
    const t = await gitApi.createThread(thread, repositoryId, pullRequestId)
    
    // Console logging shows up nicely on the actual build log
    console.log(`Comment added on pull request: ${comment}`)
  }
  catch (err:any) {
    tl.setResult(tl.TaskResult.Failed, err.message)
  }
}

run()
Mastodon