diff --git a/CopilotCodeReviewV1/index.ts b/CopilotCodeReviewV1/index.ts index dfe0726..c029940 100644 --- a/CopilotCodeReviewV1/index.ts +++ b/CopilotCodeReviewV1/index.ts @@ -73,6 +73,19 @@ async function run(): Promise { // Get Azure DevOps authentication settings const useSystemAccessToken = tl.getBoolInput('useSystemAccessToken', false); const azureDevOpsPat = tl.getInput('azureDevOpsPat'); + const onPremise = tl.getBoolInput('onPremise', false); + const systemCollectionUri = tl.getVariable('System.CollectionUri') || + process.env['SYSTEM_COLLECTIONURI'] || + ''; + + process.env['AZUREDEVOPS_ONPREMISE'] = onPremise ? 'true' : 'false'; + + if (onPremise && !systemCollectionUri) { + tl.setResult(tl.TaskResult.Failed, + 'On-Premise mode is enabled, but System.CollectionUri is not available. ' + + 'Ensure the pipeline provides $(System.CollectionUri) or disable On-Premise mode.'); + return; + } // Determine which token and auth type to use let azureDevOpsToken: string; @@ -106,19 +119,30 @@ async function run(): Promise { let project = tl.getInput('project'); let repository = tl.getInput('repository'); - // Auto-detect organization from System.CollectionUri if not provided - // CollectionUri format: https://dev.azure.com/orgname/ or https://orgname.visualstudio.com/ - if (!organization) { - const collectionUri = tl.getVariable('System.CollectionUri'); - if (collectionUri) { - const devAzureMatch = collectionUri.match(/https:\/\/dev\.azure\.com\/([^\/]+)/); - const vstsMatch = collectionUri.match(/https:\/\/([^\.]+)\.visualstudio\.com/); - if (devAzureMatch) { - organization = devAzureMatch[1]; - console.log(`Auto-detected organization from CollectionUri: ${organization}`); - } else if (vstsMatch) { - organization = vstsMatch[1]; - console.log(`Auto-detected organization from CollectionUri: ${organization}`); + // Auto-detect organization/collection from System.CollectionUri if not provided + // CollectionUri format examples: + // https://dev.azure.com/orgname/ + // https://orgname.visualstudio.com/ + // https://tfs.contoso.com/tfs/DefaultCollection/ + if (!organization && systemCollectionUri) { + const devAzureMatch = systemCollectionUri.match(/https:\/\/dev\.azure\.com\/([^\/]+)/); + const vstsMatch = systemCollectionUri.match(/https:\/\/([^\.]+)\.visualstudio\.com/); + if (devAzureMatch) { + organization = devAzureMatch[1]; + console.log(`Auto-detected organization from CollectionUri: ${organization}`); + } else if (vstsMatch) { + organization = vstsMatch[1]; + console.log(`Auto-detected organization from CollectionUri: ${organization}`); + } else { + try { + const url = new URL(systemCollectionUri); + const segments = url.pathname.split('/').filter(Boolean); + if (segments.length > 0) { + organization = segments[segments.length - 1]; + console.log(`Auto-detected collection from CollectionUri: ${organization}`); + } + } catch { + // Ignore parse errors; organization may be provided explicitly } } } diff --git a/CopilotCodeReviewV1/package-lock.json b/CopilotCodeReviewV1/package-lock.json index 576db04..a0e061a 100644 --- a/CopilotCodeReviewV1/package-lock.json +++ b/CopilotCodeReviewV1/package-lock.json @@ -1,12 +1,12 @@ { "name": "copilot-code-review-task", - "version": "1.0.12", + "version": "1.4.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "copilot-code-review-task", - "version": "1.0.12", + "version": "1.4.2", "license": "MIT", "dependencies": { "azure-pipelines-task-lib": "^4.17.3", diff --git a/CopilotCodeReviewV1/scripts/Add-AzureDevOpsPRComment.ps1 b/CopilotCodeReviewV1/scripts/Add-AzureDevOpsPRComment.ps1 index 56293e9..d79fd8b 100644 --- a/CopilotCodeReviewV1/scripts/Add-AzureDevOpsPRComment.ps1 +++ b/CopilotCodeReviewV1/scripts/Add-AzureDevOpsPRComment.ps1 @@ -131,6 +131,9 @@ param( [int]$IterationId ) +# Shared URL helpers +. "$PSScriptRoot\AzureDevOpsUrl.ps1" + #region Helper Functions function Get-AuthorizationHeader { @@ -229,7 +232,11 @@ function Format-AzureDevOpsFilePath { #region Main Logic $headers = Get-AuthorizationHeader -Token $Token -AuthType $AuthType -$baseUrl = "https://dev.azure.com/$Organization/$Project/_apis/git/repositories/$Repository/pullrequests/$Id" +$baseUrls = Get-AzureDevOpsBaseUrls -Project $Project -Organization $Organization +if ($null -eq $baseUrls) { + exit 1 +} +$baseUrl = "$($baseUrls.ApiBaseUrl)/git/repositories/$Repository/pullrequests/$Id" $apiVersion = "api-version=7.1" # First, verify the PR exists @@ -401,7 +408,7 @@ else { } # Provide link to the PR -$webUrl = "https://dev.azure.com/$Organization/$Project/_git/$Repository/pullrequest/$Id" +$webUrl = "$($baseUrls.WebBaseUrl)/_git/$Repository/pullrequest/$Id" Write-Host "`nView PR: $webUrl" -ForegroundColor Cyan #endregion diff --git a/CopilotCodeReviewV1/scripts/AzureDevOpsUrl.ps1 b/CopilotCodeReviewV1/scripts/AzureDevOpsUrl.ps1 new file mode 100644 index 0000000..4571dc2 --- /dev/null +++ b/CopilotCodeReviewV1/scripts/AzureDevOpsUrl.ps1 @@ -0,0 +1,60 @@ +<# +.SYNOPSIS + Shared helpers for building Azure DevOps REST and web base URLs. + +.DESCRIPTION + Centralizes URL construction for Azure DevOps Services and Server (on-prem). + Uses $env:AZUREDEVOPS_ONPREMISE to determine whether to use on-prem URLs. +#> + +function Get-AzureDevOpsBaseUrls { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Project, + + [Parameter(Mandatory = $false)] + [string]$Organization, + + [Parameter(Mandatory = $false)] + [switch]$Silent + ) + + $onPremise = $false + if ($env:AZUREDEVOPS_ONPREMISE -match '^(?i:true|1|yes)$') { + $onPremise = $true + } + + $collectionUri = $null + if ($onPremise) { + $collectionUri = $env:SYSTEM_COLLECTIONURI + if ([string]::IsNullOrWhiteSpace($collectionUri)) { + if (-not $Silent) { + Write-Error "SYSTEM_COLLECTIONURI is required when On-Premise mode is enabled." + } + return $null + } + $collectionUri = $collectionUri.Trim() + if (-not $collectionUri.EndsWith('/')) { + $collectionUri += '/' + } + } + else { + if ([string]::IsNullOrWhiteSpace($Organization)) { + if (-not $Silent) { + Write-Error "Organization is required when On-Premise mode is disabled." + } + return $null + } + } + + $apiBaseUrl = if ($onPremise) { "$collectionUri$Project/_apis" } else { "https://dev.azure.com/$Organization/$Project/_apis" } + $webBaseUrl = if ($onPremise) { "$collectionUri$Project" } else { "https://dev.azure.com/$Organization/$Project" } + + return [PSCustomObject]@{ + ApiBaseUrl = $apiBaseUrl + WebBaseUrl = $webBaseUrl + OnPremise = $onPremise + CollectionUri = $collectionUri + } +} diff --git a/CopilotCodeReviewV1/scripts/Delete-CopilotComment.ps1 b/CopilotCodeReviewV1/scripts/Delete-CopilotComment.ps1 index b81d168..bca0700 100644 --- a/CopilotCodeReviewV1/scripts/Delete-CopilotComment.ps1 +++ b/CopilotCodeReviewV1/scripts/Delete-CopilotComment.ps1 @@ -49,6 +49,9 @@ param( [int]$CommentId ) +# Shared URL helpers +. "$PSScriptRoot\AzureDevOpsUrl.ps1" + # Wrap entire script in try/catch for silent failure try { # Read credentials from environment variables @@ -61,7 +64,6 @@ try { # Validate required environment variables if ([string]::IsNullOrEmpty($token) -or - [string]::IsNullOrEmpty($organization) -or [string]::IsNullOrEmpty($project) -or [string]::IsNullOrEmpty($repository) -or [string]::IsNullOrEmpty($prId)) { @@ -90,7 +92,11 @@ try { } # Build the API URL for deleting a comment - $baseUrl = "https://dev.azure.com/$organization/$project/_apis" + $baseUrls = Get-AzureDevOpsBaseUrls -Project $project -Organization $organization -Silent + if ($null -eq $baseUrls) { + exit 0 + } + $baseUrl = $baseUrls.ApiBaseUrl $uri = "$baseUrl/git/repositories/$repository/pullrequests/$prId/threads/$ThreadId/comments/$CommentId`?api-version=7.1" # Send DELETE request diff --git a/CopilotCodeReviewV1/scripts/Get-AzureDevOpsPR.ps1 b/CopilotCodeReviewV1/scripts/Get-AzureDevOpsPR.ps1 index fe5c3b6..d578a5e 100644 --- a/CopilotCodeReviewV1/scripts/Get-AzureDevOpsPR.ps1 +++ b/CopilotCodeReviewV1/scripts/Get-AzureDevOpsPR.ps1 @@ -86,6 +86,9 @@ param( [string]$OutputFile ) +# Shared URL helpers +. "$PSScriptRoot\AzureDevOpsUrl.ps1" + #region Helper Functions function Write-Output-Line { @@ -275,7 +278,11 @@ $script:OutputToFile = -not [string]::IsNullOrEmpty($OutputFile) $script:OutputBuilder = [System.Text.StringBuilder]::new() $headers = Get-AuthorizationHeader -Token $Token -AuthType $AuthType -$baseUrl = "https://dev.azure.com/$Organization/$Project/_apis" +$baseUrls = Get-AzureDevOpsBaseUrls -Project $Project -Organization $Organization +if ($null -eq $baseUrls) { + exit 1 +} +$baseUrl = $baseUrls.ApiBaseUrl $apiVersion = "api-version=7.1" # If a specific PR ID is provided, get detailed information @@ -487,7 +494,7 @@ if ($Id -gt 0) { } Write-Output-Line "`n[Links]" -ForegroundColor Yellow - $webUrl = "https://dev.azure.com/$Organization/$Project/_git/$($pr.repository.name)/pullrequest/$($pr.pullRequestId)" + $webUrl = "$($baseUrls.WebBaseUrl)/_git/$($pr.repository.name)/pullrequest/$($pr.pullRequestId)" Write-Output-Line " Web URL: $webUrl" Write-Output-Line ("`n" + ("=" * 80)) -ForegroundColor DarkGray diff --git a/CopilotCodeReviewV1/scripts/Get-AzureDevOpsPRChanges.ps1 b/CopilotCodeReviewV1/scripts/Get-AzureDevOpsPRChanges.ps1 index d3ea18f..72d1732 100644 --- a/CopilotCodeReviewV1/scripts/Get-AzureDevOpsPRChanges.ps1 +++ b/CopilotCodeReviewV1/scripts/Get-AzureDevOpsPRChanges.ps1 @@ -73,6 +73,9 @@ param( [string]$OutputFile ) +# Shared URL helpers +. "$PSScriptRoot\AzureDevOpsUrl.ps1" + #region Helper Functions function Write-Output-Line { @@ -186,7 +189,11 @@ $script:OutputToFile = -not [string]::IsNullOrEmpty($OutputFile) $script:OutputBuilder = [System.Text.StringBuilder]::new() $headers = Get-AuthorizationHeader -Token $Token -AuthType $AuthType -$baseUrl = "https://dev.azure.com/$Organization/$Project/_apis/git/repositories/$Repository/pullrequests/$Id" +$baseUrls = Get-AzureDevOpsBaseUrls -Project $Project -Organization $Organization +if ($null -eq $baseUrls) { + exit 1 +} +$baseUrl = "$($baseUrls.ApiBaseUrl)/git/repositories/$Repository/pullrequests/$Id" $apiVersion = "api-version=7.1" # Verify the PR exists @@ -299,7 +306,7 @@ else { Write-Output-Line ("`n" + ("=" * 80)) -ForegroundColor DarkGray # Provide link to the PR -$webUrl = "https://dev.azure.com/$Organization/$Project/_git/$Repository/pullrequest/$Id" +$webUrl = "$($baseUrls.WebBaseUrl)/_git/$Repository/pullrequest/$Id" Write-Host "`nView PR: $webUrl" -ForegroundColor Cyan if ($script:OutputToFile) { $script:OutputBuilder.AppendLine("`nView PR: $webUrl") | Out-Null diff --git a/CopilotCodeReviewV1/scripts/Update-CopilotComment.ps1 b/CopilotCodeReviewV1/scripts/Update-CopilotComment.ps1 index e84f5dd..76129cc 100644 --- a/CopilotCodeReviewV1/scripts/Update-CopilotComment.ps1 +++ b/CopilotCodeReviewV1/scripts/Update-CopilotComment.ps1 @@ -69,6 +69,9 @@ param( [string]$Content ) +# Shared URL helpers +. "$PSScriptRoot\AzureDevOpsUrl.ps1" + # Wrap entire script in try/catch for silent failure try { # Validate that at least one update is requested @@ -93,7 +96,6 @@ try { # Validate required environment variables if ([string]::IsNullOrEmpty($token) -or - [string]::IsNullOrEmpty($organization) -or [string]::IsNullOrEmpty($project) -or [string]::IsNullOrEmpty($repository) -or [string]::IsNullOrEmpty($prId)) { @@ -121,7 +123,11 @@ try { } } - $baseUrl = "https://dev.azure.com/$organization/$project/_apis" + $baseUrls = Get-AzureDevOpsBaseUrls -Project $project -Organization $organization -Silent + if ($null -eq $baseUrls) { + exit 0 + } + $baseUrl = $baseUrls.ApiBaseUrl # Update thread status if specified if (-not [string]::IsNullOrEmpty($Status)) { diff --git a/CopilotCodeReviewV1/task.json b/CopilotCodeReviewV1/task.json index 7226ba3..4e53c75 100644 --- a/CopilotCodeReviewV1/task.json +++ b/CopilotCodeReviewV1/task.json @@ -37,6 +37,14 @@ "defaultValue": false, "helpMarkDown": "When enabled, uses the pipeline's System.AccessToken (OAuth) instead of a PAT. This is the Microsoft-recommended authentication method for Azure DevOps Services. **Note**: The Build Service identity must have 'Contribute to pull requests' permission on the repository. Not supported for Azure DevOps Server (on-prem)." }, + { + "name": "onPremise", + "type": "boolean", + "label": "On-Premise (Azure DevOps Server)", + "required": false, + "defaultValue": false, + "helpMarkDown": "Enable for Azure DevOps Server (on-prem). When enabled, the task uses $(System.CollectionUri) as the API base URL." + }, { "name": "organization", "type": "string", @@ -127,4 +135,4 @@ "allowed": [] } } -} \ No newline at end of file +}