PRIX / src /commenter.ts
Rachit-Tw's picture
Upload 169 files
9284ad7 verified
Raw
History Blame Contribute Delete
28.8 kB
// Removed @actions imports for Probot migration
import {octokit} from './octokit'
import {als} from './context'
import pino from 'pino'
import pRetry from 'p-retry'
const logger = pino({ level: process.env.LOG_LEVEL || 'info' })
const info = (msg: string) => logger.info(msg)
const warning = (msg: string) => logger.warn(msg)
const getInput = (name: string) => process.env[name.toUpperCase()] || ''
// Retry configuration for GitHub API calls
const GITHUB_RETRY_OPTIONS = {
retries: 3,
minTimeout: 1000,
maxTimeout: 10000,
factor: 2,
onFailedAttempt: (error: any) => {
logger.warn({
attemptNumber: error.attemptNumber,
retriesLeft: error.retriesLeft,
error: error.message
}, 'GitHub API call failed, retrying...')
}
}
// Wrapper for retryable GitHub API calls
async function withRetry<T>(fn: () => Promise<T>): Promise<T> {
return pRetry(fn, GITHUB_RETRY_OPTIONS)
}
export const context: any = new Proxy({} as any, {
get(target, prop) {
return (als.getStore()?.probotContext as any)[prop]
}
})
export const repo: any = new Proxy({} as any, {
get(target, prop) {
return (als.getStore()?.repo as any)[prop]
}
})
export const setCommenterContext = (newContext: any) => {
// context and repo are now Proxies
}
export const LOGO_URL = 'https://www.prixai.xyz/logo.png'
export const COMMENT_GREETING = `<div align="center">
<img src="${LOGO_URL}" width="64" height="64" alt="PRIX Logo">
# 🤖 PRIX AI Code Review
</div>
---
Welcome! I'm PRIX, your AI code reviewer. I've analyzed this pull request and here are my findings:
---`
export const COMMENT_TAG =
'<!-- This is an auto-generated comment by AI PR Reviewer -->'
export const COMMENT_REPLY_TAG =
'<!-- This is an auto-generated reply by AI PR Reviewer -->'
export const SUMMARIZE_TAG =
'<!-- This is an auto-generated comment: summarize by AI PR Reviewer -->'
export const IN_PROGRESS_START_TAG =
'<!-- This is an auto-generated comment: summarize review in progress by AI PR Reviewer -->'
export const IN_PROGRESS_END_TAG =
'<!-- end of auto-generated comment: summarize review in progress by AI PR Reviewer -->'
export const DESCRIPTION_START_TAG =
'<!-- This is an auto-generated comment: release notes by AI PR Reviewer -->'
export const DESCRIPTION_END_TAG =
'<!-- end of auto-generated comment: release notes by AI PR Reviewer -->'
export const RAW_SUMMARY_START_TAG = `<!-- This is an auto-generated comment: raw summary by AI PR Reviewer -->
<!--
`
export const RAW_SUMMARY_END_TAG = `-->
<!-- end of auto-generated comment: raw summary by AI PR Reviewer -->`
export const SHORT_SUMMARY_START_TAG = `<!-- This is an auto-generated comment: short summary by AI PR Reviewer -->
<!--
`
export const SHORT_SUMMARY_END_TAG = `-->
<!-- end of auto-generated comment: short summary by OSS CodeRabbit -->`
export const COMMIT_ID_START_TAG = '<!-- commit_ids_reviewed_start -->'
export const COMMIT_ID_END_TAG = '<!-- commit_ids_reviewed_end -->'
export const VERIFIED_TAG = '<!-- verified_by_prix -->'
export const VERIFICATION_FAILED_TAG = '<!-- verification_failed -->'
export const ISSUE_REPLY_TAG =
'<!-- This is an auto-generated issue analysis by PRIX -->'
export const PRIX_BRANDING = `
---
<div align="center">
🚀 **[Get PRIX for your team](https://www.prixai.xyz/)** — AI-powered code reviews that catch bugs before they ship
</div>`
export const PLAN_BRANDING = `
---
<div align="center">
🚀 **[Get PRIX for your team](https://www.prixai.xyz/)** — AI-powered code reviews that catch bugs before they ship
</div>`
export class Commenter {
/**
* @param mode Can be "create", "replace". Default is "replace".
*/
async comment(message: string, tag: string, mode: string) {
let target: number
if (context.payload.pull_request != null) {
target = context.payload.pull_request.number
} else if (context.payload.issue != null) {
target = context.payload.issue.number
} else {
warning(
'Skipped: context.payload.pull_request and context.payload.issue are both null'
)
return
}
if (!tag) {
tag = COMMENT_TAG
}
const body = `${COMMENT_GREETING}
${message}
${tag}${PRIX_BRANDING}`
if (mode === 'create') {
await this.create(body, target)
} else if (mode === 'replace') {
await this.replace(body, tag, target)
} else {
warning(`Unknown mode: ${mode}, use "replace" instead`)
await this.replace(body, tag, target)
}
}
getContentWithinTags(content: string, startTag: string, endTag: string) {
const start = content.indexOf(startTag)
const end = content.indexOf(endTag)
if (start >= 0 && end >= 0) {
return content.slice(start + startTag.length, end)
}
return ''
}
removeContentWithinTags(content: string, startTag: string, endTag: string) {
const start = content.indexOf(startTag)
const end = content.lastIndexOf(endTag)
if (start >= 0 && end >= 0) {
return content.slice(0, start) + content.slice(end + endTag.length)
}
return content
}
getRawSummary(summary: string) {
return this.getContentWithinTags(
summary,
RAW_SUMMARY_START_TAG,
RAW_SUMMARY_END_TAG
)
}
getShortSummary(summary: string) {
return this.getContentWithinTags(
summary,
SHORT_SUMMARY_START_TAG,
SHORT_SUMMARY_END_TAG
)
}
getDescription(description: string) {
return this.removeContentWithinTags(
description,
DESCRIPTION_START_TAG,
DESCRIPTION_END_TAG
)
}
getReleaseNotes(description: string) {
const releaseNotes = this.getContentWithinTags(
description,
DESCRIPTION_START_TAG,
DESCRIPTION_END_TAG
)
return releaseNotes.replace(/(^|\n)> .*/g, '')
}
async updateDescription(pullNumber: number, message: string) {
// add this response to the description field of the PR as release notes by looking
// for the tag (marker)
try {
// get latest description from PR
const pr = await octokit.rest.pulls.get({
owner: repo.owner,
repo: repo.repo,
// eslint-disable-next-line camelcase
pull_number: pullNumber,
request: { timeout: 10000 }
})
let body = ''
if (pr.data.body) {
body = pr.data.body
}
const description = this.getDescription(body)
const messageClean = this.removeContentWithinTags(
message,
DESCRIPTION_START_TAG,
DESCRIPTION_END_TAG
)
const newDescription = `${description}\n${DESCRIPTION_START_TAG}\n${messageClean}\n${DESCRIPTION_END_TAG}`
await octokit.rest.pulls.update({
owner: repo.owner,
repo: repo.repo,
// eslint-disable-next-line camelcase
pull_number: pullNumber,
body: newDescription,
request: { timeout: 10000 }
})
} catch (e) {
warning(
`Failed to get PR: ${e}, skipping adding release notes to description.`
)
}
}
private readonly reviewCommentsBuffer: Array<{
path: string
startLine: number
endLine: number
message: string
verified?: boolean
verificationFeedback?: string
level?: string
type?: string
}> = []
async bufferReviewComment(
path: string,
startLine: number,
endLine: number,
message: string,
verified?: boolean,
verificationFeedback?: string,
testCase?: string
) {
// Premium UI Formatting: Convert "[LEVEL] | [TYPE]" to emoji badges
const levelTypeRegex = /^(\w+)\s*\|\s*(\w+)/
const match = message.match(levelTypeRegex)
let level = 'info'
let type = 'General'
let severityEmoji = 'ℹ️'
let color = 'Info'
if (match != null) {
level = match[1].toLowerCase()
type = match[2]
if (level === 'critical') {
severityEmoji = '🚨'
color = 'Critical'
} else if (level === 'major') {
severityEmoji = '🔴'
color = 'Major'
} else if (level === 'minor') {
severityEmoji = '🟠'
color = 'Minor'
} else if (level === 'info') {
severityEmoji = 'ℹ️'
color = 'Info'
}
message = message.replace(levelTypeRegex, '').trim()
}
// Generate AI Agent Prompt for actionable issues
let aiAgentSection = ''
if (level === 'critical' || level === 'major') {
const aiPrompt = this.generateAIAgentPrompt(path, startLine, endLine, message)
aiAgentSection = `
<details>
<summary>🤖 Prompt for AI Agents</summary>
Copy and paste this prompt into your AI IDE (Cursor, Windsurf, etc.) to auto-fix:
\`\`\`
${aiPrompt}
\`\`\`
</details>`
}
let verificationBadge = ''
if (verified === true) {
verificationBadge =
'\n---\n✅ **Verified**: This suggestion passed syntax, lint, and AST validation.\n'
} else if (verified === false && verificationFeedback) {
verificationBadge = `\n---\n⚠️ **Verification Failed**: ${verificationFeedback}\n`
}
// Add test case section if available
const testCaseSection = testCase ? `\n\n${testCase}` : ''
// CodeRabbit-style formatted comment
const formattedMessage = `**${severityEmoji} Potential Issue | ${color} (${type})**
${message}${aiAgentSection}${testCaseSection}${verificationBadge}
${COMMENT_TAG}`
this.reviewCommentsBuffer.push({
path,
startLine,
endLine,
message: formattedMessage,
verified,
verificationFeedback,
level,
type
})
}
private generateAIAgentPrompt(
path: string,
startLine: number,
endLine: number,
issue: string
): string {
return `Fix the following issue in ${path} around lines ${startLine}-${endLine}:
Issue: ${issue.split('\n')[0]}
Steps:
1. Read the file ${path}
2. Locate lines ${startLine}-${endLine}
3. Fix the issue described above
4. Ensure the fix follows the existing code style
5. Verify no other parts of the code are broken
Provide the complete fixed code block.`
}
async deletePendingReview(pullNumber: number) {
try {
const reviews = await octokit.rest.pulls.listReviews({
owner: repo.owner,
repo: repo.repo,
// eslint-disable-next-line camelcase
pull_number: pullNumber,
request: { timeout: 10000 }
})
const pendingReview = reviews.data.find(
review => review.state === 'PENDING'
)
if (pendingReview) {
info(
`Deleting pending review for PR #${pullNumber} id: ${pendingReview.id}`
)
try {
await octokit.rest.pulls.deletePendingReview({
owner: repo.owner,
repo: repo.repo,
// eslint-disable-next-line camelcase
pull_number: pullNumber,
// eslint-disable-next-line camelcase
review_id: pendingReview.id,
request: { timeout: 10000 }
})
} catch (e) {
warning(`Failed to delete pending review: ${e}`)
}
}
} catch (e) {
warning(`Failed to list reviews: ${e}`)
}
}
async submitReview(pullNumber: number, commitId: string, statusMsg: string) {
const bodyHeader = `${COMMENT_GREETING}
${statusMsg}
${PRIX_BRANDING}`
if (this.reviewCommentsBuffer.length === 0) {
// Submit empty review with statusMsg
info(`Submitting empty review for PR #${pullNumber}`)
try {
await octokit.rest.pulls.createReview({
owner: repo.owner,
repo: repo.repo,
// eslint-disable-next-line camelcase
pull_number: pullNumber,
// eslint-disable-next-line camelcase
commit_id: commitId,
event: 'COMMENT',
body: bodyHeader,
request: { timeout: 10000 }
})
} catch (e) {
warning(`Failed to submit empty review: ${e}`)
}
return
}
// De-duplicate and clear existing bot comments in the same range
for (const comment of this.reviewCommentsBuffer) {
const comments = await this.getCommentsAtRange(
pullNumber,
comment.path,
comment.startLine,
comment.endLine
)
for (const c of comments) {
if (c.body.includes(COMMENT_TAG)) {
try {
await octokit.rest.pulls.deleteReviewComment({
owner: repo.owner,
repo: repo.repo,
// eslint-disable-next-line camelcase
comment_id: c.id,
request: { timeout: 10000 }
})
} catch (e) {
warning(`Failed to delete review comment: ${e}`)
}
}
}
}
await this.deletePendingReview(pullNumber)
// Unified Review: All comments are standalone threads to support suggestions
const pushableComments = this.reviewCommentsBuffer.map(comment =>
this.generateCommentData(comment)
)
try {
const review = await octokit.rest.pulls.createReview({
owner: repo.owner,
repo: repo.repo,
// eslint-disable-next-line camelcase
pull_number: pullNumber,
// eslint-disable-next-line camelcase
commit_id: commitId,
comments: pushableComments.map(comment => comment),
request: { timeout: 10000 }
})
info(
`Submitting review for PR #${pullNumber}, total comments: ${pushableComments.length}, review id: ${review.data.id}`
)
await octokit.rest.pulls.submitReview({
owner: repo.owner,
repo: repo.repo,
// eslint-disable-next-line camelcase
pull_number: pullNumber,
// eslint-disable-next-line camelcase
review_id: review.data.id,
event: 'COMMENT',
body: bodyHeader,
request: { timeout: 10000 }
})
} catch (e) {
warning(
`Failed to create review: ${e}. Falling back to individual comments.`
)
await this.deletePendingReview(pullNumber)
let commentCounter = 0
for (const comment of pushableComments) {
info(
`Creating new review comment for ${comment.path}:${comment.start_line || comment.line}-${comment.line}: ${comment.body}`
)
const commentData: any = {
owner: repo.owner,
repo: repo.repo,
// eslint-disable-next-line camelcase
pull_number: pullNumber,
// eslint-disable-next-line camelcase
commit_id: commitId,
...comment
}
try {
await octokit.rest.pulls.createReviewComment({...commentData, request: { timeout: 10000 }})
} catch (ee) {
warning(`Failed to create review comment: ${ee}`)
}
commentCounter++
info(`Comment ${commentCounter}/${pushableComments.length} posted`)
}
}
}
private generateCommentData(comment: any) {
const commentData: any = {
path: comment.path,
body: comment.message,
line: comment.endLine
}
if (comment.startLine !== comment.endLine) {
// eslint-disable-next-line camelcase
commentData.start_line = comment.startLine
// eslint-disable-next-line camelcase
commentData.start_side = 'RIGHT'
}
return commentData
}
async reviewCommentReply(
pullNumber: number,
topLevelComment: any,
message: string
) {
const reply = `${COMMENT_GREETING}
${message}
${COMMENT_REPLY_TAG}${PRIX_BRANDING}
`
try {
// Post the reply to the user comment
await octokit.rest.pulls.createReplyForReviewComment({
owner: repo.owner,
repo: repo.repo,
// eslint-disable-next-line camelcase
pull_number: pullNumber,
body: reply,
// eslint-disable-next-line camelcase
comment_id: topLevelComment.id,
request: { timeout: 10000 }
})
} catch (error) {
warning(`Failed to reply to the top-level comment ${error}`)
try {
await octokit.rest.pulls.createReplyForReviewComment({
owner: repo.owner,
repo: repo.repo,
// eslint-disable-next-line camelcase
pull_number: pullNumber,
body: `Could not post the reply to the top-level comment due to the following error: ${error}`,
// eslint-disable-next-line camelcase
comment_id: topLevelComment.id,
request: { timeout: 10000 }
})
} catch (e) {
warning(`Failed to reply to the top-level comment ${e}`)
}
}
try {
if (topLevelComment.body.includes(COMMENT_TAG)) {
// replace COMMENT_TAG with COMMENT_REPLY_TAG in topLevelComment
const newBody = topLevelComment.body.replace(
COMMENT_TAG,
COMMENT_REPLY_TAG
)
await octokit.rest.pulls.updateReviewComment({
owner: repo.owner,
repo: repo.repo,
// eslint-disable-next-line camelcase
comment_id: topLevelComment.id,
body: newBody,
request: { timeout: 10000 }
})
}
} catch (error) {
warning(`Failed to update the top-level comment ${error}`)
}
}
async getCommentsWithinRange(
pullNumber: number,
path: string,
startLine: number,
endLine: number
) {
const comments = await this.listReviewComments(pullNumber)
return comments.filter(
(comment: any) =>
comment.path === path &&
comment.body !== '' &&
((comment.start_line !== undefined &&
comment.start_line >= startLine &&
comment.line <= endLine) ||
(startLine === endLine && comment.line === endLine))
)
}
async getCommentsAtRange(
pullNumber: number,
path: string,
startLine: number,
endLine: number
) {
const comments = await this.listReviewComments(pullNumber)
return comments.filter(
(comment: any) =>
comment.path === path &&
comment.body !== '' &&
((comment.start_line !== undefined &&
comment.start_line === startLine &&
comment.line === endLine) ||
(startLine === endLine && comment.line === endLine))
)
}
async getCommentChainsWithinRange(
pullNumber: number,
path: string,
startLine: number,
endLine: number,
tag = ''
) {
const existingComments = await this.getCommentsWithinRange(
pullNumber,
path,
startLine,
endLine
)
// find all top most comments
const topLevelComments = []
for (const comment of existingComments) {
if (!comment.in_reply_to_id) {
topLevelComments.push(comment)
}
}
let allChains = ''
let chainNum = 0
for (const topLevelComment of topLevelComments) {
// get conversation chain
const chain = await this.composeCommentChain(
existingComments,
topLevelComment
)
if (chain && chain.includes(tag)) {
chainNum += 1
allChains += `Conversation Chain ${chainNum}:
${chain}
---
`
}
}
return allChains
}
async composeCommentChain(reviewComments: any[], topLevelComment: any) {
const conversationChain = reviewComments
.filter((cmt: any) => cmt.in_reply_to_id === topLevelComment.id)
.map((cmt: any) => `${cmt.user.login}: ${cmt.body}`)
conversationChain.unshift(
`${topLevelComment.user.login}: ${topLevelComment.body}`
)
return conversationChain.join('\n---\n')
}
async getCommentChain(pullNumber: number, comment: any) {
try {
const reviewComments = await this.listReviewComments(pullNumber)
const topLevelComment = await this.getTopLevelComment(
reviewComments,
comment
)
const chain = await this.composeCommentChain(
reviewComments,
topLevelComment
)
return {chain, topLevelComment}
} catch (e) {
warning(`Failed to get conversation chain: ${e}`)
return {
chain: '',
topLevelComment: null
}
}
}
async getTopLevelComment(reviewComments: any[], comment: any) {
let topLevelComment = comment
while (topLevelComment.in_reply_to_id) {
const parentComment = reviewComments.find(
(cmt: any) => cmt.id === topLevelComment.in_reply_to_id
)
if (parentComment) {
topLevelComment = parentComment
} else {
break
}
}
return topLevelComment
}
private reviewCommentsCache: Record<number, any[]> = {}
async listReviewComments(target: number) {
if (this.reviewCommentsCache[target]) {
return this.reviewCommentsCache[target]
}
const allComments: any[] = []
let page = 1
try {
for (;;) {
const {data: comments} = await octokit.rest.pulls.listReviewComments({
owner: repo.owner,
repo: repo.repo,
// eslint-disable-next-line camelcase
pull_number: target,
page,
// eslint-disable-next-line camelcase
per_page: 100,
request: { timeout: 10000 }
})
allComments.push(...comments)
page++
if (!comments || comments.length < 100) {
break
}
}
this.reviewCommentsCache[target] = allComments
return allComments
} catch (e) {
warning(`Failed to list review comments: ${e}`)
return allComments
}
}
async create(body: string, target: number) {
try {
// get comment ID from the response
const response = await octokit.rest.issues.createComment({
owner: repo.owner,
repo: repo.repo,
// eslint-disable-next-line camelcase
issue_number: target,
body,
request: { timeout: 10000 }
})
// add comment to issueCommentsCache
if (this.issueCommentsCache[target]) {
this.issueCommentsCache[target].push(response.data)
} else {
this.issueCommentsCache[target] = [response.data]
}
} catch (e) {
warning(`Failed to create comment: ${e}`)
}
}
async replace(body: string, tag: string, target: number) {
try {
const cmt = await this.findCommentWithTag(tag, target)
if (cmt) {
await octokit.rest.issues.updateComment({
owner: repo.owner,
repo: repo.repo,
// eslint-disable-next-line camelcase
comment_id: cmt.id,
body,
request: { timeout: 10000 }
})
} else {
await this.create(body, target)
}
} catch (e) {
warning(`Failed to replace comment: ${e}`)
}
}
async findCommentWithTag(tag: string, target: number) {
try {
const comments = await this.listComments(target)
for (const cmt of comments) {
if (cmt.body && cmt.body.includes(tag)) {
return cmt
}
}
return null
} catch (e: unknown) {
warning(`Failed to find comment with tag: ${e}`)
return null
}
}
private issueCommentsCache: Record<number, any[]> = {}
async listComments(target: number) {
if (this.issueCommentsCache[target]) {
return this.issueCommentsCache[target]
}
const allComments: any[] = []
let page = 1
try {
for (;;) {
const {data: comments} = await octokit.rest.issues.listComments({
owner: repo.owner,
repo: repo.repo,
// eslint-disable-next-line camelcase
issue_number: target,
page,
// eslint-disable-next-line camelcase
per_page: 100,
request: { timeout: 10000 }
})
allComments.push(...comments)
page++
if (!comments || comments.length < 100) {
break
}
}
this.issueCommentsCache[target] = allComments
return allComments
} catch (e: any) {
warning(`Failed to list comments: ${e}`)
return allComments
}
}
// function that takes a comment body and returns the list of commit ids that have been reviewed
// commit ids are comments between the commit_ids_reviewed_start and commit_ids_reviewed_end markers
// <!-- [commit_id] -->
getReviewedCommitIds(commentBody: string): string[] {
const start = commentBody.indexOf(COMMIT_ID_START_TAG)
const end = commentBody.indexOf(COMMIT_ID_END_TAG)
if (start === -1 || end === -1) {
return []
}
const ids = commentBody.substring(start + COMMIT_ID_START_TAG.length, end)
// remove the <!-- and --> markers from each id and extract the id and remove empty strings
return ids
.split('<!--')
.map(id => id.replace('-->', '').trim())
.filter(id => id !== '')
}
// get review commit ids comment block from the body as a string
// including markers
getReviewedCommitIdsBlock(commentBody: string): string {
const start = commentBody.indexOf(COMMIT_ID_START_TAG)
const end = commentBody.indexOf(COMMIT_ID_END_TAG)
if (start === -1 || end === -1) {
return ''
}
return commentBody.substring(start, end + COMMIT_ID_END_TAG.length)
}
// add a commit id to the list of reviewed commit ids
// if the marker doesn't exist, add it
addReviewedCommitId(commentBody: string, commitId: string): string {
const start = commentBody.indexOf(COMMIT_ID_START_TAG)
const end = commentBody.indexOf(COMMIT_ID_END_TAG)
if (start === -1 || end === -1) {
return `${commentBody}\n${COMMIT_ID_START_TAG}\n<!-- ${commitId} -->\n${COMMIT_ID_END_TAG}`
}
const ids = commentBody.substring(start + COMMIT_ID_START_TAG.length, end)
return `${commentBody.substring(
0,
start + COMMIT_ID_START_TAG.length
)}${ids}<!-- ${commitId} -->\n${commentBody.substring(end)}`
}
// given a list of commit ids provide the highest commit id that has been reviewed
getHighestReviewedCommitId(
commitIds: string[],
reviewedCommitIds: string[]
): string {
for (let i = commitIds.length - 1; i >= 0; i--) {
if (reviewedCommitIds.includes(commitIds[i])) {
return commitIds[i]
}
}
return ''
}
async getAllCommitIds(): Promise<string[]> {
const allCommits = []
let page = 1
let commits
if (context && context.payload && context.payload.pull_request != null) {
do {
commits = await octokit.rest.pulls.listCommits({
owner: repo.owner,
repo: repo.repo,
// eslint-disable-next-line camelcase
pull_number: context.payload.pull_request.number,
// eslint-disable-next-line camelcase
per_page: 100,
page,
request: { timeout: 10000 }
})
allCommits.push(...commits.data.map(commit => commit.sha))
page++
} while (commits.data.length > 0)
}
return allCommits
}
// add in-progress status to the comment body
addInProgressStatus(commentBody: string, statusMsg: string): string {
const start = commentBody.indexOf(IN_PROGRESS_START_TAG)
const end = commentBody.indexOf(IN_PROGRESS_END_TAG)
// add to the beginning of the comment body if the marker doesn't exist
// otherwise do nothing
if (start === -1 || end === -1) {
return `${IN_PROGRESS_START_TAG}
Currently reviewing new changes in this PR...
${statusMsg}
${IN_PROGRESS_END_TAG}
---
${commentBody}`
}
return commentBody
}
// remove in-progress status from the comment body
removeInProgressStatus(commentBody: string): string {
const start = commentBody.indexOf(IN_PROGRESS_START_TAG)
const end = commentBody.indexOf(IN_PROGRESS_END_TAG)
// remove the in-progress status if the marker exists
// otherwise do nothing
if (start !== -1 && end !== -1) {
return (
commentBody.substring(0, start) +
commentBody.substring(end + IN_PROGRESS_END_TAG.length)
)
}
return commentBody
}
}