src/Connect-CciGet.ps1
|
function Connect-CciGet { <# .SYNOPSIS Register configured CCI feeds as PSResource repositories and prepare credentials. .DESCRIPTION For each enabled feed in Get-CciGetConfig, Connect-CciGet: - Ensures the Azure Artifacts Credential Provider is installed (one-time bootstrap on Windows; uses the Microsoft installer at https://github.com/microsoft/artifacts-credprovider). - Calls Register-PSResourceRepository for the feed (idempotent). After Connect-CciGet succeeds, Find-CciModule / Install-CciModule will transparently use the user's Entra identity (or, in CI, a workload identity) to authenticate to the feed. No PATs are required. .PARAMETER Tenant Optional. Limit registration to the named feed only. .PARAMETER SkipCredentialProvider Skip the credential-provider bootstrap (use when you have managed it centrally or are running in CI with a different auth strategy). #> [CmdletBinding()] param( [string]$Tenant, [switch]$SkipCredentialProvider ) if (-not (Get-Module -ListAvailable -Name Microsoft.PowerShell.PSResourceGet)) { throw "cciget: Microsoft.PowerShell.PSResourceGet is required but not installed. Run: Install-Module Microsoft.PowerShell.PSResourceGet -Scope CurrentUser" } Import-Module Microsoft.PowerShell.PSResourceGet -ErrorAction Stop # Remote registry refresh (designed-in hook, unbound by default): when the # config carries a registryUrl, fetch a replacement registry and persist it. # The baked registry remains the fallback on any failure. $cfg = Get-CciGetConfig $registryUrlProp = $cfg.PSObject.Properties['registryUrl'] $registryUrl = if ($registryUrlProp) { $registryUrlProp.Value } if ($registryUrl) { try { $remote = Invoke-RestMethod -Uri $registryUrl -TimeoutSec 15 if ($remote.feeds -and $remote.defaultFeed) { $path = _Get-CciGetConfigPath $dir = Split-Path $path -Parent if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null } [System.IO.File]::WriteAllText($path, ($remote | ConvertTo-Json -Depth 6), (New-Object System.Text.UTF8Encoding $false)) Write-Host "cciget: tenant registry refreshed from $registryUrl." } else { Write-Warning "cciget: registry at $registryUrl is malformed (needs feeds + defaultFeed); using local config." } } catch { Write-Warning "cciget: registry refresh from $registryUrl failed ($_); using local config." } } if (-not $SkipCredentialProvider) { $pluginRoot = Join-Path $env:USERPROFILE '.nuget\plugins\netcore' if (-not (Test-Path $pluginRoot) -or -not (Get-ChildItem $pluginRoot -Filter 'CredentialProvider.Microsoft.dll' -Recurse -ErrorAction SilentlyContinue)) { Write-Host "cciget: installing Azure Artifacts Credential Provider..." try { $script = (New-Object System.Net.WebClient).DownloadString('https://aka.ms/install-artifacts-credprovider.ps1') Invoke-Expression $script } catch { Write-Warning "cciget: credential provider install failed: $_. You may need to install manually from https://github.com/microsoft/artifacts-credprovider." } } } $feeds = _Resolve-CciGetFeed -Tenant $Tenant if (-not $Tenant -and @($feeds).Count -eq 1) { Write-Host "cciget: single configured tenant '$($feeds[0].name)' auto-selected." } $authFailed = @() # Credential provider configuration for this session. These persist after # Connect-CciGet returns so Find-/Install-CciModule benefit too. # # FORCE_CANSHOWDIALOG_TO=false: PSResourceGet invokes the provider in plugin # mode with CanShowDialog=true, which attempts the WAM broker dialog and # fails on freshly built machines with 0x800703f0 (ERROR_NO_TOKEN) instead # of falling back. Forcing it off selects the device-code flow, which works. # DEVICEFLOWTIMEOUTSECONDS: the provider default is 90s - not enough time for # a human to open a browser and complete the sign-in, producing # "A task was canceled". Give it 10 minutes. # Both preferred (ARTIFACTS_*) and legacy (NUGET_*/VSS_*) names are set; # older provider builds only understand the legacy ones. $env:ARTIFACTS_CREDENTIALPROVIDER_FORCE_CANSHOWDIALOG_TO = 'false' $env:NUGET_CREDENTIALPROVIDER_FORCE_CANSHOWDIALOG_TO = 'false' $env:ARTIFACTS_CREDENTIALPROVIDER_DEVICEFLOWTIMEOUTSECONDS = '600' $env:NUGET_CREDENTIALPROVIDER_VSTS_DEVICEFLOWTIMEOUTSECONDS = '600' # If an Azure CLI session is active, pre-seed feed credentials so the # credential provider doesn't need to prompt interactively. This allows # non-interactive scenarios (remoting, CI, WAM-incompatible shells). $azdoToken = $null if (Get-Command az -ErrorAction SilentlyContinue) { try { $azdoToken = az account get-access-token --resource '499b84ac-1321-427f-aa17-267ca6975798' --query accessToken -o tsv 2>$null } catch { } } if ($azdoToken) { $endpoints = @{ endpointCredentials = @($feeds | ForEach-Object { @{ endpoint = $_.url; username = 'VssSessionToken'; password = $azdoToken } }) } $endpointJson = $endpoints | ConvertTo-Json -Compress -Depth 3 $env:VSS_NUGET_EXTERNAL_FEED_ENDPOINTS = $endpointJson $env:ARTIFACTS_CREDENTIALPROVIDER_EXTERNAL_FEED_ENDPOINTS = $endpointJson Write-Verbose "cciget: seeded feed credentials from Azure CLI session." } else { # Fresh-machine path: no Azure CLI. PSResourceGet runs the credential # provider in PLUGIN mode, which cannot prompt — it fails with exit # code 2 rather than starting a device-code flow. Drive the provider # directly (stand-alone, interactive) and seed the same endpoint # variable, so the probe below behaves identically to the az path. $interactiveToken = _Invoke-CciCredentialProvider -FeedUrl $feeds[0].url if ($interactiveToken) { $endpoints = @{ endpointCredentials = @($feeds | ForEach-Object { @{ endpoint = $_.url; username = 'VssSessionToken'; password = $interactiveToken } }) } $endpointJson = $endpoints | ConvertTo-Json -Compress -Depth 3 $env:VSS_NUGET_EXTERNAL_FEED_ENDPOINTS = $endpointJson $env:ARTIFACTS_CREDENTIALPROVIDER_EXTERNAL_FEED_ENDPOINTS = $endpointJson Write-Verbose "cciget: seeded feed credentials from interactive sign-in." } } foreach ($feed in $feeds) { $repoName = _Get-CciGetRepositoryName -FeedName $feed.name # Set tenant-specific authority for this feed. if ($feed.tenantId) { $env:NUGET_CREDENTIALPROVIDER_MSAL_AUTHORITY = "https://login.microsoftonline.com/$($feed.tenantId)" } $existing = Get-PSResourceRepository -Name $repoName -ErrorAction SilentlyContinue if ($existing) { if ($existing.Uri -ne $feed.url) { Set-PSResourceRepository -Name $repoName -Uri $feed.url -Trusted Write-Verbose "cciget: updated $repoName URL." } } else { Register-PSResourceRepository -Name $repoName -Uri $feed.url -Trusted Write-Host "cciget: registered repository '$repoName' -> $($feed.url)" } # Verify auth by probing the feed. This triggers the credential provider # now (at connect time) so auth issues surface here, not in Find-CciModule. Write-Host "cciget: authenticating to '$repoName'..." $probeOk = $false try { # Use a wildcard probe — this hits the V2 search endpoint which # actually requires authentication, unlike specific-name lookups # which may return empty 200s without auth. $null = Find-PSResource -Name '*' -Repository $repoName -ErrorAction Stop # Got results — auth works and the feed has packages. $probeOk = $true } catch { $msg = $_.Exception.Message if ($msg -match 'No match was found|could not be found in repository') { # Auth succeeded — feed is simply empty. $probeOk = $true } elseif ($msg -match '\[Warning\].*CredentialProvider' -and $msg -notmatch '401|Unauthorized|forbidden') { Write-Verbose "cciget: credential provider warning (non-fatal): $msg" $probeOk = $true } else { Write-Warning "cciget: authentication failed for '$repoName'." Write-Warning " Error: $msg" # NOTE: single-quoted + concatenated deliberately. A previous # version used C-style \" escaping inside a double-quoted string, # which terminates the PowerShell string early and made # Connect-CciGet fail with PositionalParameterNotFound. $recovery = ' & "$env:USERPROFILE\.nuget\plugins\netcore\CredentialProvider.Microsoft\CredentialProvider.Microsoft.exe" -U ' + $feed.url + ' -C false -I -R' Write-Warning ' Hint: run the credential provider directly to sign in, then retry Connect-CciGet:' Write-Warning $recovery $authFailed += $repoName } } if ($probeOk) { Write-Host "cciget: '$repoName' authenticated and ready." } } if ($authFailed.Count -gt 0) { Write-Warning "cciget: $($authFailed.Count) feed(s) failed authentication: $($authFailed -join ', ')" # Fall back to the tenant DISTRIBUTION STORE. Azure Artifacts requires an # Azure DevOps entitlement + paid Basic licence per user; machine builders # have neither, but do hold Azure RBAC on the tenant store. Serving the # same modules from there keeps a machine build on one auth system. $Script:CciGetSource = $null foreach ($feed in $feeds) { if ($feed.name -notin $authFailed -and (_Get-CciGetRepositoryName -FeedName $feed.name) -notin $authFailed) { continue } $blobCtx = _Connect-CciBlobStore -Feed $feed if ($blobCtx) { $index = _Get-CciBlobModuleIndex -Feed $feed -Context $blobCtx if ($index) { $Script:CciGetSource = 'blob' $names = @($index.modules.PSObject.Properties.Name) Write-Host "cciget: using the tenant distribution store for '$($feed.name)' ($($names.Count) modules available)." $authFailed = @($authFailed | Where-Object { $_ -ne (_Get-CciGetRepositoryName -FeedName $feed.name) }) } } } if ($authFailed.Count -gt 0) { Write-Warning "cciget: You may need to run Connect-CciGet again. If prompted for a device code, complete the sign-in in your browser." } } else { $Script:CciGetSource = 'feed' } # Return a clean summary instead of the wide default table. Get-PSResourceRepository -Name (_Get-CciGetRepositoryName -FeedName '*') | Select-Object Name, Uri, Trusted } |