Public/Imported/Publish-PowerShellGalleryModule.ps1

# https://raw.githubusercontent.com/adbertram/Random-PowerShell-Work/master/PowerShell%20Gallery/Publish-PowerShellGalleryModule.ps1

function ShowMenu {
    <#
        .SYNOPSIS
            A helper function to display a menu when a test fails.

        .EXAMPLE
            PS> ShowMenu -Title 'What to do' -ChoiceMessage 'Should I do it?'

    #>

    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$Title,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$ChoiceMessage,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$NoMessage = 'No thanks'
    )

    $yes = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes", $ChoiceMessage
    $no = New-Object System.Management.Automation.Host.ChoiceDescription "&No", $NoMessage
    $options = [System.Management.Automation.Host.ChoiceDescription[]]($yes, $no)
    PromptChoice -Title $Title -ChoiceMessage $ChoiceMessage -options $options
}

function PromptChoice {
    param(
        $Title,
        $ChoiceMessage,
        $Options
    )
    $host.ui.PromptForChoice($Title, $ChoiceMessage, $options, 0)
}

function GetRequiredManifestKeyParams {
    <#
        .SYNOPSIS
            A helper function to retrieve values for the required manifest keys from the user.

        .EXAMPLE
            PS> GetRequiredManifestKeyParams

    #>

    [CmdletBinding()]
    param
    (
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string[]]$RequiredKeys = @('Description', 'Version', 'ProjectUri', 'Author')
    )

    $paramNameMap = @{
        Version     = 'ModuleVersion'
        Description = 'Description'
        Author      = 'Author'
        ProjectUri  = 'ProjectUri'
    }
    $params = @{ }
    foreach ($val in $RequiredKeys) {
        $result = Read-Host -Prompt "Input value for module manifest key: [$val]"
        $paramName = $paramNameMap.$val
        $params.$paramName = $result
    }
    $params
}

function Invoke-Test {
    param(
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$TestName,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [ValidateSet('Test', 'Fix')]
        [string]$Action,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [object]$Module
    )

    $testHt = $moduleTests | Where-Object { $_.TestName -eq $TestName }
    $actionName = '{0}{1}' -f $Action, 'Action'
    & $testHt.$actionName -Module $Module
}

