Public/Publish-PRComments.ps1
|
<#
.SYNOPSIS Post AI code review violations as PR thread comments using Azure DevOps REST API .DESCRIPTION Creates inline comments on pull request files at the specific line numbers where violations were detected. Uses Azure DevOps REST API to post comments. Requires System.AccessToken with vso.code_write permissions. .PARAMETER Violations Array of violation objects from Invoke-AICodeReview (must have file, line, severity, message properties) .PARAMETER AccessToken Azure DevOps OAuth token (typically System.AccessToken from pipeline) .PARAMETER OrganizationUrl Azure DevOps organization URL (e.g., https://dev.azure.com/iFacto/) Default: Uses System.TeamFoundationCollectionUri environment variable .PARAMETER ProjectName Project name containing the repository Default: Uses System.TeamProject environment variable .PARAMETER RepositoryId Repository GUID Default: Uses Build.Repository.ID environment variable .PARAMETER PullRequestId Pull request ID number Default: Uses System.PullRequest.PullRequestId environment variable .PARAMETER SkipDuplicates Skip posting if a comment already exists at the same file:line location .EXAMPLE $violations = Invoke-AICodeReview -ReviewContext $context -Rules $rules Publish-PRComments -Violations $violations -AccessToken $env:SYSTEM_ACCESSTOKEN .NOTES - Requires "Allow scripts to access OAuth token" enabled in pipeline settings - File paths must use forward slashes (/) and start with / - Handles API failures gracefully without throwing exceptions #> function Publish-PRComments { [CmdletBinding()] param( [Parameter(Mandatory)] [array]$Violations, [Parameter(Mandatory)] [string]$AccessToken, [string]$OrganizationUrl = $env:SYSTEM_TEAMFOUNDATIONCOLLECTIONURI, [string]$ProjectName = $env:SYSTEM_TEAMPROJECT, [string]$RepositoryId = $env:BUILD_REPOSITORY_ID, [string]$PullRequestId = $env:SYSTEM_PULLREQUEST_PULLREQUESTID, [switch]$SkipDuplicates ) Write-Host "`n=========================================" Write-Host "Publishing PR Comments via REST API" Write-Host "=========================================`n" # Validate required context if ([string]::IsNullOrWhiteSpace($OrganizationUrl)) { throw "OrganizationUrl is required (System.TeamFoundationCollectionUri not found)" } if ([string]::IsNullOrWhiteSpace($ProjectName)) { throw "ProjectName is required (System.TeamProject not found)" } if ([string]::IsNullOrWhiteSpace($RepositoryId)) { throw "RepositoryId is required (Build.Repository.ID not found)" } if ([string]::IsNullOrWhiteSpace($PullRequestId)) { throw "PullRequestId is required (System.PullRequest.PullRequestId not found). Are you running in a PR build?" } Write-Host "Organization: $OrganizationUrl" Write-Host "Project: $ProjectName" Write-Host "Repository: $RepositoryId" Write-Host "Pull Request: $PullRequestId" Write-Host "Violations to post: $($Violations.Count)" Write-Host "" # Prepare API client $headers = @{ "Authorization" = "Bearer $AccessToken" "Content-Type" = "application/json" } $baseUri = "$OrganizationUrl$ProjectName/_apis/git/repositories/$RepositoryId/pullRequests/$PullRequestId" # Get existing threads if deduplication is enabled $existingThreads = @() if ($SkipDuplicates) { try { Write-Host "Fetching existing PR threads for deduplication..." $response = Invoke-RestMethod -Uri "$baseUri/threads?api-version=7.1" -Headers $headers -Method Get $existingThreads = $response.value Write-Host "Found $($existingThreads.Count) existing thread(s)`n" } catch { Write-Warning "Failed to fetch existing threads: $($_.Exception.Message)" Write-Warning "Proceeding without deduplication" } } # Post comments $successCount = 0 $skippedCount = 0 $failedCount = 0 foreach ($violation in $Violations) { # Normalize file path (must start with / and use forward slashes) $filePath = $violation.file -replace '\\', '/' if (-not $filePath.StartsWith('/')) { $filePath = "/$filePath" } Write-Verbose "Processing: ${filePath}:$($violation.line) - $($violation.severity)" # Check for duplicates if ($SkipDuplicates) { $duplicate = $existingThreads | Where-Object { $_.threadContext -and $_.threadContext.filePath -eq $filePath -and $_.threadContext.rightFileStart -and $_.threadContext.rightFileStart.line -eq $violation.line } if ($duplicate) { Write-Host " ⏭️ Skipped (duplicate): ${filePath}:$($violation.line)" $skippedCount++ continue } } # Build comment content $emoji = switch ($violation.severity) { 'error' { '❌' } 'warning' { '⚠️' } 'info' { 'ℹ️' } default { '💡' } } $commentContent = "**$emoji $($violation.severity.ToUpper())**: $($violation.message)" if ($violation.suggestion) { $commentContent += "`n`n**Suggested Fix:**`n``````al`n$($violation.suggestion)`n``````" } $commentContent += "`n---`n*Generated by AI Code Review*" # Prepare thread payload $threadPayload = @{ comments = @(@{ parentCommentId = 0 content = $commentContent commentType = 1 # Text }) status = if ($violation.severity -eq 'error') { 1 } else { 2 } # Active (1) or Pending (2) threadContext = @{ filePath = $filePath rightFileStart = @{ line = $violation.line offset = 1 } rightFileEnd = @{ line = $violation.line offset = 999 } } } | ConvertTo-Json -Depth 10 # Post thread try { $null = Invoke-RestMethod -Uri "$baseUri/threads?api-version=7.1" ` -Headers $headers ` -Method Post ` -Body $threadPayload ` -TimeoutSec 30 Write-Host " ✅ Posted: ${filePath}:$($violation.line)" $successCount++ } catch { Write-Warning "Failed to post comment to ${filePath}:$($violation.line): $($_.Exception.Message)" $failedCount++ # Log detailed error for debugging if ($_.ErrorDetails.Message) { Write-Verbose "API Error Details: $($_.ErrorDetails.Message)" } } } # Summary Write-Host "`n=========================================" Write-Host "PR Comment Summary" Write-Host "=========================================`n" Write-Host "✅ Posted: $successCount" if ($skippedCount -gt 0) { Write-Host "⏭️ Skipped: $skippedCount (duplicates)" } if ($failedCount -gt 0) { Write-Host "❌ Failed: $failedCount" } Write-Host "`n=========================================" return @{ Success = $successCount Skipped = $skippedCount Failed = $failedCount } } Export-ModuleMember -Function Publish-PRComments |