Modules/businessdev.ALbuild.Feeds/Public/Resolve-BcDevOpsContext.ps1
|
function Resolve-BcDevOpsContext { <# .SYNOPSIS Resolves the Azure DevOps organization and project a workspace belongs to. .DESCRIPTION A project-scoped Universal feed can only be reached with BOTH an organization and a project. Azure DevOps answers a request for a project-scoped feed made in organization scope with 'TF1600011: The feed with ID ... doesn't exist' - not "no access" - so a missing project surfaces as a feed that appears not to exist at all, and the search for the cause goes the wrong way. The org/project pair was derived independently in five places: the CLI backend, two MCP call sites, and two Azure DevOps tasks. The git-based ones parsed the remote themselves (one of them with an HTTPS-only regex, so an SSH clone yielded no project), while the task-based ones read the pipeline environment and never looked at git. This is the one place that does both: 1. the workspace's 'git remote get-url origin', parsed by ConvertFrom-BcRemoteUrl (which knows HTTPS, user@HTTPS, legacy visualstudio.com, SSH v3 and SSH host aliases), then 2. the Azure Pipelines environment (SYSTEM_TEAMFOUNDATIONCOLLECTIONURI / SYSTEM_TEAMPROJECT), which is authoritative when there is no git remote to read - a pipeline that checks out without origin, or a workspace assembled from an artifact. Both parts are optional and independent: git may give the organization while the environment gives the project, and either may come back empty. Empty is a valid answer - the caller then falls back to organization scope instead of failing - so this never throws. .PARAMETER WorkspaceRoot The repository folder whose 'origin' remote is read. Omit to skip the git step entirely (useful when only the environment should be consulted). .PARAMETER GitExecutable The git executable to use (default 'git'). .OUTPUTS PSCustomObject with Organization (a full https://dev.azure.com/{org}/ URL, or '') and Project (the project name, or ''), plus Source ('git', 'environment', 'git+environment' or 'none') so a caller can log where the answer came from. .EXAMPLE $ctx = Resolve-BcDevOpsContext -WorkspaceRoot 'D:\Code\DevOps\businessdev.Api.Banking' Resolve-BcExternalDependency -UniversalPackages $cfg.UniversalPackages -Organization $ctx.Organization -Project $ctx.Project -OutputFolder $staging #> [CmdletBinding()] [OutputType([PSCustomObject])] param( [string] $WorkspaceRoot, [string] $GitExecutable = 'git' ) $organization = '' $project = '' $fromGit = $false if ($WorkspaceRoot -and (Test-Path -LiteralPath $WorkspaceRoot)) { # Best-effort: no git, no repository, no 'origin' - all mean "nothing to learn here", not a failure. $remote = '' try { $remote = "$(& $GitExecutable -C $WorkspaceRoot remote get-url origin 2>$null)".Trim() } catch { $remote = '' } if ($remote) { $parsed = ConvertFrom-BcRemoteUrl -RemoteUrl $remote if ($parsed.Organization) { $organization = $parsed.Organization; $fromGit = $true } if ($parsed.Project) { $project = $parsed.Project; $fromGit = $true } } } # The pipeline environment fills only what git did not answer, so an explicit remote keeps winning # for a repository built outside its own project. $fromEnv = $false if (-not $organization -and $env:SYSTEM_TEAMFOUNDATIONCOLLECTIONURI) { $organization = $env:SYSTEM_TEAMFOUNDATIONCOLLECTIONURI $fromEnv = $true } if (-not $project -and $env:SYSTEM_TEAMPROJECT) { $project = $env:SYSTEM_TEAMPROJECT $fromEnv = $true } $source = if ($fromGit -and $fromEnv) { 'git+environment' } elseif ($fromGit) { 'git' } elseif ($fromEnv) { 'environment' } else { 'none' } return [PSCustomObject]@{ Organization = $organization Project = $project Source = $source } } |