Modules/businessdev.ALbuild.Feeds/Public/Invoke-BcPackagePromotion.ps1

function Invoke-BcPackagePromotion {
    <#
    .SYNOPSIS
        Promotes a published NuGet package version to a view on an Azure DevOps Artifacts feed.
 
    .DESCRIPTION
        Azure DevOps Artifacts feeds expose 'views' (e.g. @Prerelease, @Release). Promoting a package
        version to a view lets partners consume it from a view-scoped feed URL
        (.../_packaging/<feed>@Prerelease/nuget/v3/index.json), which is how ALbuild distributes
        prerelease (dev) and release (master) app packages from one feed.
 
        The view assignment is a one-shot PATCH against the Azure DevOps Packaging REST API (it is not
        part of the NuGet protocol). The feed's v3 index URL is parsed to derive the organisation,
        optional project and feed name. Promotion is OPT-IN: when -View is not supplied the cmdlet is a
        no-op and returns $true, so callers can pass the parameter through unconditionally.
 
    .PARAMETER Url
        The feed's NuGet v3 service index URL (.../nuget/v3/index.json) on an Azure DevOps Artifacts
        host (pkgs.dev.azure.com or *.pkgs.visualstudio.com).
 
    .PARAMETER View
        Target view name, e.g. 'Prerelease' or 'Release' (any custom view name is accepted). When
        empty or omitted, no promotion is performed.
 
    .PARAMETER ApiKey
        The Azure DevOps PAT used for the REST call (a Packaging read/write token, the same one used
        to publish).
 
    .PARAMETER PackagePath
        Path to the .nupkg. The package id and version are read from its .nuspec. Mutually exclusive
        with -PackageId/-Version.
 
    .PARAMETER PackageId
        Explicit package id. Use with -Version instead of -PackagePath.
 
    .PARAMETER Version
        Explicit package version. Use with -PackageId instead of -PackagePath.
 
    .EXAMPLE
        Invoke-BcPackagePromotion -Url $feedUrl -PackagePath .\pkg.1.0.0.nupkg -View Prerelease -ApiKey $env:FEED_PAT
 
    .OUTPUTS
        System.Boolean ($true on success or when there is nothing to promote).
    #>

    [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'FromPackage')]
    [OutputType([bool])]
    param(
        [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $Url,
        [string] $View,
        [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $ApiKey,
        [Parameter(ParameterSetName = 'FromPackage')] [string] $PackagePath,
        [Parameter(ParameterSetName = 'Explicit', Mandatory)] [ValidateNotNullOrEmpty()] [string] $PackageId,
        [Parameter(ParameterSetName = 'Explicit', Mandatory)] [ValidateNotNullOrEmpty()] [string] $Version
    )

    # Opt-in: nothing to do without a target view.
    if ([string]::IsNullOrWhiteSpace($View)) {
        Write-ALbuildLog -Level Verbose 'No view requested; skipping promotion.'
        return $true
    }

    # Resolve the package identity, from the .nupkg's .nuspec when a path is given.
    if ($PSCmdlet.ParameterSetName -eq 'FromPackage') {
        if (-not $PackagePath) { throw 'Provide -PackagePath, or -PackageId and -Version.' }
        if (-not (Test-Path -LiteralPath $PackagePath)) { throw "Package not found: '$PackagePath'." }
        $identity = Get-BcNuspecIdentity -PackagePath $PackagePath
        $PackageId = $identity.Id
        $Version = $identity.Version
    }

    # Parse the Azure DevOps feed coordinates from the v3 index URL.
    $coords = ConvertTo-BcAzureDevOpsFeedCoordinate -Url $Url

    # Build the Packaging REST endpoint (project segment is optional for org-scoped feeds).
    $projectSegment = if ($coords.Project) { "$($coords.Project)/" } else { '' }
    $patchUrl = "$($coords.BaseUrl)/${projectSegment}_apis/packaging/feeds/$($coords.Feed)/nuget/packages/$PackageId/versions/$Version`?api-version=7.1-preview.1"
    $body = @{ views = @{ op = 'add'; path = '/views/-'; value = $View } } | ConvertTo-Json -Depth 4 -Compress

    if (-not $PSCmdlet.ShouldProcess("$PackageId $Version", "Promote to '$View' view on feed '$($coords.Feed)'")) { return $true }

    $headers = @{
        Authorization  = "Basic $([Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes("user:$ApiKey")))"
        'Content-Type' = 'application/json'
    }

    try {
        Invoke-RestMethod -Uri $patchUrl -Method Patch -Headers $headers -Body $body -UseBasicParsing -ErrorAction Stop | Out-Null
        Write-ALbuildLog -Level Success "Promoted '$PackageId' $Version to the '$View' view."
        return $true
    }
    catch {
        throw "Failed to promote '$PackageId' $Version to the '$View' view on feed '$($coords.Feed)': $($_.Exception.Message)"
    }
}