Publish-Packages.psm1

<#
.Synopsis
   Returns all published versions of a package on a feed
#>

function Get-FeedPackageVersions {
    param ([string] $Id, [string] $Feed)
    $json = dotnet package search $Id --exact-match --prerelease --source $Feed --format json | Out-String
    $exitCode = $LASTEXITCODE
    $result = try { $json | ConvertFrom-Json } catch { $null }
    if ($null -eq $result) { throw "dotnet package search failed for $Id on $Feed`n$json" }
    #errors (unknown feed, auth, network) are reported per source with exit code 0 - treat any problem as failure,
    #otherwise an unreachable feed would look like "version not on feed" and the package would be pushed
    $problems = @($result.problems) + @($result.searchResult | ForEach-Object { $_.problems }) | Where-Object { $null -ne $_ }
    if ($problems.Count -gt 0) { throw "dotnet package search failed for $Id on ${Feed}: $(($problems | ForEach-Object { $_.text }) -join '; ')" }
    if ($exitCode -ne 0) { throw "dotnet package search failed for $Id on $Feed`n$json" }
    if (@($result.searchResult).Count -eq 0) { throw "dotnet package search returned no result for $Id on $Feed" }
    @($result.searchResult.packages | Where-Object { $_.id -eq $Id } | ForEach-Object { $_.version })
}

<#
.Synopsis
   Automatic Publish Tool (APT)
.DESCRIPTION
   Publish nugets to a feed.
   Only packages whose id+version is not on the feed yet are pushed; an existing version is never sent.
   -Feed is a source name from 'dotnet nuget list source' (run in the -Path directory, so the repository
   nuget.config is used). Credentials come from nuget.config. Lomtec.Packages (Azure Artifacts) gets the
   api key 'az', other feeds (Nuaktiv.GitLab) no key.
   -WhatIf runs every step (file names, version lookup on the feed) except the push itself.
.EXAMPLE
   Publish-Packages -Path D:\Projects\Framework\ActiveOffice.NET\developer\publish\ -Feed Lomtec.Packages
.EXAMPLE
   Publish-Packages -Path D:\Projects\Framework\ActiveOffice.NET\developer\publish\ -Feed Nuaktiv.GitLab
.EXAMPLE
   Publish-Packages -Path D:\Projects\Framework\ActiveOffice.NET\developer\publish\ -Feed Nuaktiv.GitLab -WhatIf
 .NOTES
   Author: Imrich Szolik
#>