function Publish-PowerShellGalleryModule {
    <#
        .SYNOPSIS
            This script is a script designed to remove all barriers to entry when publishing modules to the PowerShell
            Gallery. Before running, ensure the NuGetApiKey parameter has a default parameter value.

        .DESCRIPTION
            This script has two different purposes; to ensure your module meets all official Gallery requirements and to
            assist in creating your own "requirements". As-is, the script ensure your module is Gallery-ready by checking for
            all official requirements but also performs a couple extra tests. It's purpose is to provide a foundation to
            add upon to for your own "requirements" for the Gallery.

            Each run will ensure a module manifest is in the same folder as the ModuleFilePath and will ensure that manifest
            has all of the required keys. Also, it will run Test-ModuleManifest to ensure the result passes there as well.

        .EXAMPLE
            PS> Publish-PowerShellGalleryModule -ModuleFilePath C:\Foo\Foo.psm1 -NuGetApiKey XXXXXXXXX

            This example will check the Foo module for all pre-defined requirements and fix as necessary.

        .EXAMPLE
            PS> Publish-PowerShellGalleryModule -ModuleFilePath C:\Modules\Foo.psm1 -RunOptionalTests

            This example assumes that you've included a default value for the NuGetApiKey.

        .PARAMETER ModuleFilePath
            A mandatory string parameter representing the file path to a PSM1 file. The folder path also represents the
            folder that will be searched for a matching module manifest as well.

        .PARAMETER RunOptionalTests
            A switch parameter to enable if you'd like to run any optional tests. Currently, the only optional tests is a
            Pester tests file. If a file matching $ModuleName.Tests.ps1 is not in the same folder as the PSM1, it will
            notice this and prompt to create a simple template.

            To add more tests, just add a hashtable to the $moduleTests array by copying an existing one ensuring
            that the Mandatory key value is as expected.

        .PARAMETER NuGetApiKey
            A optional PowerShell parameter yet required Gallery attribute representing the NuGet API key provided when
            signing up for an account with the PowerShell Gallery. This can be found by going to the URL
            https://www.powershellgallery.com/users/account/LogOn?returnUrl=%2F. It is recommended that your key be placed
            as the default parameter value to remove the need of providing it each time.

        .PARAMETER PublishToGallery
            An optional switch parameter to use if you'd like to automatically published the tested module to the PowerShell Gallery.
            If this isn't used, you will be prompted to publish.
    #>


    [CmdletBinding(DefaultParameterSetName = 'ByName')]
    param(
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
                if (-not (Test-Path -Path $_ -PathType Leaf)) {
                    throw "The module $($_) could not be found."
                } else {
                    $true
                }
            })]
        [string]$ModuleFilePath,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [switch]$RunOptionalTests,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$NuGetApiKey,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [switch]$PublishToGallery
    )

    <#
    Here are all of the individual tests. This is where you can add additional mandatory or optional tests depending on the
    value of the Mandatory key. To add a test, add a hashtable with the same key values. Any test marked as Mandatory will
    always run. Any test marked as Optional will only run with the -RunOptionalTests parameter is used.
    #>

    $moduleTests = @(
        @{
            TestName       = 'Module manifest exists'
            Mandatory      = $true
            FailureMessage = 'The module manifest does not exist at the expected path.'
            FixMessage     = 'Run New-ModuleManifest to create a new manifest'
            FixAction      = {
                param($Module)

                ## Gather up all of the requuired key values from the user
                $newManParams = @{ Path = $Module.Path }
                $newManParams += GetRequiredManifestKeyParams

                ## Create the new manifest
                Write-Verbose -Message "Running New-ModuleManifest with params: [$($newManParams | Out-String)]"
                New-ModuleManifest @newManParams
            }
            TestAction     = {
                param($Module)

                ## Module path will always be the PSD1 since we're overriding the property earlier
                if (-not (Test-Path -Path $Module.Path -PathType Leaf)) {
                    $false
                } else {
                    $true
                }
            }
        }
        @{
            TestName       = 'Manifest has all required keys'
            Mandatory      = $true
            FailureMessage = 'The module manifest does not have all the required keys populated.'
            FixMessage     = 'Run Update-ModuleManifest to update existing manifest'
            FixAction      = {
                param($Module)

                ## Have to get the module from the file system again here in case it was just created with New-ModuleManifest
                $Module = Get-Module -Name $Module.Path -ListAvailable

                ## Gather up all of the keys required and their values and update the existing manifest.
                $updateManParams = @{ Path = $Module.Path }
                $missingKeys = ($Module.PsObject.Properties | Where-Object -FilterScript { $_.Name -in @('Description', 'Author', 'Version') -and (-not $_.Value) }).Name
                if ((-not $Module.LicenseUri) -and (-not $Module.PrivateData.PSData.ProjectUri)) {
                    $missingKeys += 'ProjectUri'
                }

                $updateManParams += GetRequiredManifestKeyParams -RequiredKeys $missingKeys
                Update-ModuleManifest @updateManParams
            }
            TestAction     = {
                param($Module)

                ## Have to get the module again here to either update any new keys New-ModuleManifest just created or, since the
                ## module was originally passed as a PSM1, it has no idea of an already existing manifest anyway.
                $Module = Get-Module -Name $Module.Path -ListAvailable

                if ($Module.PsObject.Properties | Where-Object -FilterScript { $_.Name -in @('Description', 'Author', 'Version') -and (-not $_.Value) }) {
                    $false
                } elseif ((-not $Module.LicenseUri) -and (-not $Module.PrivateData.PSData.ProjectUri)) {
                    $false
                } else {
                    $true
                }
            }
        }
        @{
            TestName       = 'Manifest passes Test-Modulemanifest validation'
            Mandatory      = $true
            FailureMessage = 'The module manifest does not pass validation with Test-ModuleManifest'
            FixMessage     = 'Run Test-ModuleManifest explicitly to investigate problems discovered'
            FixAction      = {
                param($Module)
                Test-ModuleManifest -Path $module.Path
            }
            TestAction     = {
                param($Module)
                if (-not (Test-ModuleManifest -Path $Module.Path -ErrorAction SilentlyContinue)) {
                    $false
                } else {
                    $true
                }
            }
        }
        @{
            TestName       = 'Pester Tests Exists'
            Mandatory      = $false
            FailureMessage = 'The module does not have any associated Pester tests.'
            FixMessage     = 'Create a new Pester test file using a common template'
            FixAction      = {
                param($Module)

                ## Create a $ModuleName.Tests.ps1 file using a template inside of the module folder creating a Describe block
                ## for each function that's exported inside of the module.
                $pesterTestPath = "$($Module.ModuleBase)\$($Module.Name).Tests.ps1"
                $publicFunctionNames = (Get-Command -Module $Module).Name

                $templateFuncs = ''
                $templateFuncs += $publicFunctionNames | ForEach-Object {
                    @"
        describe '$_' {

        }

"@

                }

                ## This is a sample Pester template template that will represnt the contents of the $ModuleName.Tests.ps1 file
                ## Optionally, this text could be stored in an external template file as well instead of here as a here string.
                $pesterTestTemplate = @'
#region import modules
$ThisModule = "$($MyInvocation.MyCommand.Path -replace "\.Tests\.ps1$", '').psm1"
$ThisModuleName = (($ThisModule | Split-Path -Leaf) -replace ".psm1")
Get-Module -Name $ThisModuleName -All | Remove-Module -Force

Import-Module -Name $ThisModule -Force -ErrorAction Stop

## If a module is in $Env:PSModulePath and $ThisModule is not, you will have two modules loaded when importing and
## InModuleScope does not like that. 0.0 will always be the one imported directly from PSM1.
@(Get-Module -Name $ThisModuleName).where({{ $_.version -ne "0.0" }}) | Remove-Module -Force
#endregion

InModuleScope $ThisModuleName {{
{0}
}}
'@
 -f $templateFuncs

                Add-Content -Path $pesterTestPath -Value $pesterTestTemplate
            }
            TestAction     = {
                param($Module)

                if (-not (Test-Path -Path "$($Module.ModuleBase)\$($Module.Name).Tests.ps1" -PathType Leaf)) {
                    $false
                } else {
                    $true
                }
            }
        }
    )

    try {

        if (-not $NuGetApiKey) {
            throw @"
The NuGet API key was not found in the NuGetAPIKey parameter. In order to publish to the PowerShell Gallery this key is required.
Go to https://www.powershellgallery.com/users/account/LogOn?returnUrl=%2F for instructions on registering an account and obtaining
a NuGet API key.
"@

        }

        $module = Get-Module -Name $ModuleFilePath -ListAvailable

        ## Force the manifest to show up if it exists. This is done as an easy way to bring along a manifest reference
        $module | Add-Member -MemberType NoteProperty -Name 'Path' -Value "$($module.ModuleBase)\$($Module.Name).psd1" -Force

        if ($RunOptionalTests.IsPresent) {
            $whereFilter = { '*' }
        } else {
            $whereFilter = { $_.Mandatory }
        }

        foreach ($test in ($moduleTests | Where-Object $whereFilter)) {
            if (-not (Invoke-Test -TestName $test.TestName -Action 'Test' -Module $module)) {
                $result = ShowMenu -Title $test.FailureMessage -ChoiceMessage "Would you like to resolve this with action: [$($test.FixMessage)]?"
                switch ($result) {
                    0 {
                        Write-Verbose -Message 'Running fix action...'
                        Invoke-Test -TestName $test.TestName -Action 'Fix' -Module $module
                    }
                    1 { Write-Verbose -Message 'Leaving the problem be...' }
                }
            } else {
                Write-Verbose -Message "Module passed test: [$($test.TestName)]"
            }
        }

        $publishAction = {
            Write-Output  'Publishing module...'
            Publish-Module -Name $module.Name -NuGetApiKey $NuGetApiKey
            Write-Output  'Done.'
        }
        if ($PublishToGallery.IsPresent) {
            & $publishAction
        } else {
            $result = ShowMenu -Title 'PowerShell Gallery Publication' -ChoiceMessage 'All mandatory tests have passed. Publish it?'
            switch ($result) {
                0 {
                    & $publishAction
                }
                1 {
                    Write-Host "Postponing publishing. When ready, use this syntax: Publish-Module -Name $($module.Name) -NuGetApiKey $NuGetApiKey"
                }
            }
        }

    } catch {
        Write-Error -Message $_.Exception.Message
    }
}