Public/Invoke-AICodeReviewPipeline.ps1
|
function Invoke-AICodeReviewPipeline { <# .SYNOPSIS Executes the complete AI Code Review pipeline for Azure DevOps. .DESCRIPTION This cmdlet encapsulates all the logic needed to run AI code review in an Azure DevOps pipeline: - Git repository setup and cloning - Installing and loading the AI Code Review module - Gathering statistics from git diff - Executing the AI review - Creating artifacts and reports - Setting pipeline variables for downstream tasks .PARAMETER BaseBranch The base branch to compare against (e.g., 'origin/main') .PARAMETER Provider The AI provider to use ('github', 'azure', 'openai', 'anthropic') .PARAMETER SeverityFailBuild Severity level that should fail the build ('error', 'warning', 'none') .PARAMETER CustomRulesPath Optional path to custom rules directory .PARAMETER CustomConfigPath Optional path to custom model-config.json .PARAMETER AccessToken System.AccessToken from Azure DevOps for git authentication .PARAMETER RepositoryUri Build.Repository.Uri from Azure DevOps .PARAMETER SourceBranch Build.SourceBranch from Azure DevOps .PARAMETER BuildReason Build.Reason from Azure DevOps (e.g., 'PullRequest') .PARAMETER ArtifactStagingDirectory Build.ArtifactStagingDirectory from Azure DevOps .PARAMETER BuildSourcesDirectory Build.SourcesDirectory from Azure DevOps (where rules are generated) .PARAMETER BuildId Build.BuildId from Azure DevOps (for temp directory naming) .EXAMPLE Invoke-AICodeReviewPipeline -BaseBranch 'origin/main' -Provider 'azure' -SeverityFailBuild 'error' ` -AccessToken $env:SYSTEM_ACCESSTOKEN -RepositoryUri $env:BUILD_REPOSITORY_URI ` -SourceBranch $env:BUILD_SOURCEBRANCH -BuildReason $env:BUILD_REASON #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$BaseBranch, [Parameter(Mandatory)] [ValidateSet('github', 'azure', 'openai', 'anthropic')] [string]$Provider, [Parameter()] [ValidateSet('error', 'warning', 'none')] [string]$SeverityFailBuild = 'error', [Parameter()] [string]$CustomRulesPath = '', [Parameter()] [string]$CustomConfigPath = '', [Parameter(Mandatory)] [string]$AccessToken, [Parameter(Mandatory)] [string]$RepositoryUri, [Parameter(Mandatory)] [string]$SourceBranch, [Parameter(Mandatory)] [string]$BuildReason, [Parameter(Mandatory)] [string]$ArtifactStagingDirectory, [Parameter(Mandatory)] [string]$BuildSourcesDirectory, [Parameter(Mandatory)] [string]$BuildId, [Parameter()] [string]$BuildNumber = '', [Parameter()] [string]$SourceVersion = '' ) $ErrorActionPreference = 'Continue' $global:LASTEXITCODE = 0 Write-Host "##[section]AI Code Review Pipeline" # ========================================= # STEP 1: Ensure git is available # ========================================= Write-Host "`n--- Step 1: Finding git ---" $gitPath = (Get-Command git -ErrorAction SilentlyContinue)?.Source if (-not $gitPath) { Write-Host "Git not in PATH, searching..." $whereResult = where.exe git.exe 2>$null if ($LASTEXITCODE -eq 0 -and $whereResult) { $gitDir = Split-Path $whereResult[0] -Parent $env:PATH = "$gitDir;$env:PATH" Write-Host "✓ Found git via where.exe: $gitDir" } else { # Deep search for git.exe Write-Host "Searching filesystem for git.exe..." $searchPaths = @( "C:\Program Files", "C:\Program Files (x86)", "C:\tools", "C:\Git", "${env:AGENT_HOMEDIRECTORY}", "${env:AGENT_TOOLSDIRECTORY}" ) $gitFound = $false foreach ($searchRoot in $searchPaths) { if (-not (Test-Path $searchRoot)) { continue } Write-Host " Searching: $searchRoot" $found = Get-ChildItem -Path $searchRoot -Filter "git.exe" -Recurse -ErrorAction SilentlyContinue -Depth 4 | Where-Object { $_.FullName -match '\\cmd\\git\.exe$' } | Select-Object -First 1 if ($found) { $gitDir = Split-Path $found.FullName -Parent $env:PATH = "$gitDir;$env:PATH" Write-Host "✓ Found git: $gitDir" $gitFound = $true break } } if (-not $gitFound) { Write-Host "##[warning]Git not found on agent" Write-Host "##[warning]Skipping AI Code Review - git is required but not available" Write-Host "##vso[task.setvariable variable=HasAIReviewResults]false" exit 0 } } } git --version # ========================================= # STEP 2: Clone repository to temp folder # ========================================= Write-Host "`n--- Step 2: Cloning repository ---" $tempRoot = Join-Path $env:TEMP "ai-code-review-$BuildId" if (Test-Path $tempRoot) { Remove-Item $tempRoot -Recurse -Force } New-Item -ItemType Directory -Path $tempRoot -Force | Out-Null Write-Host "Created temp directory: $tempRoot" $sourceBranch = $SourceBranch -replace '^refs/heads/', '' $baseBranch = $BaseBranch -replace '^origin/', '' # Prepare authenticated URL $cloneUrl = $RepositoryUri if ($AccessToken -and -not $AccessToken.StartsWith('$(')) { $cloneUrl = $RepositoryUri -replace 'https://[^@]+@', 'https://' $cloneUrl = $cloneUrl -replace 'https://', "https://PAT:$AccessToken@" Write-Host "Using authenticated clone" } else { Write-Host "##[warning]System.AccessToken not available" } Write-Host "Repository: $RepositoryUri" Write-Host "Source Branch: $sourceBranch" Write-Host "Base Branch: $baseBranch" Set-Location $tempRoot git config --global credential.useHttpPath true # Clone based on build reason if ($BuildReason -eq "PullRequest") { Write-Host "PR build detected - cloning base branch first" git clone --depth=50 --single-branch --branch $baseBranch $cloneUrl repo 2>&1 | ForEach-Object { Write-Host $_ } if ($LASTEXITCODE -ne 0) { Write-Host "##[warning]Failed to clone repository - skipping AI review" Write-Host "##vso[task.setvariable variable=HasAIReviewResults]false" exit 0 } Set-Location (Join-Path $tempRoot "repo") git fetch origin $SourceBranch --depth=50 2>&1 | ForEach-Object { Write-Host $_ } if ($LASTEXITCODE -eq 0) { git checkout FETCH_HEAD 2>&1 | ForEach-Object { Write-Host $_ } Write-Host "✓ PR merge commit checked out" } } else { git clone --depth=50 --single-branch --branch $sourceBranch $cloneUrl repo 2>&1 | ForEach-Object { Write-Host $_ } if ($LASTEXITCODE -ne 0) { Write-Host "##[warning]Failed to clone repository - skipping AI review" Write-Host "##vso[task.setvariable variable=HasAIReviewResults]false" exit 0 } Set-Location (Join-Path $tempRoot "repo") git fetch origin "${baseBranch}:${baseBranch}" --depth=50 2>&1 | ForEach-Object { Write-Host $_ } if ($LASTEXITCODE -ne 0) { git fetch origin 2>&1 | ForEach-Object { Write-Host $_ } } } $repoPath = Get-Location Write-Host "`n✓ Repository ready at: $repoPath" # ========================================= # STEP 3: Prepare for AI Code Review # ========================================= Write-Host "`n--- Step 3: Preparing AI Code Review ---" Write-Host "AI will analyze the code changes and provide statistics + guidelines in the review" # Determine rules path $rulesPath = if ($CustomRulesPath -ne '') { $CustomRulesPath } else { "$BuildSourcesDirectory/.devops/code-review/rules" } Write-Host "Rules path: $rulesPath" # ========================================= # STEP 4: Execute AI Code Review # ========================================= Write-Host "`n--- Step 4: Running AI Code Review ---" $reviewParams = @{ BaseBranch = $BaseBranch Provider = $Provider SeverityFailBuild = $SeverityFailBuild } if ($CustomRulesPath -ne '') { $reviewParams['RulesPath'] = $CustomRulesPath Write-Host "Using custom rules from: $CustomRulesPath" } else { $reviewParams['RulesPath'] = $rulesPath Write-Host "Using iFacto default rules" } if ($CustomConfigPath -ne '') { $reviewParams['ConfigPath'] = $CustomConfigPath } try { $result = Invoke-AICodeReview @reviewParams # Check for deployment errors if (-not $result -or $result.RawResponse -match 'DeploymentNotFound') { Write-Host "##[warning]AI deployment not available - skipping review" Write-Host "##vso[build.addbuildtag]AI-Review-Skipped" Write-Host "##vso[task.setvariable variable=HasAIReviewResults]false" exit 0 } # Store the AI-provided PR comment text (statistics + guidelines) for use in PR comments if ($result.PRCommentText) { Write-Host "`n##[section]PR Comment Content Available" # Store as pipeline variable for the YAML template to use # Encode for safe transport through pipeline variables $encodedComment = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($result.PRCommentText)) Write-Host "##vso[task.setvariable variable=AI_REVIEW_PR_COMMENT]$encodedComment" Write-Host "Stored PR comment text ($($result.PRCommentText.Length) characters)" } Write-Host "`n##[section]Review Complete" Write-Host "`nViolations Summary:" Write-Host " Errors: $($result.ErrorCount)" Write-Host " Warnings: $($result.WarningCount)" Write-Host " Info: $($result.InfoCount)" if ($result.Violations.Count -eq 0) { Write-Host "`n✅ No violations found!" Write-Host "##vso[build.addbuildtag]AI-Review-Passed" Write-Host "##vso[task.setvariable variable=HasAIReviewResults]true" Write-Host "##vso[task.setvariable variable=AI_REVIEW_VIOLATIONS_JSON][]" # Save success report $artifactDir = Join-Path $ArtifactStagingDirectory "AICodeReview" if (-not (Test-Path $artifactDir)) { New-Item -ItemType Directory -Path $artifactDir -Force | Out-Null } $buildDate = Get-Date -Format "yyyy-MM-dd HH:mm:ss" $successReport = "# ✅ AI Code Review - All Clear!`n`n" $successReport += "**Build:** $BuildNumber`n" $successReport += "**Date:** $buildDate`n" $successReport += "**Branch:** $SourceBranch`n" $successReport += "**Commit:** $SourceVersion`n`n" $successReport += "No violations found! 🎉`n" $mdPath = Join-Path $artifactDir "code-review-report.md" $successReport | Out-File $mdPath -Encoding UTF8 exit 0 } else { # Output violations $adoOutput = $result.Violations | ConvertTo-ADOLogFormat Write-Host $adoOutput # Store violations for PR comment $violationsJson = $result.Violations | ConvertTo-Json -Compress -Depth 10 Write-Host "##vso[task.setvariable variable=AI_REVIEW_VIOLATIONS_JSON]$violationsJson" Write-Host "##vso[task.setvariable variable=HasAIReviewResults]true" # Save artifacts $artifactDir = Join-Path $ArtifactStagingDirectory "AICodeReview" if (-not (Test-Path $artifactDir)) { New-Item -ItemType Directory -Path $artifactDir -Force | Out-Null } $jsonPath = Join-Path $artifactDir "code-review-results.json" $result | ConvertTo-Json -Depth 10 | Out-File $jsonPath -Encoding UTF8 $rawPath = Join-Path $artifactDir "ai-raw-response.txt" $result.RawResponse | Out-File $rawPath -Encoding UTF8 # Create markdown report $mdPath = Join-Path $artifactDir "code-review-report.md" $buildDate = Get-Date -Format "yyyy-MM-dd HH:mm:ss" $mdReport = "# AI Code Review Report`n`n" $mdReport += "**Build:** $BuildNumber`n" $mdReport += "**Date:** $buildDate`n" $mdReport += "**Branch:** $SourceBranch`n" $mdReport += "**Commit:** $SourceVersion`n`n" $mdReport += "## Summary`n`n" $mdReport += "- **Errors:** $($result.ErrorCount)`n" $mdReport += "- **Warnings:** $($result.WarningCount)`n" $mdReport += "- **Info:** $($result.InfoCount)`n" $mdReport += "- **Total:** $($result.Violations.Count)`n`n" $mdReport += "## Violations`n`n" foreach ($violation in ($result.Violations | Sort-Object -Property severity, file, line)) { $emoji = switch ($violation.severity) { 'error' { '❌' } 'warning' { '⚠️' } 'info' { 'ℹ️' } } $mdReport += "`n### $emoji $($violation.severity.ToUpper()): $($violation.file)`n`n" $mdReport += "**Line:** $($violation.line)`n" $mdReport += "**Message:** $($violation.message)`n`n" if ($violation.suggestion) { $mdReport += "**Suggestion:** $($violation.suggestion)`n`n" } } $mdReport | Out-File $mdPath -Encoding UTF8 # Add build tags if ($result.ErrorCount -gt 0) { Write-Host "##vso[build.addbuildtag]AI-Review-Errors" } if ($result.WarningCount -gt 0) { Write-Host "##vso[build.addbuildtag]AI-Review-Warnings" } # Never fail build Write-Host "`n##[warning]Violations found - build continues (informational only)" Write-Host "##vso[build.addbuildtag]AI-Review-Violations-Found" exit 0 } } catch { $errorMessage = $_.Exception.Message # Check for deployment errors if ($errorMessage -match 'DeploymentNotFound') { Write-Host "##[warning]AI deployment not available - skipping review" Write-Host "##vso[build.addbuildtag]AI-Review-Skipped" Write-Host "##vso[task.setvariable variable=HasAIReviewResults]false" exit 0 } Write-Host "##[warning]AI Code Review encountered an error" Write-Host "##[warning]Error: $errorMessage" Write-Host "##[warning]Build continues - AI review is optional" Write-Host "##vso[build.addbuildtag]AI-Review-Error" Write-Host "##vso[task.setvariable variable=HasAIReviewResults]false" exit 0 } } |