Modules/businessdev.ALbuild.Feeds/Public/Publish-BcPackage.ps1

function Publish-BcPackage {
    <#
    .SYNOPSIS
        Pushes a NuGet package (.nupkg) to a NuGet v3 feed.
 
    .DESCRIPTION
        Resolves the feed's PackagePublish service from its service index and uploads the package
        using a multipart PUT (the NuGet v3 push protocol). An existing package version is reported
        as a conflict rather than failing the build (unless -FailOnConflict is set).
 
    .PARAMETER PackagePath
        Path to the .nupkg to push.
 
    .PARAMETER Url
        The feed's NuGet v3 service index URL (.../index.json).
 
    .PARAMETER ApiKey
        The API key (nuget.org) or PAT (Azure DevOps/GitHub) for publishing.
 
    .PARAMETER FailOnConflict
        Throw if the package version already exists. Default: warn and continue.
 
    .PARAMETER View
        Optional Azure DevOps Artifacts view (e.g. 'Prerelease' or 'Release') to promote the package
        to after a successful push. When omitted, only the push is performed. Promotion still runs when
        the version already existed (a non-failing conflict), since promoting an existing version is
        valid. Ignored for non-Azure-DevOps feeds (Invoke-BcPackagePromotion throws if misused there).
 
    .EXAMPLE
        Publish-BcPackage -PackagePath .\pkg.1.0.0.nupkg -Url $feedUrl -ApiKey $env:NUGET_KEY
 
    .EXAMPLE
        Publish-BcPackage -PackagePath .\pkg.1.0.0.nupkg -Url $feedUrl -ApiKey $env:FEED_PAT -View Prerelease
 
    .OUTPUTS
        System.Boolean ($true on success or conflict-when-not-failing).
    #>

    [CmdletBinding(SupportsShouldProcess)]
    [OutputType([bool])]
    param(
        [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $PackagePath,
        [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $Url,
        [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $ApiKey,
        [switch] $FailOnConflict,
        [string] $View
    )

    if (-not (Test-Path -LiteralPath $PackagePath)) { throw "Package not found: '$PackagePath'." }

    $headers = @{}
    if ($Url -notlike 'https://api.nuget.org/*') {
        $headers['Authorization'] = "Basic $([Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes("user:$ApiKey")))"
    }

    $index = Invoke-RestMethod -Uri $Url -Headers $headers -Method Get -UseBasicParsing -ErrorAction Stop
    $publishUrl = $index.resources | Where-Object { $_.'@type' -eq 'PackagePublish/2.0.0' } | Select-Object -ExpandProperty '@id' -First 1
    if (-not $publishUrl) { throw "Feed '$Url' does not expose PackagePublish/2.0.0." }

    if (-not $PSCmdlet.ShouldProcess($publishUrl, "Push $(Split-Path $PackagePath -Leaf)")) { return $true }

    # Build a multipart/form-data body containing the package bytes.
    $boundary = [Guid]::NewGuid().ToString()
    $lf = "`r`n"
    $fileName = [System.IO.Path]::GetFileName($PackagePath)
    $tempBody = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), [Guid]::NewGuid().ToString() + '.body')
    $fs = [System.IO.File]::OpenWrite($tempBody)
    try {
        $enc = [System.Text.Encoding]::UTF8
        $header = "--$boundary$lf" + "Content-Type: application/octet-stream$lf" + "Content-Disposition: form-data; name=package; filename=`"$fileName`"$lf$lf"
        $headerBytes = $enc.GetBytes($header)
        $fs.Write($headerBytes, 0, $headerBytes.Length)
        $packageBytes = [System.IO.File]::ReadAllBytes((Resolve-Path -LiteralPath $PackagePath).ProviderPath)
        $fs.Write($packageBytes, 0, $packageBytes.Length)
        $footerBytes = $enc.GetBytes("$lf--$boundary--$lf")
        $fs.Write($footerBytes, 0, $footerBytes.Length)
    }
    finally { $fs.Dispose() }

    $pushHeaders = $headers.Clone()
    $pushHeaders['X-NuGet-ApiKey'] = $ApiKey

    try {
        Invoke-RestMethod -Uri $publishUrl -Method Put -Headers $pushHeaders `
            -ContentType "multipart/form-data; boundary=$boundary" -InFile $tempBody -ErrorAction Stop | Out-Null
        Write-ALbuildLog -Level Success "Pushed '$fileName' to '$Url'."
    }
    catch {
        $status = $null
        if ($_.Exception.Response) { try { $status = [int]$_.Exception.Response.StatusCode } catch { $status = $null } }
        if ($status -eq 409) {
            if ($FailOnConflict) { throw "Package '$fileName' already exists on '$Url'." }
            Write-ALbuildLog -Level Warning "Package '$fileName' already exists on '$Url'; skipping."
        }
        else {
            throw "Failed to push '$fileName' to '$Url': $($_.Exception.Message)"
        }
    }
    finally {
        if (Test-Path -LiteralPath $tempBody) { Remove-Item -LiteralPath $tempBody -Force -ErrorAction SilentlyContinue }
    }

    # The version now exists on the feed (freshly pushed or already present) - promote it if requested.
    if ($View) {
        Invoke-BcPackagePromotion -Url $Url -PackagePath $PackagePath -View $View -ApiKey $ApiKey | Out-Null
    }

    return $true
}