Developing Azure DevOps Pull Request Comments Task Extension
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:
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:
- Extension manifest file (vss-extension.json)
- Discovery assets (screenshots, markdown), things you see in Marketplace
- Static files, like in this case build task Typescript code
Following tutorials show this quite well so I don't duplicate them here:
- Extensions overview - Azure DevOps | Microsoft Learn
- Add a build or release task in an extension - Azure DevOps | Microsoft Learn
- Custom build/release task reference - Azure DevOps | Microsoft Learn
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:
- npm install azure-pipelines-task-lib
- npm install azure-devops-node-api
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()