Skip to content

Commit a4f3e82

Browse files
committed
Route ADO API host derivation through a tested helper (groundwork for on-prem, #12)
Extract the inline organization-URL normalization and the hardcoded dev.azure.com subdomain derivation into a single Resolve-AdoApiHosts helper. - Cloud (Azure DevOps Services) behavior is preserved exactly and pinned by tests (short name, dev.azure.com URL, *.visualstudio.com URL). - On-prem (Azure DevOps Server) collection URLs are now recognized and produce collection-relative API hosts instead of the broken vssps.dev.azure.com/<full-url> strings the old inline code produced. - A run against an on-prem URL emits a warning that this path is experimental. EXPERIMENTAL / RFC: this addresses URL derivation only. On-prem auth still uses an Entra ID token via 'az account get-access-token' (typically rejected on-prem; PAT support is the remaining blocker), and Services-only checks (e.g. audit log streaming) will report NOT CHECKED. Opening as groundwork for #12.
1 parent 0abe4fc commit a4f3e82

4 files changed

Lines changed: 146 additions & 13 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ and this project adheres to Semantic Versioning.
88
## [Unreleased]
99

1010
### Added
11+
- Experimental Azure DevOps Server (on-prem) URL handling: organization/collection input is now routed through a single, tested `Resolve-AdoApiHosts` helper that derives the graph/extension/audit/feeds API hosts. Cloud behavior is unchanged; on-prem collection URLs derive collection-relative hosts. NOTE: on-prem authentication (PAT) and Services-only checks are not yet supported — see issue #12.
1112
- Executive summary now includes an Organization Extensions section that lists all extensions with Installed vs Default classification and installed-first ordering.
1213
- Top navigation now includes an Extensions anchor placed before Run Comparison for faster access to extension findings.
1314

invoke-adoqr.ps1

Lines changed: 86 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5529,6 +5529,78 @@ function Import-AdoqrSettings {
55295529
return $settings
55305530
}
55315531

5532+
function Resolve-AdoApiHosts {
5533+
<#
5534+
.SYNOPSIS
5535+
Derives the organization URL plus the specialized ADO REST API host URLs
5536+
from the -Organization input.
5537+
.DESCRIPTION
5538+
Azure DevOps Services (cloud) serves several APIs from dedicated
5539+
subdomains keyed by org short name: vssps (graph/identity), extmgmt
5540+
(extensions), auditservice (audit log), and feeds (packaging). Azure
5541+
DevOps Server (on-premises) has no such subdomains — those APIs are
5542+
served collection-relative under the same host. Centralizing the mapping
5543+
here keeps the rest of the script host-agnostic.
5544+
5545+
EXPERIMENTAL: on-prem support here covers URL derivation only. It does
5546+
NOT yet solve authentication — the script still acquires an Entra ID
5547+
(AAD) bearer token via `az account get-access-token`, which a typical
5548+
on-prem deployment will not accept (those use PAT/Windows auth). Some
5549+
controls (e.g. audit log streaming) are Services-only features with no
5550+
on-prem equivalent and are expected to report NOT CHECKED. See issue #12.
5551+
.PARAMETER Organization
5552+
The -Organization value: a short name, a cloud org URL
5553+
(https://dev.azure.com/org or https://org.visualstudio.com), or an
5554+
on-prem collection URL (e.g. https://server/tfs/DefaultCollection).
5555+
.OUTPUTS
5556+
Hashtable with keys OrgUrl, OrgShortName, VsspsUrl, ExtMgmtUrl,
5557+
AuditUrl, FeedsUrl, and IsOnPrem.
5558+
#>
5559+
[CmdletBinding()]
5560+
param(
5561+
[Parameter(Mandatory)][string]$Organization
5562+
)
5563+
5564+
# A bare short name always refers to the cloud service.
5565+
if ($Organization -notmatch '^https?://') {
5566+
$orgUrl = "https://dev.azure.com/$Organization"
5567+
}
5568+
else {
5569+
$orgUrl = $Organization.TrimEnd('/')
5570+
}
5571+
5572+
$isCloud = ($orgUrl -match '^https?://dev\.azure\.com/') -or
5573+
($orgUrl -match '^https?://[^/]+\.visualstudio\.com')
5574+
5575+
if ($isCloud) {
5576+
$orgShortName = $orgUrl -replace '^https?://dev\.azure\.com/', '' -replace '^https?://([^.]+)\.visualstudio\.com.*', '$1'
5577+
return @{
5578+
OrgUrl = $orgUrl
5579+
OrgShortName = $orgShortName
5580+
VsspsUrl = "https://vssps.dev.azure.com/$orgShortName"
5581+
ExtMgmtUrl = "https://extmgmt.dev.azure.com/$orgShortName"
5582+
AuditUrl = "https://auditservice.dev.azure.com/$orgShortName"
5583+
FeedsUrl = "https://feeds.dev.azure.com/$orgShortName"
5584+
IsOnPrem = $false
5585+
}
5586+
}
5587+
5588+
# On-prem Azure DevOps Server: graph and packaging APIs are collection-
5589+
# relative under the same host, so the collection URL is the base for all of
5590+
# them. Extension management and audit have no standard on-prem endpoint, so
5591+
# reuse the collection URL and let those specific checks degrade gracefully.
5592+
$orgShortName = ($orgUrl -split '/') | Where-Object { $_ } | Select-Object -Last 1
5593+
return @{
5594+
OrgUrl = $orgUrl
5595+
OrgShortName = $orgShortName
5596+
VsspsUrl = $orgUrl
5597+
ExtMgmtUrl = $orgUrl
5598+
AuditUrl = $orgUrl
5599+
FeedsUrl = $orgUrl
5600+
IsOnPrem = $true
5601+
}
5602+
}
5603+
55325604
#endregion
55335605

55345606
#region Main
@@ -5540,21 +5612,22 @@ if ($_userSettings.ContainsKey('InactiveRepoDays')) {
55405612
Write-Verbose "Settings: InactiveRepoDays overridden to $($script:InactiveRepoDays) (from adoqr.settings.psd1)"
55415613
}
55425614

5543-
# Normalize organization URL
5544-
if ($Organization -notmatch '^https?://') {
5545-
$OrgUrl = "https://dev.azure.com/$Organization"
5546-
} else {
5547-
$OrgUrl = $Organization.TrimEnd('/')
5615+
# Normalize organization URL and derive the specialized ADO REST API hosts.
5616+
# Cloud uses dedicated subdomains; on-prem Azure DevOps Server serves them
5617+
# collection-relative (experimental — see Resolve-AdoApiHosts and issue #12).
5618+
$adoHosts = Resolve-AdoApiHosts -Organization $Organization
5619+
$OrgUrl = $adoHosts.OrgUrl
5620+
$OrgShortName = $adoHosts.OrgShortName
5621+
$script:VsspsUrl = $adoHosts.VsspsUrl
5622+
$script:ExtMgmtUrl = $adoHosts.ExtMgmtUrl
5623+
$script:AuditUrl = $adoHosts.AuditUrl
5624+
$script:FeedsUrl = $adoHosts.FeedsUrl
5625+
$script:IsOnPrem = $adoHosts.IsOnPrem
5626+
5627+
if ($script:IsOnPrem) {
5628+
Write-Warning "On-prem Azure DevOps Server URL detected ($OrgUrl). On-prem support is experimental: authentication still uses an Entra ID token via 'az login' (typically rejected on-prem), and Services-only checks such as audit log streaming will report NOT CHECKED. See https://github.com/microsoft/adoqr/issues/12."
55485629
}
55495630

5550-
$OrgShortName = $OrgUrl -replace '^https?://dev\.azure\.com/', '' -replace '^https?://([^.]+)\.visualstudio\.com.*', '$1'
5551-
5552-
# Subdomain URLs for specialized ADO REST APIs
5553-
$script:VsspsUrl = "https://vssps.dev.azure.com/$OrgShortName"
5554-
$script:ExtMgmtUrl = "https://extmgmt.dev.azure.com/$OrgShortName"
5555-
$script:AuditUrl = "https://auditservice.dev.azure.com/$OrgShortName"
5556-
$script:FeedsUrl = "https://feeds.dev.azure.com/$OrgShortName"
5557-
55585631
# Ensure output directory exists — create timestamped subfolder per run
55595632
$timestamp = Get-Date -Format "yyyy-MM-dd-HHmmss"
55605633
$orgSafeForPath = ($OrgShortName -replace '[^a-zA-Z0-9\-]', '-').ToLower().Trim('-')

tests/ApiHosts.Tests.ps1

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '5.0.0' }
2+
3+
BeforeAll {
4+
. $PSScriptRoot/_Bootstrap.ps1
5+
}
6+
7+
Describe 'Resolve-AdoApiHosts' {
8+
Context 'Azure DevOps Services (cloud)' {
9+
# These cases pin the exact behavior of the previous inline derivation
10+
# so the refactor cannot regress the cloud path.
11+
It 'expands a bare short name to the dev.azure.com org URL and subdomains' {
12+
$h = Resolve-AdoApiHosts -Organization 'MyOrg'
13+
$h.OrgUrl | Should -Be 'https://dev.azure.com/MyOrg'
14+
$h.OrgShortName | Should -Be 'MyOrg'
15+
$h.VsspsUrl | Should -Be 'https://vssps.dev.azure.com/MyOrg'
16+
$h.ExtMgmtUrl | Should -Be 'https://extmgmt.dev.azure.com/MyOrg'
17+
$h.AuditUrl | Should -Be 'https://auditservice.dev.azure.com/MyOrg'
18+
$h.FeedsUrl | Should -Be 'https://feeds.dev.azure.com/MyOrg'
19+
$h.IsOnPrem | Should -BeFalse
20+
}
21+
22+
It 'accepts a full dev.azure.com org URL and trims a trailing slash' {
23+
$h = Resolve-AdoApiHosts -Organization 'https://dev.azure.com/MyOrg/'
24+
$h.OrgUrl | Should -Be 'https://dev.azure.com/MyOrg'
25+
$h.OrgShortName | Should -Be 'MyOrg'
26+
$h.VsspsUrl | Should -Be 'https://vssps.dev.azure.com/MyOrg'
27+
$h.IsOnPrem | Should -BeFalse
28+
}
29+
30+
It 'maps a legacy *.visualstudio.com URL to the short name' {
31+
$h = Resolve-AdoApiHosts -Organization 'https://myorg.visualstudio.com'
32+
$h.OrgShortName | Should -Be 'myorg'
33+
$h.VsspsUrl | Should -Be 'https://vssps.dev.azure.com/myorg'
34+
$h.FeedsUrl | Should -Be 'https://feeds.dev.azure.com/myorg'
35+
$h.IsOnPrem | Should -BeFalse
36+
}
37+
}
38+
39+
Context 'Azure DevOps Server (on-prem, experimental)' {
40+
It 'treats a collection URL as on-prem and serves APIs collection-relative' {
41+
$h = Resolve-AdoApiHosts -Organization 'https://tfs.contoso.com/DefaultCollection'
42+
$h.OrgUrl | Should -Be 'https://tfs.contoso.com/DefaultCollection'
43+
$h.OrgShortName | Should -Be 'DefaultCollection'
44+
$h.VsspsUrl | Should -Be 'https://tfs.contoso.com/DefaultCollection'
45+
$h.ExtMgmtUrl | Should -Be 'https://tfs.contoso.com/DefaultCollection'
46+
$h.FeedsUrl | Should -Be 'https://tfs.contoso.com/DefaultCollection'
47+
$h.IsOnPrem | Should -BeTrue
48+
}
49+
50+
It 'handles a /tfs/ virtual directory and trailing slash' {
51+
$h = Resolve-AdoApiHosts -Organization 'https://server/tfs/MyCollection/'
52+
$h.OrgUrl | Should -Be 'https://server/tfs/MyCollection'
53+
$h.OrgShortName | Should -Be 'MyCollection'
54+
$h.VsspsUrl | Should -Be 'https://server/tfs/MyCollection'
55+
$h.IsOnPrem | Should -BeTrue
56+
}
57+
}
58+
}

tests/_Bootstrap.ps1

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ $wantedFns = @(
3535
'Get-AdoqrHeaderCss'
3636
'Get-AdoqrHeaderHtml'
3737
'Import-AdoqrSettings'
38+
'Resolve-AdoApiHosts'
3839
)
3940

4041
$funcs = $tree.FindAll({

0 commit comments

Comments
 (0)