function Publish-Packages {
    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        #Packages path
        [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$false, HelpMessage="Packages path")]
        [string]
        $Path,
        #Package feed name (dotnet nuget list source), e.g. Lomtec.Packages or Nuaktiv.GitLab
        [Parameter(Mandatory=$false, ValueFromPipelineByPropertyName=$false, HelpMessage="Packages feed name (dotnet nuget list source).")]
        [string]
        $Feed = "Lomtec.Packages"
    )
    begin {
        if ($null -eq (Get-Command dotnet -ErrorAction SilentlyContinue)) { throw "Please install .NET SDK 8.0.200 or newer (dotnet package search)." }
    }
    process {
        #run dotnet from the packages directory so the repository nuget.config (sources + credentials) is used
        Push-Location -Path $Path
        try {
            #validate feed: must be an enabled source in 'dotnet nuget list source'
            $sourceList = dotnet nuget list source
            if ($LASTEXITCODE -ne 0) { throw "dotnet nuget list source failed:`n$($sourceList -join "`n")" }
            $sources = @($sourceList | ForEach-Object {
                if ($_ -match '^\s*\d+\.\s+(?<name>.+?)\s+\[(?<state>[^\]]+)\]\s*$') {
                    [pscustomobject]@{ Name = $Matches['name']; Enabled = ($Matches['state'] -eq 'Enabled') }
                }
            })
            $source = $sources | Where-Object { $_.Name -eq $Feed } | Select-Object -First 1
            if ($null -eq $source) {
                throw "Feed '$Feed' not found in NuGet sources. Available: $(@($sources | Where-Object { $_.Enabled }).Name -join ', ')"
            }
            if (-not $source.Enabled) { throw "Feed '$Feed' is disabled in NuGet sources." }
            $Feed = $source.Name
            #
            #legacy *.symbols.nupkg have the same id+version as the package, never push them as packages
            $files = Get-ChildItem -Path . -Filter *.nupkg | Where-Object { $_.Name -notlike '*.symbols.nupkg' }
            if(($files |  Measure-Object).Count -eq 0 ){
                Write-Host "#### APT NUGET deploy into $Feed aborted. Not found any packages ####"
                return
            }
            #
            Write-Host "#### APT NUGET deploy into $Feed ####"
            #
            $pushed = 0
            $skipped = 0
            $notPushed = 0
            #
            foreach($file in $files){
                #id and version from the file name: <id>.<major>.<minor>.<patch>[-prerelease].nupkg
                $package = [regex]::Match($file.Name, '^(?<id>.+?)\.(?<version>\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)\.nupkg$')
                if (-not $package.Success) { throw "Cannot read id/version from file name: $($file.Name)" }
                $id = $package.Groups['id'].Value
                $version = $package.Groups['version'].Value
                $name = "$id.$version"
                #
                #versions already on the feed are never pushed
                $feedVersions = Get-FeedPackageVersions -Id $id -Feed $Feed
                if ($feedVersions -contains $version) {
                    Write-Verbose "Found $name, not deployed."
                    $skipped++
                    continue
                }
                #
                if ($PSCmdlet.ShouldProcess($name, "Push to $Feed")) {
                    Write-Host "#### APT NUGET deploy: $name ####"
                    $pushArgs = @('nuget', 'push', $file.FullName, '--source', $Feed, '--skip-duplicate')
                    #Azure Artifacts requires an api key (any value), GitLab authenticates with nuget.config credentials only
                    if ($Feed -eq 'Lomtec.Packages') { $pushArgs += @('--api-key', 'az') }
                    dotnet @pushArgs
                    if ($LASTEXITCODE -ne 0) { throw "Push of $name to $Feed failed." }
                    $pushed++
                }
                else {
                    #-WhatIf (or -Confirm answered No): everything checked, only the push is not sent
                    $notPushed++
                }
            }
            #
            $summary = "Pushed: $pushed, already on feed: $skipped"
            if ($notPushed -gt 0) { $summary += ", would push (not sent): $notPushed" }
            Write-Host "#### APT NUGET deploy into $Feed done. $summary ####"
        }
        finally {
            Pop-Location
        }
    }
    end {
    }
}
Export-ModuleMember -Function Publish-Packages

# SIG # Begin signature block
# MIIIEwYJKoZIhvcNAQcCoIIIBDCCCAACAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCAKXku34jHRIcgS
# RF1KjpymK/IZa2WIa74XMdtwauReQKCCBQ8wggULMIIC86ADAgECAgIApTANBgkq
# hkiG9w0BAQsFADCBoDELMAkGA1UEBhMCU0sxETAPBgNVBAgTCFNsb3Zha2lhMRMw
# EQYDVQQHEwpCcmF0aXNsYXZhMRMwEQYDVQQKEwpMb210ZWMuY29tMSswKQYDVQQD
# EyJMb210ZWMuY29tIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MScwJQYJKoZIhvcN
# AQkBFhhJbXJpY2guU3pvbGlrQGxvbXRlYy5jb20wHhcNMTkwMzI2MTUwNDQzWhcN
# MzAwMTAxMDAwMDAwWjCBkzELMAkGA1UEBhMCU0sxETAPBgNVBAgTCFNsb3Zha2lh
# MRMwEQYDVQQHEwpCcmF0aXNsYXZhMRMwEQYDVQQKEwpMb210ZWMuY29tMR4wHAYD
# VQQDExVMb210ZWMuY29tIFBvd2Vyc2hlbGwxJzAlBgkqhkiG9w0BCQEWGEltcmlj
# aC5Tem9saWtAbG9tdGVjLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
# ggEBAL4n4nCHbkdrf09IHNFQ2P/z6I43GKScsFJOQHUMkRD3ALoUFL/URC9sW2fY
# rqG5VkFAKhnM0VxeiICR53cAshNShjFf58PhaS973jtCoJcaugIVBVoFIuQ+gnNY
# Jp2VdBbIPKMW4JjjZOxBEkHpMWmfitXKWGKeU68Qcn3oI6PO+YSztfXLe1NU+1GI
# O3fA7E0vHVPUf/qWZXMYU5ElLQVm9AXbfX79mTgl76A57+OC6j+Aehkd2OPfUl4w
# snox3fOyUAUA8iojeWh97PpXd/s+RkuxWdgsC4YSWDUjhZSzBkml9uerYqo9a+XA
# b39dvkpK9TPl3q5HNBQMkCfp8bkCAwEAAaNaMFgwCQYDVR0TBAIwADALBgNVHQ8E
# BAMCB4AwKwYDVR0lBCQwIgYIKwYBBQUHAwMGCisGAQQBgjcCARUGCisGAQQBgjcC
# ARYwEQYJYIZIAYb4QgEBBAQDAgSwMA0GCSqGSIb3DQEBCwUAA4ICAQAJJapv9skY
# jh5HTsJnqDdtqh7YOOXuA8g+DKBj+5gDEZE5V9VhAFVp8UJ9RoITGGIRTVId0lqc
# LJiVSTHx305VW9aID8vo77kfrTyXvPXNIsTtHnPkkwH47+CoiY3IpPQLjUA/Q6Qd
# qwINvvwom7/Wc+OoIqlPdJH5DbBrIy85dr6M/Lm3Rw2BolcTRwXTB3xAhweth78B
# P6pbcAd32FdymkRopLIihuNs7g7ZR/Q/5803G+OiQIMRGyTvaQ+aQjJgFpkzp7NI
# whzougfCOV47Sc89jEpUqw16i2UFfz2ywOlWUyYtue1S1PjM1ljgJfRo+e0wUnFp
# gFQzXGF1bTYVaQ4e3nJleADfvqeXoH2AYeBTbz9BcogkkfURAC3iiob1bNs5jE1C
# brEDCw6m/03k0oOmm3xQksXyAhBYuUkRwu9jd4y3FwZ/syDGLz3b6cY8o1YyINOO
# A3B2r92shNt0rWhJu3v+qcIVmFQ0aKlhNNRoiVlQgJ7NgO0UV+vU2lgiscpUSxEt
# xKN+450r49su06NA6zsyn3CELUmVkPyjx5fyizwt9KxVuYOUSEb32Y7QCffHJ1qt
# F6SuQrKbgb/24y7cCDW6PDVRvPOySUlKu9sPykICDjvzXBvjYEILM7AmtYCIBGQg
# imBsQvTEoXJXiwAOi1XNz3LxcjQHQ/ZM4zGCAlowggJWAgEBMIGnMIGgMQswCQYD
# VQQGEwJTSzERMA8GA1UECBMIU2xvdmFraWExEzARBgNVBAcTCkJyYXRpc2xhdmEx
# EzARBgNVBAoTCkxvbXRlYy5jb20xKzApBgNVBAMTIkxvbXRlYy5jb20gQ2VydGlm
# aWNhdGlvbiBBdXRob3JpdHkxJzAlBgkqhkiG9w0BCQEWGEltcmljaC5Tem9saWtA
# bG9tdGVjLmNvbQICAKUwDQYJYIZIAWUDBAIBBQCggYQwGAYKKwYBBAGCNwIBDDEK
# MAigAoAAoQKAADAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgorBgEEAYI3
# AgELMQ4wDAYKKwYBBAGCNwIBFjAvBgkqhkiG9w0BCQQxIgQgF/Vc25Gx5pA45cJM
# pMCuPIeKw3n0hMh4o1ABlkq72Z8wDQYJKoZIhvcNAQEBBQAEggEAe2TopUWFJ/AF
# Z+KnF3O0eDsCIDnJt4xFuRuQ3tHkw99y10M6BGymvLgUZDdoHIULWZXame8c4Srh
# 2N/fd2T+1J66O+Wp16Tp3VDp+qApEYgze0UZF45Qs1aRrXFXU6iNkLd15xgy8GJq
# IlhRAgUf3KhZ6FhdRHPUmrsfWzQnhHlfKtVMgDCff8LRwOdzaxU6yxEHiG+WEBlo
# Mwt1Skj30rLfdurPf/9hRR/on5ajSw0FqUhh0fcFskn/5PFgMgOcbqICIRMwVVxZ
# /DaXB2IveXcyaCb0WzgiX0RlDBB7NKkAhjb85YlAji96Dt6kvhvY0VoCdgiqnQd9
# TLGckhkG+Q==
# SIG # End signature block