Public/Common/Get-Organisation.ps1

function Get-Organisation {
    <#
    .SYNOPSIS
    Returns organisation entries from organisations.json with PATs resolved from secrets.
 
    .DESCRIPTION
    Reads the organisations.json data file (same shape as data\sample\organisations.json) and
    resolves each entry's PAT in this order:
 
      1. A non-empty inline 'pat' property in organisations.json (back-compat with standalone
         data files; customer repos commit organisations.json WITHOUT pats).
      2. A secrets.json entry matched by url (trailing-slash and case insensitive) or org name.
      3. The derived environment variable AZDO_PAT_<ORG>, where <ORG> comes from the secrets
         entry's Org or, failing that, the last path segment of the url.
      4. Otherwise pat stays empty and a warning names the url and the secrets file to fill in.
 
    Placeholder values ('somePat', '<...>', 'REPLACE_WITH_PAT') are treated as absent. Entries
    with a resolved PAT also get an 'authHeader' property (via Get-AzureDevOpsAuthHeader) ready
    for Invoke-RestMethod. Never log or print the returned pat/authHeader values.
 
    .PARAMETER Url
    Return only the organisation whose url matches (trailing-slash and case insensitive).
 
    .PARAMETER IncludeDisabled
    Include entries with enabled = false (default: enabled only).
 
    .PARAMETER Path
    Path to organisations.json. Defaults to <workspace DataFolder>\organisations.json when a
    workspace is initialised; legacy scripts can pass -Path "$dataFolder\organisations.json".
 
    .PARAMETER SecretsPath
    Path to secrets.json. Defaults to the workspace secrets path when initialised.
 
    .EXAMPLE
    $orgs = Get-Organisation
    foreach ($org in $orgs) { Invoke-RestMethod -Uri "$($org.url)/_apis/projects?$queryString" -Headers $org.authHeader }
    #>

    [CmdletBinding()]
    param(
        [string]$Url,
        [switch]$IncludeDisabled,
        [string]$Path,
        [string]$SecretsPath
    )

    if (-not $Path) {
        if (-not $script:Workspace) {
            throw "No -Path given and no workspace initialised. Run Initialize-AutomationWorkspace (or init.ps1) first, or pass -Path to organisations.json."
        }
        $Path = Join-Path $script:Workspace.DataFolder 'organisations.json'
    }
    if (-not (Test-Path -LiteralPath $Path)) {
        throw "Organisations file not found: $Path"
    }
    if (-not $SecretsPath -and $script:Workspace) {
        $SecretsPath = $script:Workspace.SecretsPath
    }

    $secrets = if ($SecretsPath) { Get-AutomationSecrets -SecretsPath $SecretsPath } else { @() }
    $organisations = (Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json).organisations

    $normalise = { param([string]$value) if ($value) { $value.TrimEnd('/').ToLowerInvariant() } }
    $isPlaceholder = { param([string]$value) [string]::IsNullOrWhiteSpace($value) -or $value -like '<*>' -or $value -in 'somePat', 'REPLACE_WITH_PAT' }

    $results = foreach ($org in @($organisations)) {
        if (-not $IncludeDisabled -and -not $org.enabled) { continue }
        if ($Url -and (& $normalise $org.url) -ne (& $normalise $Url)) { continue }

        $secretsMatch = $secrets | Where-Object {
            (& $normalise $_.Url) -eq (& $normalise $org.url) -or
            ($_.Org -and $org.url -and (& $normalise $org.url).EndsWith('/' + $_.Org.ToLowerInvariant()))
        } | Select-Object -First 1

        $orgName = if ($secretsMatch -and $secretsMatch.Org) { $secretsMatch.Org }
        elseif ($org.url) { ([uri]$org.url).Segments[-1].Trim('/') }

        $inlinePat = if ($org.PSObject.Properties['pat']) { $org.pat }
        $pat = if (-not (& $isPlaceholder $inlinePat)) { $inlinePat }
        elseif ($secretsMatch -and $secretsMatch.AccessToken) { $secretsMatch.AccessToken }
        elseif ($orgName -and (Get-Item -Path ("Env:{0}" -f (Get-DerivedPatEnvVarName -Org $orgName)) -ErrorAction SilentlyContinue)) {
            (Get-Item -Path ("Env:{0}" -f (Get-DerivedPatEnvVarName -Org $orgName))).Value
        }

        $resolved = $org | Select-Object *
        if (-not $resolved.PSObject.Properties['pat']) {
            $resolved | Add-Member -NotePropertyName 'pat' -NotePropertyValue $null
        }
        $resolved.pat = $pat
        if ($pat) {
            $resolved | Add-Member -NotePropertyName 'authHeader' -NotePropertyValue (Get-AzureDevOpsAuthHeader -Pat $pat) -Force
        }
        else {
            $secretsHint = if ($SecretsPath) { $SecretsPath } else { 'secrets\secrets.json' }
            Write-Warning "No PAT resolved for $($org.url) - add it to $secretsHint or set AZDO_PAT_<ORG>."
        }
        $resolved
    }

    return $results
}