psmodulebuildhelper.psm1

#.ExternalHelp PSModuleBuildHelper-help.xml
function Get-BuildEnvironment {
    <#
    .SYNOPSIS
        Get properties required to build the project.
    .DESCRIPTION
        Get the properties required to build the project, or elements of the
        project.
 
        All areas of the build process takes it's details from the data produced
        by this function. To influence part of the build process the data
        produced only need be altered.
    .EXAMPLE
        Get-BuildEnvironment
 
        Get build information for the current or any child directories.
    .OUTPUTS
        [PSObject]
    .NOTES
        Author : Paul Broadwith (https://github.com/pauby)
        History : 1.0 - 15/03/18 - Initial release
                  1.1 - 04/04/18 - Added CodeCoverageThreshold parameter
 
        The idea came from the Indented.Build project
        (https://github.com/indented-automation/Indented.Build) and heavily
        modified. Credit should be given to that project.
    .LINK
        Get-ProjectEnvironment
    .LINK
        Get-TestEnvironment
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingEmptyCatchBlock", "", Justification = "Easiest way of jumping out of the block when one fails")]
    [OutputType([PSObject])]
    [CmdletBinding()]
    Param (
        # The type of release we should be building. If this is 'Unknown' then
        # the release type will be deteremined from the commit message.
        [String]
        [ValidateSet( 'Major', 'Minor', 'Build', 'None', 'Unknown' )]
        $ReleaseType,

        # The GitHub Username that has write access to this repo.
        [string]
        $GitHubUsername = '',

        # The GitHub Api Key that has write access to this repo.
        [string]
        $GitHubApiKey = '',

        # The PowerShell Gallery key for publishing the module.
        [string]
        $PSGalleryApiKey = '',

        # Filename of the PowerShell ScriptAnalyzer settings file. This file
        # will be searched for under the project root.
        [string]
        $PSSASettingsName = 'PSScriptAnalyzerSettings.psd1',

        # The PowerShell ScriptAnalyzer Custom Rules folder. This folder will be
        # searched for under the project root and any .psd1 files found under
        # teh folder will be used.
        [string]
        $PSSACustomRulesFolderName = 'CustomAnalyzerRules',

        # The threshold that test code coverage must meet expressed between 0.01 to 1.00.
        [ValidateRange(0.01, 1.00)]
        [single]
        $CodeCoverageThreshold = 0.8
    )

    if (-not $PSBoundParameters.ContainsKey('Verbose')) {
        $VerbosePreference = $PSCmdlet.GetVariableValue('VerbosePreference')
    }

    $buildInfo = Get-ProjectEnvironment

    @(  'LatestVersion', 'ReleaseVersion', 'ReleaseType', 'PSGalleryApiKey', 'RepoBranch', 'RepoLastCommitHash', `
        'RepoLastCommitMessage', 'GitHubUsername', 'GitHubApiKey', 'SourceManifestPath', 'SourceModulePath', `
        'BuildPath', 'BuildManifestPath', 'BuildModulePath', 'PSSASettingsPath', 'PSSACustomRulesPath', `
        'BuildArtifactPath', 'CodeCoverageThreshold'
    ) | ForEach-Object {
        $buildInfo | Add-Member -MemberType NoteProperty -Name $_ -Value ''
    }

    # ReleaseType & PSGallery API Key
    if ([string]::IsNullOrEmpty($ReleaseType) -or $ReleaseType -eq 'Unknown') {
        Write-Verbose "Determining 'ReleaseType' from the last commit message."
        $buildInfo.ReleaseType = Get-ReleaseType -CommitMessage (Get-GitLastCommitMessage)
    }
    else {
       $buildInfo.ReleaseType = $ReleaseType
    }
    $buildInfo.PSGalleryApiKey = $PSGalleryApiKey

    # Git
    $buildInfo.GitHubUsername = $GitHubUsername
    $buildInfo.GitHubApiKey = $GitHubApiKey

    try {
        $buildInfo.RepoBranch = Get-GitBranchName -ErrorAction SilentlyContinue
        $buildInfo.RepoLastCommitHash = Get-GitLastCommitHash -ErrorAction SilentlyContinue
        $buildInfo.RepoLastCommitMessage = Get-GitLastCommitMessage -ErrorAction SilentlyContinue
    }
    catch {
    }

    # Source Paths
    $buildInfo.SourceManifestPath = Join-Path -Path $buildInfo.SourcePath -ChildPath "$($buildInfo.ModuleName).psd1"
    $buildInfo.SourceModulePath = Join-Path -Path $buildInfo.SourcePath -ChildPath "$($buildInfo.ModuleName).psm1"

    # Versions
    if (Test-Path -Path $buildInfo.SourceManifestPath) {
        $buildInfo.LatestVersion = Get-ManifestVersion -Path $buildInfo.SourceManifestPath
        $buildInfo.ReleaseVersion = Get-NextReleaseVersion -LatestVersion $buildInfo.LatestVersion -ReleaseType $buildInfo.ReleaseType
    }
    else {
        throw "Source manifest '$($buildInfo.SourceManifestPath)' does not exist."
    }

    # Build paths
    $buildPath = Join-Path -Path (Join-Path -Path $buildInfo.ProjectRootPath -ChildPath 'buildoutput') -ChildPath $buildInfo.ReleaseVersion
    $buildInfo.BuildPath = $buildPath
    $buildInfo.BuildManifestPath = Join-Path -Path $buildPath -ChildPath "$($buildInfo.ModuleName).psd1"
    $buildInfo.BuildModulePath = Join-Path -Path $buildPath -ChildPath "$($buildInfo.ModuleName).psm1"

    # Build Abstract
    $buildInfo.BuildArtifactPath = Join-Path -Path $buildInfo.ProjectRootPath -ChildPath "$($buildInfo.ModuleName)-$($buildInfo.ReleaseVersion).zip"

    # PSSA
    $buildInfo.CodeCoverageThreshold = $CodeCoverageThreshold
    $settingsPath = Get-ChildItem -Path $PSSASettingsName -File -Recurse | Select-Object -First 1
    if ($settingsPath) {
        $buildInfo.PSSASettingsPath = $settingsPath.FullName
    }
    else {
        Write-Verbose "Could not find PSScriptAnalyzer Settings file '$PSSASettingsName'."
    }

    $path = Get-ChildItem -Path $PSSACustomRulesFolderName -Directory -Recurse | Select-Object -First 1
    if ($path -and (Test-Path -Path (Join-Path -Path $path.FullName -ChildPath '*.psd1'))) {
        $buildInfo.PSSACustomRulesPath = $path.FullName
    }
    else {
        Write-Verbose "No PSScriptAnalyzer Custom Rules folder '$PSSACustomRulesFolderName' found."
    }

    $buildInfo
}

#.ExternalHelp PSModuleBuildHelper-help.xml
function Get-BuildItem {
    <#
    .SYNOPSIS
        Gets the files to be used in the build.
    .DESCRIPTION
        Gets a list of files to be used in the build from the 'source' folder.
    .EXAMPLE
        Get-BuildItem -Type 'Static' -Path 'c:\mymodule\source'
 
        Gets a list of the static build items from the path 'c:\mymodule\source'
    .OUTPUTS
        [System.IO.FileInfo], [System.IO.DirectoryInfo]
    .NOTES
        Author : Paul Broadwith (https://github.com/pauby)
        History : 1.0 - 15/03/18 - Initial release
 
        This function was lifted as is from Indented.Build
        (https://github.com/indented-automation/Indented.Build) so all credit
        goes to that project.
    .LINK
    #>


    [CmdletBinding()]
    [OutputType([System.IO.FileInfo], [System.IO.DirectoryInfo])]
    Param (
        # Gets items by type.
        #
        # ShouldMerge - *.ps1 files from enum*, class*, priv*, pub* and
        # InitializeModule if present.
        # Static - Files which are not within a well known top-level
        # folder. Captures help content in en-US, format
        # files, configuration files, etc.
        [Parameter(Mandatory)]
        [ValidateSet('ShouldMerge', 'Static')]
        [String]$Type,

        # The path to the module 'source' folder.
        [Parameter(Mandatory, ValueFromPipeline)]
        [string]$Path,

        # Exclude script files containing PowerShell classes.
        [Switch]$ExcludeClass
    )

    Push-Location $Path

    $itemTypes = [Ordered]@{
        enumeration    = 'enum*'
        class          = 'class*'
        private        = 'priv*'
        public         = 'pub*'
        initialisation = 'InitializeModule.ps1'
    }

    if ($Type -eq 'ShouldMerge') {
        foreach ($itemType in $itemTypes.Keys) {
            if ($itemType -ne 'class' -or ($itemType -eq 'class' -and -not $ExcludeClass)) {
                $items = Get-ChildItem $itemTypes[$itemType] -Recurse -ErrorAction SilentlyContinue |
                    Where-Object { -not $_.PSIsContainer -and $_.Extension -eq '.ps1' -and $_.Length -gt 0 }

                $orderingFilePath = Join-Path $itemTypes[$itemType] 'order.txt'
                if (Test-Path $orderingFilePath) {
                    [String[]]$order = Get-Content (Resolve-Path $orderingFilePath).Path

                    $items = $items | Sort-Object {
                        $index = $order.IndexOf($_.BaseName)
                        if ($index -eq -1) {
                            [Int32]::MaxValue
                        }
                        else {
                            $index
                        }
                    }, Name
                }

                $items
            }
        }
    }
    elseif ($Type -eq 'Static') {
        [String[]]$exclude = $itemTypes.Values + '*.config', 'test*', 'doc', 'help', '.build*.ps1'

        # Should work, fails when testing.
        # Get-ChildItem -Exclude $exclude
        foreach ($item in Get-ChildItem) {
            $shouldExclude = $false

            foreach ($exclusion in $exclude) {
                if ($item.Name -like $exclusion) {
                    $shouldExclude = $true
                }
            }

            if (-not $shouldExclude) {
                $item
            }
        }
    }

    Pop-Location
}

#.ExternalHelp PSModuleBuildHelper-help.xml
function Get-BuildOperatingSystemDetail {
    <#
    .SYNOPSIS
        Gets the operating system of the build system.
    .DESCRIPTION
        Gets the operating system of the build system in the following formt:
 
            - OSName - name of the operating system (Microsoft Windows 10 Pro)
            - OSArchitecture - x86 or x64 (64-bit)
            - Version - Version number (10.0.16299)
 
        The function is essentially a wrapper around Get-CimInstance
        win32_operatingsystem.
    .EXAMPLE
        Get-BuildOperatingSystem
 
        Returns the name, architecture and version of the current operating system.
    .OUTPUTS
        [PSObject]
    .NOTES
        Author : Paul Broadwith (https://github.com/pauby)
        History : 1.0 - 15/03/18 - Initial release
    .LINK
        Get-BuildPowerShellDetail
    .LINK
        Get-BuildEnvironment
    .LINK
        Get-BuildSystemEnvironment
    #>

    [CmdletBinding()]
    [OutputType([PSObject])]
    Param ()

    Get-CimInstance -ClassName win32_operatingsystem -Property Caption, OSArchitecture, Version | `
        Select-Object -Property @{l = 'OSName'; e = {$_.Caption} }, OSArchitecture, Version
}

#.ExternalHelp PSModuleBuildHelper-help.xml
function Get-BuildPowerShellDetail {
    <#
    .SYNOPSIS
        Return the $PSVersionTable as an object.
    .DESCRIPTION
        Return the $PSVersionTable as an object.
    .EXAMPLE
        Get-BuildPowerShellDetail
 
        Returns the $PSVersionTable as a PSObject.
    .OUTPUTS
        [PSObject]
    .NOTES
        Author : Paul Broadwith (https://github.com/pauby)
        History : 1.0 - 15/03/18 - Initial release
    .LINK
        Get-BuildOperatingSystemDetail
    .LINK
        Get-BuildEnvironment
    .LINK
        Get-BuildEnvironmentDetail
#>


    [CmdletBinding()]
    [OutputType([PSObject])]
    Param ()

    New-Object -TypeName PSObject -Property $PSVersionTable
}

#.ExternalHelp PSModuleBuildHelper-help.xml

$script:ModuleBuildScriptFilename = 'Start-ModuleBuild.ps1'
function Get-BuildScript {
    <#
    .SYNOPSIS
        Provides the full path to the module build script.
    .DESCRIPTION
        Provides the full path to the module build script that is held in the module folder.
    .EXAMPLE
        Get-BuildScript
 
        Returns the path to the Start-ModuleBuild.ps1
    .OUTPUTS
        [System.IO.FileInfo]
    .NOTES
        Author : Paul Broadwith (https://github.com/pauby)
        History : v1.0 - 15/03/18 - Initial release
    .LINK
    #>


    [CmdletBinding()]
    [OutputType([System.IO.FileInfo])]
    Param()

    $rootPath = $PSScriptRoot
    if ((Split-Path -Path $rootPath -Leaf) -eq 'public') {
        $rootPath = Split-Path -Path $PSScriptRoot -Parent
    }

    # returns a [System.IO.FileInfo] object
    Get-Item -Path (Join-Path -Path $rootPath -ChildPath $script:ModuleBuildScriptFilename)
}

#.ExternalHelp PSModuleBuildHelper-help.xml
function Get-BuildSystem {
    <#
    .SYNOPSIS
        Gets the current build system.
    .DESCRIPTION
        Gets the current build system, such as AppVeyor, GitLab CI,
        Teamcity etc. If a build system cannot be detected (such as
        runnning on a local machine), then 'Unknown' will be returned.
    .EXAMPLE
        Get-BuildSystem
 
        Return the current build system.
    .OUTPUTS
        [String]
    .NOTES
        Author : Paul Broadwith (https://github.com/pauby) History : 1.0 -
        15/03/18 - Initial release
    .LINK
        Get-BuildSystemEnvironment
    .LINK
        Get-BuildEnvironment
    #>

    [CmdletBinding()]
    [OutputType([String])]
    Param ()

    # todo Make these values Enums for use elsewhere?
    $system = switch ((Get-Item env:).name) {
        'APPVEYOR_BUILD_FOLDER' { 'AppVeyor'; break }
        'GITLAB_CI' { 'GitLab' ; break }
        'JENKINS_URL' { 'Jenkins'; break }
        'BUILD_REPOSITORY_URI' { 'VSTS'; break }
        'TEAMCITY_VERSION' { 'Teamcity'; break }
        'BAMBOO_BUILDKEY' { 'Bamboo'; break }
        'GOCD_SERVER_URL' { 'GoCD'; break }
        'TRAVIS' { 'Travis'; break }
    }

    if (-not $system) {
        $system = 'Unknown'
    }

    $system
}

#.ExternalHelp PSModuleBuildHelper-help.xml
function Get-BuildSystemEnvironment {
    <#
    .SYNOPSIS
        Get the current build system environment variables.
    .DESCRIPTION
        Get the current build system environment variables. Primarily for use in a
        CI / CD build environments where it is useful to see.
 
        There is a default set of keywords that are searched for which can be
        replaced.
 
        The following CI / CD systems are supported:
 
            - AppVeyor
            - Unknown (retrieves ALL of the current environment variables)
 
        The data is returned as a PSObject.
    .EXAMPLE
        Get-BuildSystemEnvironment
 
        Returns the environment variables for the current build environment.
    .OUTPUTS
        [PSObject]
    .NOTES
        Author : Paul Broadwith (https://github.com/pauby)
        History : 1.0 - 15/03/18 - Initial release
    .LINK
        Get-BuildSystem
    .LINK
        Get-BuildEnvironment
    #>


    [CmdletBinding()]
    [OutputType([PSObject])]
    Param ()

    $envVars = switch (Get-BuildSystem) {
        'AppVeyor' {
            Get-Item env:APPVEYOR*
        }
        'Unknown' {
            Get-Item env:*
        }
    }

    # return the environment as an object
    $envObject = New-Object -TypeName psobject
    $envVars.GetEnumerator() | ForEach-Object {
        $envObject | Add-Member -MemberType NoteProperty -Name $_.Name -Value $_.Value
    }
    $envObject
}

#.ExternalHelp PSModuleBuildHelper-help.xml
function Get-ChangelogVersion {
    <#
    .SYNOPSIS
        Gets the version from the changelog.
    .DESCRIPTION
        Gets the version from the changelog.
    .EXAMPLE
        Get-ChangelogVersion -Path 'c:\mymodule\chaneglog.md'
 
        This will return the first version listed in your changelog that matches the default regular expression.
    .EXAMPLE
        'Value1', 'Value2' | <FUNCTION>
    .INPUTS
        [String]
    .OUTPUTS
        [PSObject]
    .NOTES
        Author : Paul Broadwith (https://github.com/pauby)
        History : 1.0 - 15/03/18 - Initial release
    .LINK
        Get-PowerShellGalleryVersion
    .LINK
        Get-ManifestVersion
    .LINK
        Get-NextReleaseVersion
    #>


    [OutputType([Version])]
    [CmdletBinding()]
    Param (
        # Path to the changelog.
        [Parameter(Mandatory)]
        [ValidateScript ( { Test-Path $_ } )]
        [string]
        $Path,

        # Regular expression to match the version. This assumes that the format
        # of your changelog (in Markdown) versions are:
        #
        # 'hash'hash' v1.0.0 Some text
        # 'hash'hash' v0.9.0 Some more text
        #
        # This will then return the version as 1.0.0. Note that becasue the help
        # files are in Markdown I cannot type a double hash (which is a pound in
        # the US). So 'hash' is '#'
        [string]
        $VersionRegex = '##\s+v(\d+\.\d+\.\d+)'
    )

    # get the version from the changelog.md if we have one
    switch -Regex -File ($Path) {
        $VersionRegex {
            return [version]$Matches[1]
        }
    } # end switch

    # if we get here we did not find the version
    [version]'0.0.0'
}

#.ExternalHelp PSModuleBuildHelper-help.xml
function Get-FunctionParameter {
    <#
    .SYNOPSIS
        Gets the parameters and their properties for a specified function.
    .DESCRIPTION
        Gets the parameters and their properties for a specified function. The
        function must exist within the 'Function:' provider so will not work on
        cmdlets.
    .EXAMPLE
        Get-FunctionParameters -Name 'Get-FunctionParameters'
 
        Returns the properties of each function parameter for the 'Import-Module'
        function excluding the common parameters for Advanced Functions.
    .EXAMPLE
        Get-FunctionParameters -Name 'Get-FunctionParameters' -Exclude ''
 
        Returns the properties of each function parameter for the 'Import-Module'
        function excluding no parameter names.
    .NOTES
        Author : Paul Broadwith (https://github.com/pauby)
        History : 1.0 - 17/03/18 - Initial release
    #>

    [CmdletBinding()]
    Param (
        # Name of the function. The function must exist within the 'Function:'
        # provider or an exception will be thrown.
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Name,

        # Array of parameter names to exclude. By default the Advanced Functions
        # common parameters are excluded. Pass an empty array to have all
        # parameters returned.
        [AllowEmptyCollection()]
        [string[]]
        $Exclude = @('Verbose', 'Debug', 'ErrorAction', 'WarningAction', `
                'InformationAction', 'ErrorVariable', 'WarningVariable', 'InformationVariable', `
                'OutVariable', 'OutBuffer', 'PipelineVariable' )
    )

    if (-not $PSBoundParameters.ContainsKey('Verbose')) {
        $VerbosePreference = $PSCmdlet.GetVariableValue('VerbosePreference')
    }

    try {
        (Get-Item -Path "Function:\$Name").Parameters.GetEnumerator() | Where-Object { $Exclude -notcontains $_.key}
    }
    catch {
        throw "Cannot find function '$Name' loaded in the current session."
    }
}

#.ExternalHelp PSModuleBuildHelper-help.xml
function Get-GitBranchName {
    <#
    .SYNOPSIS
        Get the name of the current branch.
    .DESCRIPTION
        Get the name of the current branch.
    .EXAMPLE
        Get-GitBranchName
 
        Returns the current branch name for the current repo.
    .OUTPUTS
        [String]
    .NOTES
        Author : Paul Broadwith (https://github.com/pauby)
        History : 1.0 - 15/03/18 - Initial release
    .LINK
    #>


    [CmdletBinding()]
    [OutputType([String])]
    Param ()

    $branch = git rev-parse --abbrev-ref HEAD 2>&1
    if (-not $?) {
        throw 'Cannot determine the current git branch.'
    }

    $branch
}

#.ExternalHelp PSModuleBuildHelper-help.xml
function Get-GitChange {
    <#
    .SYNOPSIS
        Gets the git unstaged changed files in the local
    .DESCRIPTION
        Gets a list of hte changed files in the local repository. Only the file
        names are returned not their change status (deleted, modified, unstaged
        etc.)
    .EXAMPLE
        Get-GitChange
 
        Gets the lilst of git changed files in the current repository.
    .OUTPUTS
        [String[]]
    .NOTES
        Author : Paul Broadwith (https://github.com/pauby) History : 1.0 -
        15/03/18 - Initial release
    .LINK
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingInvokeExpression", "", Justification = "This will eventually be rewritten")]
    [CmdletBinding()]
    [OutputType([String[]])]
    Param ()

    # todo this really needs rewritten to use something other than the local git command
    @(Invoke-Expression -Command 'git status -s') | ForEach-Object {
        if ($_ -match '\S*$') {
            $matches[0]
        }
    }
}

#.ExternalHelp PSModuleBuildHelper-help.xml
function Get-GitLastCommitHash {
    <#
    .SYNOPSIS
        Gets the hash of the last commit.
    .DESCRIPTION
        Gets the hash of the last commit.
    .EXAMPLE
        Get-GitLastCommitHash
 
        Gets the hash of hte last commit for the current branch and repo.
    .OUTPUTS
        [String]
    .NOTES
        Author : Paul Broadwith (https://github.com/pauby)
        History : 1.0 - 15/03/18 - Initial release
    .LINK
    #>


    [CmdletBinding()]
    [OutputType([String])]
    param ()

    $hash = git log -1 --pretty=%H 2>&1
    if (-not $?) {
        throw 'There are no commits.'
    }
    else {
        ($hash | Where-Object { $_ } | Out-String).Trim()
    }
}

#.ExternalHelp PSModuleBuildHelper-help.xml
function Get-GitLastCommitMessage {
    <#
    .SYNOPSIS
        Getsh the last Git commit message.
    .DESCRIPTION
        Gets the last Git commit message. If there are no messages to retrieve
        then it will throw and exception.
 
        Uses the 'git' command to retrieve the messages so this must be
        installed.
    .EXAMPLE
        Get-GitLastCommitMessage
 
        Returns the last commit message for the current branch and repo.
    .OUTPUTS
        [String]
    .NOTES
        Author : Paul Broadwith (https://github.com/pauby)
        History : 1.0 - 15/03/18 - Initial release
    .LINK
    #>


    [CmdletBinding()]
    [OutputType([String])]
    Param ()

    $message = git log -1 --pretty=%B 2>&1
    if (-not $?) {
        throw 'There are no commit messages.'
    }
    else {
        ($message | Where-Object { $_ } | Out-String).Trim()
    }
}

#.ExternalHelp PSModuleBuildHelper-help.xml
function Get-ManifestVersion {
    <#
    .SYNOPSIS
        Gets the version from the module manifest.
    .DESCRIPTION
        Gets the version from the module manifest.
    .EXAMPLE
        Get-ManifestVersion -Path 'c:\temp\mymodule.psd1'
    .OUTPUTS
        [Version]
    .NOTES
        Author : Paul Broadwith (https://github.com/pauby)
        History : 1.0 - 15/03/18 - Initial release
    .LINK
        Get-PowerShellGalleryVersion
    .LINK
        Get-NextReleaseVersion
    .LINK
        Get-ChangelogVersion
    #>

    [OutputType([Version])]
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory)]
        [ValidateScript( { Test-Path -Path $_})]
        [string]$Path
    )

    if (-not $PSBoundParameters.ContainsKey('Verbose')) {
        $VerbosePreference = $PSCmdlet.GetVariableValue('VerbosePreference')
    }

    # get the version from the manifest
    try {
        Write-Verbose "Getting the version from the manifest at '$Path'."
        [version](Get-MetaData -Path $Path -PropertyName ModuleVersion -ErrorAction Stop)
    }
    catch {
        [version]'0.0.0'
    }
}

#.ExternalHelp PSModuleBuildHelper-help.xml
function Get-NextReleaseVersion {
    <#
    .SYNOPSIS
        Get's the next release version.
    .DESCRIPTION
        Get's the next release version.
    .EXAMPLE
        Get-NextReleaseVersion -LatestVersion [Version]'1.0.0' -ReleaseType 'Minor'
 
        Will return a new version number of [Version]'1.1.0' which is a minor version increase from '1.0.0'
    .OUTPUTS
        [Version]
    .NOTES
        Author : Paul Broadwith (https://github.com/pauby)
        History : 1.0 - 15/03/18 - Initial release
    .LINK
        Get-PowerShellGalleryVersion
    .LINK
        Get-ManifestVersion
    .LINK
        Get-ChangelogVersion
    #>

    [OutputType([Version])]
    [CmdletBinding()]
    Param (
        # The latest releaase version.
        [Parameter(Mandatory)]
        [Version]
        $LatestVersion,

        # The type of this release.
        [Parameter(Mandatory)]
        [String]
        $ReleaseType
    )

    if (-not $PSBoundParameters.ContainsKey('Verbose')) {
        $VerbosePreference = $PSCmdlet.GetVariableValue('VerbosePreference')
    }

    Write-Verbose "Release type is $($ReleaseType)."
    $version = switch ($ReleaseType) {
        Major {
            Write-Verbose "Incrementing major version number."
            New-Object Version(($LatestVersion.Major + 1), 0, 0)
        }
        Minor {
            Write-Verbose "Incrementing minor version number."
            New-Object Version($LatestVersion.Major, ($LatestVersion.Minor + 1), 0)
        }
        Build {
            Write-Verbose "Incrementing build version number."
            New-Object Version($LatestVersion.Major, $LatestVersion.Minor, ($LatestVersion.Build + 1))
        }
        default {
            # this also catches the ReleaseType of None
            Write-Verbose 'Not incrementing any version numbers.'
            New-Object Version($LatestVersion)
        }
    }

    Write-Verbose "New version will be $version"

    $version
}

#.ExternalHelp PSModuleBuildHelper-help.xml
function Get-PowerShellGalleryVersion {
    <#
    .SYNOPSIS
        Gets the version of the module in the POwerShell Gallery.
    .DESCRIPTION
        Gets the version of the module in the POwerShell Gallery.
    .EXAMPLE
        Get-PowerShellGalleryVersion -Name 'MyModule'
 
        Gets the latest version of the module 'mymodule' listed in the PowerShell Gallery.
    .OUTPUTS
        [Version]
    .NOTES
        Author : Paul Broadwith (https://github.com/pauby)
        History : 1.0 - 15/03/18 - Initial release
    .LINK
        Get-NextReleaseVersion
    .LINK
        Get-ManifestVersion
    .LINK
        Get-ChangelogVersion
    #>

    [OutputType([Version])]
    [CmdletBinding()]
    Param (
        # Name of the module.
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Name
    )

    if (-not $PSBoundParameters.ContainsKey('Verbose')) {
        $VerbosePreference = $PSCmdlet.GetVariableValue('VerbosePreference')
    }

    # get the version from the PowerShell Gallery
    try {
        Write-Verbose "Getting the latest version of '$Name' from the PowerShell Gallery."
        [version](Find-Module -Name $Name -ErrorAction Stop).Version
    }
    catch {
        Write-Verbose "Did not find module '$Name' in the PowerShell Gallery."
        [version]'0.0.0'
    }
}

#.ExternalHelp PSModuleBuildHelper-help.xml
function Get-ProjectEnvironment {
    <#
    .SYNOPSIS
        Get the properties of the project environment.
    .DESCRIPTION
        Get the properties of the project in the current location.
    .EXAMPLE
        Get-ProjectEnvironment
 
        Gets the project environment for the current location.
    .OUTPUTS
        [PSObject]
    .NOTES
        Author : Paul Broadwith (https://github.com/pauby)
        History : 1.0 - 15/03/18 - Initial release
 
        The idea came from the Indented.Build project
        (https://github.com/indented-automation/Indented.Build) and heavily
        modified. Credit should be given to that project.
    .LINK
        Get-BuildEnvironment
    .LINK
        Get-TestEnvironment
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseConsistentWhitespace", "", Justification = "Causes issue with the large hash table")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAlignAssignmentStatement", "", Justification = "Causes issue with the large hash table")]
    [OutputType([PSObject])]
    [CmdletBinding()]
    Param ()

    if (-not $PSBoundParameters.ContainsKey('Verbose')) {
        $VerbosePreference = $PSCmdlet.GetVariableValue('VerbosePreference')
    }

    $projectRoot = Get-ProjectRoot
    $sourcePath = Get-SourcePath -ProjectRoot $projectRoot

    # test path
    try {
        $testPath = (Get-ChildItem (Join-Path -Path $projectRoot -ChildPath 'test*') -Directory | `
                Select-Object -First 1).ToString()
    }
    catch {
        Write-Warning 'We have no tests folder!'
    }

    [PSCustomObject]@{
        ModuleName              = Split-Path -Path $projectRoot -Leaf

        BuildSystem             = Get-BuildSystem

        ProjectRootPath         = $projectRoot
        SourcePath              = $sourcePath
        BuildRootPath           = Join-Path -Path $projectRoot -ChildPath 'buildoutput'
        OutputPath              = Join-Path -Path $projectRoot -ChildPath 'output'
        TestPath                = $testPath
    }
}

#.ExternalHelp PSModuleBuildHelper-help.xml
function Get-ProjectRoot {
    <#
    .SYNOPSIS
        Gets the project root folder name.
    .DESCRIPTION
        Gets the project root folder name by looking for the git repo root
        folder. It uses the 'git' executable to do this so it must be installed.
    .EXAMPLE
        Get-ProjectRoot
 
        Returns the root folder for this project / git repository.
    .OUTPUTS
        [String]
    .NOTES
        Author : Paul Broadwith (https://github.com/pauby)
        History : 1.0 - 15/03/18 - Initial release
 
        This function was lifted from the Indented.Build project
        (https://github.com/indented-automation/Indented.Build) and all credit
        for it should go to that project.
    .LINK
        Get-SourcePath
    #>

    [CmdletBinding()]
    [OutputType([String])]
    Param ()

    $path = git rev-parse --show-toplevel
    if ($null -eq $path) {
        throw 'Not a git repository - cannot provide project root'
    }
    else {
        (Get-Item $path).FullName
    }
}

function Get-ReleaseType {
    <#
    .SYNOPSIS
        Gets the release type from the last commit message.
    .DESCRIPTION
        Gets the release type from the last commit message. The message should start with:
 
            - 'Major release' : 'Major' release type
            - 'Minor release' : 'Minor' release type
            - 'Release' : 'Build' release type
            - None of the above : 'None' release type
    .EXAMPLE
        Get-ReleaseType -CommitMessage ''
 
        Gets' the release type ofr an empty commit message - this will be 'None'
    .OUTPUTS
        [String]
    .NOTES
        Author : Paul Broadwith (https://github.com/pauby)
        History : 1.0 - 15/03/18 - Initial release
    .LINK
        Get-GitLastCommitMessage
    .LINK
        Get-GitLastCommitHash
    .LINK
        Get-GitBranchName
#>

    [OutputType([String])]
    [CmdletBinding()]
    Param (
        # The commit message to be used to determine the release type.
        # The message should start with:
        #
        # - 'Major release' : 'Major' release type
        # - 'Minor release' : 'Minor' release type
        # - 'Release' : 'Build' release type
        # - None of the above : 'None' release type
        [Parameter(Mandatory)]
        [AllowEmptyString()]
        [string]$CommitMessage
    )

    if (-not $PSBoundParameters.ContainsKey('Verbose')) {
        $VerbosePreference = $PSCmdlet.GetVariableValue('VerbosePreference')
    }

    Write-Verbose "Commit message: $CommitMessage"
    switch -Regex ($CommitMessage) {
        '^Major release' {
            Write-Verbose "Commit message contains 'Major release'. ReleaseType is 'Major'."
            'Major'
        }
        '^Minor release' {
            Write-Verbose "Commit message starts with 'Minor release'. Release type is 'Minor'."
            'Minor'
        }
        '^Release' {
            Write-Verbose "Commit message contains 'Release'. Release type is 'Build'"
            'Build'
        }
        default {
            Write-Verbose "Commit message does not contain any release types. Release type is 'None'"
            'None'
        }
    }
}

#.ExternalHelp PSModuleBuildHelper-help.xml
function Get-SourcePath {
    <#
    .SYNOPSIS
        Gets the source path of the module.
    .DESCRIPTION
        Gets the source path of the module under the project root. The source
        path can be called any one of the following:
 
            - source
            - src
            - <NAME>\<NAME>.psd1
 
        Note that the last one is simply a parent folder whose name is the same
        name as the manifest within it.
    .EXAMPLE
        Get-SourcePath -Path 'c:\mymodule'
 
        Gets the path to the module source under 'c:\mymodule'
    .OUTPUTS
        [System.IO.DirectoryInfo], [System.IO.DirectoryInfo[]]
    .NOTES
        Author : Paul Broadwith (https://github.com/pauby) History : 1.0 -
        15/03/18 - Initial release
 
        Note that this function was lifted from teh Indented.Build module
        (https://github.com/indented-automation/Indented.Build) and modified to
        allow 'source' and 'src' folders to be used and replaced Push- and
        Pop-Location with Set-Location as Invoke-Build recommends not using
        those cmdlets. All credit should be given to that project.
    .LINK
        Get-BuildSystem
    .LINK
        Get-BuildEnvironment
    .LINK
        Get-ProjectRoot
    #>


    [CmdletBinding()]
    [OutputType([String])]
    Param (
        # Root path for the project.
        [Parameter(Mandatory, ValueFromPipeline)]
        [String]$ProjectRoot
    )

    if (Test-Path (Join-Path -Path $ProjectRoot -ChildPath (Split-Path -Path $ProjectRoot -Leaf))) {
        return (Join-Path -Path $ProjectRoot -ChildPath (Split-Path -Path $ProjectRoot -Leaf))
    }
    else {
        $folders = 'src', 'source'
        ForEach ($folder in $folders) {
            $sourcePath = Join-Path -Path $ProjectRoot -ChildPath $folder
            if (Test-Path $sourcePath) {
                return $sourcePath
            }
        } #end foreach-object
    } # end else

    # we get here we have found nothing
    throw 'Unable to determine the source path'
}

#.ExternalHelp PSModuleBuildHelper-help.xml
function Get-TestEnvironment {
    <#
    .SYNOPSIS
        Get the properties of the testing environment.
    .DESCRIPTION
        Get the properties required to test the project, or elements of the
        project. Some of the paths require that the module already be built
        beforehand and will throw an exception if that is not the case.
 
        All areas of the test process takes it's details from the data produced
        by this function. To influence part of the build process the data
        produced only need be altered.
 
        Note that BuildPath, BuildManifestPath and BuildModulePath use the
        latest build directory based on it's version number.
 
        This funciton assumes that the module has already been built
    .EXAMPLE
        Get-TestEnvironment
 
        Get test information for the current or any child directories.
    .OUTPUTS
        [PSObject]
    .NOTES
        Author : Paul Broadwith (https://github.com/pauby)
        History : 1.0 - 15/03/18 - Initial release
 
        The idea came from the Indented.Build project
        (https://github.com/indented-automation/Indented.Build) and heavily
        modified. Credit should be given to that project.
    .LINK
        Get-ProjectEnvironment
    .LINK
        Get-BuildEnvironment
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseConsistentWhitespace", "", Justification = "Causes issue with the large hash table")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAlignAssignmentStatement", "", Justification = "Causes issue with the large hash table")]
    [OutputType([PSObject])]
    [CmdletBinding()]
    Param (
        # Filename of the PowerShell ScriptAnalyzer settings file. This file
        # will be searched for under the project root.
        [string]
        $PSSASettingsName = 'PSScriptAnalyzerSettings.psd1',

        # The PowerShell ScriptAnalyzer Custom Rules folder. This folder will be
        # searched for under the project root and any .psd1 files found under
        # teh folder will be used.
        [string]
        $PSSACustomRulesFolderName = 'CustomAnalyzerRules'
    )

    if (-not $PSBoundParameters.ContainsKey('Verbose')) {
        $VerbosePreference = $PSCmdlet.GetVariableValue('VerbosePreference')
    }

    $testInfo = Get-ProjectEnvironment

    $buildOutput = Join-Path -Path $testInfo.ProjectRootPath -ChildPath 'buildoutput'
    # get all of the versions built in the 'buildoutput' folder, and sort them
    # by name and choose the latest one - to sort them by name properly we have
    # to convert the names to versions (as 0.10.0 comes before 0.9.0 when using
    # strings)
    $latestBuildVersion = (Get-Childitem $buildOutput | `
            Select-Object -Property @{ l = 'Name'; e = { [version]$_.Name } } | Sort-Object Name -Descending | `
            Select-Object -First 1).Name.ToString()
    if ($latestBuildVersion -eq '') {
        throw 'Cannot find the latest build of the module. Did you build it beforehand?'
    }

    @(
        @{  name    = 'SourceManifestPath'
            value   = (Join-Path -Path $testInfo.SourcePath -ChildPath "$($testInfo.ModuleName).psd1")
        },
        @{  name    = 'SourceModulePath'
            value   = (Join-Path -Path $testInfo.SourcePath -ChildPath "$($testInfo.ModuleName).psm1")
        },
        @{  name    = 'BuildPath'
            value   = (Join-Path -Path $buildOutput -ChildPath $latestBuildVersion)
        },
        @{  name    = 'BuildManifestPath'
            value   = (Join-Path -Path (Join-Path $buildOutput -ChildPath $latestBuildVersion) -ChildPath "$($testInfo.ModuleName).psd1")
        },
        @{  name    = 'BuildModulePath'
            value   = (Join-Path -Path (Join-Path $buildOutput -ChildPath $latestBuildVersion) -ChildPath "$($testInfo.ModuleName).psm1")
        }
        @{  name    = 'PSSASettingsPath'
            value   = ''
        },
        @{  name    = 'PSSACustomRulesPath'
            value   = ''
        }
    ) | ForEach-Object {
        $testInfo | Add-Member -MemberType NoteProperty -Name $_.Name -Value $_.Value
    }

    # PSSA
    $settingsPath = Get-ChildItem $PSSASettingsName -Recurse | Select-Object -First 1
    if ($settingsPath) {
        $testInfo.PSSASettingsPath = $settingsPath.FullName
    }
    else {
        Write-Verbose "Could not find PSScriptAnalyzer Settings file '$PSSASettingsName'."
    }

    $path = Get-ChildItem -Path $PSSACustomRulesFolderName -Directory -Recurse | Select-Object -First 1
    if ($path -and (Test-Path -Path (Join-Path -Path $path.FullName -ChildPath '*.psd1'))) {
        $testInfo.PSSACustomRulesPath = $path.FullName
    }
    else {
        Write-Verbose "No PSScriptAnalyzer Custom Rules folder '$PSSACustomRulesFolderName' found."
    }

    $testInfo
}

#.ExternalHelp PSModuleBuildHelper-help.xml
function Hide-SensitiveData {
    <#
    .SYNOPSIS
        Searches the object property names for keywords and masks their value if it matches.
    .DESCRIPTION
        Searches the object property names for keywords and masks their value if it matches.
 
        There is a default set of keywords that are searched for which can be
        replaced.
    .EXAMPLE
        $test = New-Object -TypeName PSObject -Property @{ api = 'abcd', name = 'Luke' }
        $test | Hide-SensitiveData
 
        Returns the object with the value of 'api' as '*****'
    .OUTPUTS
        [PSObject]
    .NOTES
        Author : Paul Broadwith (https://github.com/pauby)
        History : 1.0 - 15/03/18 - Initial release
    .LINK
        Get-BuildSystem
    .LINK
        Get-BuildEnvironment
    #>


    [CmdletBinding()]
    [OutputType([PSObject])]
    Param (
        # Object to search the keys for matching keywords. Cannot be $null or empty.
        [Parameter(Mandatory, ValueFromPipeline)]
        [ValidateScript( { $null -ne $_ -and @($_.psobject.properties).count -ne 0 } )]
        [PSObject]
        $InputObject,

        # Array of regular expressions to match the keys against.
        [String[]]
        $Keyword = @('password', 'secret', 'key', 'api', 'token'),

        # The mask that will be used to replace any matching keyword values.
        [String]
        $Mask = '[protected]'
    )

    begin {
        if (-not $PSBoundParameters.ContainsKey('Verbose')) {
            $VerbosePreference = $PSCmdlet.GetVariableValue('VerbosePreference')
        }
    }

    process {
        # objects are passed by reference so any changes to it are made to the original
        # clone the input object so we don't change it
        $copyObj = New-Object -TypeName PsObject
        $InputObject.PSObject.Properties | ForEach-Object {
            $copyObj | Add-Member -MemberType $_.MemberType -Name $_.Name -Value $_.Value
        }

        # filter the environment secret vars
        # if the environment variable is empty then don't mask it
        ForEach ($obj in $copyObj.PSObject.Properties) {
            Write-Debug "Checking '$($obj.Name)' for a keyword match."
            ForEach ($k in $Keyword) {
                # does the name match the secret and it's not empty
                if ($obj.Name -match $k) {
                    $obj.Value = $Mask
                    Write-Verbose "Key matched keyword '$k'. Value changed to '$($obj.Value)'."
                    break
                }
            }
        }
    }

    end {
        $copyObj
    }
}

#.ExternalHelp PSModuleBuildHelper-help.xml
function Initialize-BuildDependency {
    <#
    .SYNOPSIS
        Initializes the build dependencies.
    .DESCRIPTION
        Installs the modules, chocolatey packages and other dependencies for the
        module build.
 
        The format of the dependency hashtable depends on it's type but all of
        them share these keys:
 
            - Name - [required] Name of the dependency. This is the module
                          name, chocolatey package name etc.
            - Version - [optional] Version to be installed - latest by default.
            - Type - [optional] Type of dependency - 'Module', 'Chocolatey'
                          By default this will be a 'Module'
 
        For a 'Module' type there are no additional options.
 
        For a 'Chocolatey' type there are these additional options:
 
            - PackageParams - [optional] Additional parameters that are passed to
                              choco.exe using the --params parameter. Whatever is
                              put in here will be surrounded by single quotes on
                              the choco command line.
    .EXAMPLE
        Initialize-BuildDependency -Dependency $deps
 
        Initializes the dependencies from data in $deps
    .EXAMPLE
        $deps | Initialize-BuildDependency -Dependency $deps
 
        Initializes the dependencies from data in $deps
    .INPUTS
        [Hashtable]
    .NOTES
        Author : Paul Broadwith (https://github.com/pauby)
        History : 1.0 - 03/04/18 - Initial release
    .LINK
        Install-DependentModule
    .LINK
        Install-ChocolateyPackage
    #>

    [CmdletBinding(SupportsShouldProcess)]
    Param (
        # Dependency data to initialize.
        [Parameter(Mandatory, ValueFromPipeline)]
        [ValidateNotNullOrEmpty()]
        [Hashtable]
        $Dependency
    )

    Begin {
        # !if you change this you need to change the default in the switch statement below
        $DefaultType = 'Module'
    }

    Process {
        ForEach ($dep in $Dependency) {
            # check we have a type and if not default to $DefaultType
            $type = $DefaultType
            if ($dep.Keys -contains 'Type') {
                $type = $dep.Type
            }

            # we need to remove the 'Type' key before splatting
            $params = $dep.PsObject.Copy()
            $params.Remove('Type')

            switch ($type) {
                'Module' {
                    Install-DependentModule @params | Out-Null
                    break
                }
                'Chocolatey' {
                    Install-ChocolateyPackage @params | Out-Null
                    break
                }
                default {
                    Install-DependentModule @params | Out-Null
                }

            }
        }
    }
}

#.ExternalHelp PSModuleBuildHelper-help.xml
function Initialize-TestEnvironment {
    <#
    .SYNOPSIS
        Brief synopsis about the function.
    .DESCRIPTION
        Detailed explanation of the purpose of this function.
    .PARAMETER Param1
        The purpose of param1.
    .PARAMETER Param2
        The purpose of param2.
    .EXAMPLE
        <FUNCTION> -Param1 'Value1', 'Value2'
    .EXAMPLE
        'Value1', 'Value2' | <FUNCTION>
    .INPUTS
        [String]
    .OUTPUTS
        [PSObject]
    .NOTES
        Author : Paul Broadwith (https://github.com/pauby)
        History : 1.0 - 16/03/18 - Initial release
    .LINK
        Related things
    #>

    [CmdletBinding()]
    Param()

    if (-not $PSBoundParameters.ContainsKey('Verbose')) {
        $VerbosePreference = $PSCmdlet.GetVariableValue('VerbosePreference')
    }

    $testInfo = Get-TestEnvironment

    if ($testInfo.BuildSystem -eq 'Unknown') {
        # re-import the module each time if you are running it locally - using
        # the SuppressImportModule will only import it once each session
        Write-Verbose "Removing module '$($testInfo.Modulename)'."
        Remove-Module $testInfo.ModuleName -Force
        Write-Verbose "Importing module '$($testInfo.BuildManifestPath)'"
        Import-Module -FullyQualifiedName $testInfo.BuildManifestPath -Force
    }
    elseif (-not (Get-Module -Name $testInfo.ModuleName -ErrorAction SilentlyContinue) -or !(Test-Path Variable:SuppressImportModule) -or !$SuppressImportModule) {
        # The first time this is called, the module will be forcibly (re-)imported.
        # After importing it once, the $SuppressImportModule flag should prevent
        # the module from being imported again for each test file.

        # -Scope Global is needed when running tests from within a CI environment
        Import-Module -FullyQualifiedName $testInfo.BuildManifestPath -Scope Global -Force

        # Set to true so we don't need to import it again for the next test
        $Script:SuppressImportModule = $true
    }

    $testInfo
}

#.ExternalHelp PSModuleBuildHelper-help.xml
function Install-ChocolateyPackage {
    <#
    .SYNOPSIS
        Installs a Chocolatey package.
    .DESCRIPTION
        Installs a Chocolatey package and installs Chocolatey if required.
    .EXAMPLE
        Install-ChocolateyPackage -Name '7zip'
 
        Installs the latest version of 7zip.
    .EXAMPLE
        Install-ChocolateyPackage -Name '7zip' -Version '15.0'
 
        Installs version 15.0 of 7zip.
    .EXAMPLE
        Install-ChocolateyPackage -Name 'dummy' -PackageParams '--noprogress'
 
        Installs the latest version of dummy with the package parameters --noprogress.
    .NOTES
        Author : Paul Broadwith (https://github.com/pauby)
        History : 1.0 - 03/04/18 - Initial release
 
        We use Invoke-Command in this function rather than the & call operator
        as we can mock it.
    .LINK
        Install-DependentModule
    .LINK
        Initialize-BuildDependency
    #>

    [CmdletBinding(SupportsShouldProcess)]
    Param (
        # Name of the Chocolatye package to install
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Name,

        # Version of the package to install.
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateScript( { if ($_ -ne 'latest') { try { [version]$_ } catch { return $false } } $true })]
        [string]
        $Version = 'latest',

        # Chocolatey package parameters to use when installing the package.
        [ValidateNotNullOrEmpty()]
        [string]
        $PackageParams
    )

    Begin {
        if (-not $PSBoundParameters.ContainsKey('Verbose')) {
            $VerbosePreference = $PSCmdlet.GetVariableValue('VerbosePreference')
        }

        $chocoInstalled = $true
        try {
            Invoke-Command -ScriptBlock { choco.exe } | Out-Null
        }
        catch {
            $chocoInstalled = $false
        }

        if (-not $chocoInstalled) {
            try {
                Write-Verbose 'Chocolatey not installed. Installing.'
                if ($pscmdlet.ShouldProcess("Chocolatey", "Install")) {
                    # taken from https://chocolatey.org/install
                    Set-ExecutionPolicy Bypass -Scope Process -Force

                    # create a temp file to hold the Chocolatey install script and then execute it
                    do {
                        $tempFile = "$(Join-Path -Path $env:TEMP -ChildPath([System.Guid]::NewGuid().ToString())).ps1"
                    } while (Test-Path $tempFile)
                    Invoke-WebRequest -UseBasicParsing -Uri 'https://chocolatey.org/install.ps1' -OutFile $tempFile
                    Invoke-Command -Command { .\$tempFile }
                }
            }
            catch {
                throw 'Could not install Chocolatey'
            }
        }
        else {
            Write-Verbose "Chocolatey already installed."
        }
    }

    Process {
        # if we get here chocolatey is installed - install the package
        $chocoParams = @('install', "$Name", '-y', '--no-progress')
        if ($Version -ne 'latest') {
            $chocoParams += "--version=$Version"
        }

        if ($PackageParams) {
            $chocoParams += "--params='$PackageParams'"
        }

        if ($pscmdlet.ShouldProcess("'$Name' with parameters '$($chocoParams -join "" "")'", "Installing Chocolatey package")) {
            # reset the last exit
            $LASTEXITCODE = 0
            Write-Verbose "Installing version '$Version' of '$Name' package with parameters '$($chocoParams -join "" "")'."
            Invoke-Command -ScriptBlock { & choco.exe $chocoParams }

            if ($LASTEXITCODE -ne 0) {
                throw "Chocolatey package '$Name' failed to install with command line '$($chocoParams -join "" "")'"
            }
        }
    }

    End {
        Write-Verbose 'Refreshing the PATH'
        refreshenv
    }
}

#.ExternalHelp PSModuleBuildHelper-help.xml
function Install-DependentModule {
    <#
    .SYNOPSIS
        Installs a module.
    .DESCRIPTION
        Installs a module and. if necessary, installs the Nuget Package Provider
        and trusts the PowerShell Gallery repository. These last two steps are
        necessary if installing on a bare PowerShell install (such as in CI).
    .EXAMPLE
        Install-DependentModule -Name 'Dummy'
 
        Installs thelatest version of the module dummy.
    .OUTPUTS
        [PSObject]
    .NOTES
        Author : Paul Broadwith (https://github.com/pauby)
        History : 1.0 - 03/04/18 - Initial release
    .LINK
        Initialize-BuildDependency
    .LINK
        Install-ChocolateyPackage
    #>

    [CmdletBinding(SupportsShouldProcess)]
    Param (
        # The module name ot install
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Name,

        # The version to install. To install the latest version do not pass this
        # parameter or pass the string 'latest'.
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateScript( { if ($_ -ne 'latest') { try { [version]$_ } catch { return $false } } $true })]
        [string]
        $Version = 'latest'
    )

    if (-not $PSBoundParameters.ContainsKey('Verbose')) {
        $VerbosePreference = $PSCmdlet.GetVariableValue('VerbosePreference')
    }

    # check for install-module cmdlet - this tells us if we have the nuget package provider installed
    try {
        Get-Command -Name 'Install-Module' -ErrorAction Stop | Out-Null
    }
    catch {
        Write-Verbose 'Installing the Nuget Package Provider'
        if ($pscmdlet.ShouldProcess("Nuget Package Provider", "Installing")) {
            Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force | Out-Null
        }
    }

    # check the PSGallery repository is trusted
    if ((Get-PSRepository -Name PSGallery).InstallationPolicy -ne 'Trusted') {
        if ($pscmdlet.ShouldProcess("PowerShell Gallery", "Trusting")) {
            Write-verbose "Trusting PowerShell Gallery"
            # !there is a problem with mocking this cmdlet within Pester so it is not tested
            Set-PSRepository -Name PSGallery -InstallationPolicy Trusted
        }
    }

    # install the module
    if ($pscmdlet.ShouldProcess("$Version version of module $Name", "Installing module")) {
        if ($Version -ne 'latest') {
            Write-Verbose "Installing version '$Version' of module '$Name'"
            Install-Module -Name $Name -RequiredVersion $Version -Scope CurrentUser
            # return the version of the module we installed
            Get-Module -Name $Name -ListAvailable | Where-Object { [Version]$_.Version -eq [Version]$Version }
        }
        else {
            Write-Verbose "Installing latest version of module '$Name'"
            Install-Module -Name $Name -Scope CurrentUser
            # return only the latest version of this module
            Get-Module -Name $Name -ListAvailable | Sort-Object Version -Descending | Select-Object -First 1
        }
    }
}

#.ExternalHelp PSModuleBuildHelper-help.xml
function New-GithubRelease {
    <#
    .SYNOPSIS
        Creates a release on GitHub.
    .DESCRIPTION
        Creates a release on GitHub using an already created artifact.
 
        Note that this does not use the credential store for authentication but uses
        the GitHub Username and Api Key passed to the function.
    .EXAMPLE
        New-GitHubRelease -Version 1.0 -CommitId 'a6fe432'
            -ArtifactPath 'c:\temp\mymodule-1.0.zip' -GitHubUsername 'me'
            -GitHubRepository 'mymodule' -GitHubApiKey '123456789'
 
        This will create a new version 1.0 relase on GitHub for the Commit ID
        'a6fe432' using the artifact 'c:\temp\mymodule-1.0.zip' in the GitHub
        repository 'mymodule' using the Api Key and Username for authentication.
    .NOTES
        Author : Paul Broadwith (https://github.com/pauby)
        History : 1.0 - 15/03/18 - Initial release
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseConsistentWhitespace", "", Justification = "Causes issue with the open hash tables")]
    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Low')]
    Param (
        # The version number to be used for this release.
        [Parameter(Mandatory)]
        [Version]$Version,

        # The Commit ID corresponding to this release.
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$CommitID,

        # Release notes for this release.
        [string]$ReleaseNotes = '',

        # The path to the release artifact.
        [Parameter(Mandatory)]
        [ValidateScript( { Test-Path $_ } )]
        [string]$ArtifactPath,

        # The GitHub Username to use for authentication.
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]
        $GitHubUsername,

        # The Github API key used for authentication.
        # See (https://github.com/blog/1509-personal-api-tokens)
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]
        $GitHubApiKey,

        # Which GitHub repository this release is for.
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]
        $GitHubRepository,

        # Marks this release as a draft.
        [Switch]
        $DraftRelease,

        # Marks this release as a pre-release.
        [switch]
        $PreRelease
    )

    if (-not $PSBoundParameters.ContainsKey('Verbose')) {
        $VerbosePreference = $PSCmdlet.GetVariableValue('VerbosePreference')
    }

    # Get just the name of the file to attach to this release
    $artifact = Split-Path $ArtifactPath -Leaf

    $releaseData = @{
        tag_name         = "v{0}" -f $Version
        target_commitish = $CommitId
        name             = "v{0}" -f $Version
        body             = $ReleaseNotes
        draft            = $DraftRelease.IsPresent
        prerelease       = $PreRelease.IsPresent
    }

    $releaseParams = @{
        Uri         = "https://api.github.com/repos/$GitHubUsername/$GitHubRepository/releases"
        Method      = 'POST'
        Headers     = @{
            Authorization = 'Basic ' + [Convert]::ToBase64String(
                [Text.Encoding]::ASCII.GetBytes($GitHubApiKey + ":x-oauth-basic"))
        }
        ContentType = 'application/json'
        Body        = (ConvertTo-Json $releaseData -Compress)
        UseBasicParsing = $true
    }

    # force use of TLS 1.2
    Write-Verbose 'Forcing using of TLS1.2 for GitHub.'
    [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

    Write-Verbose 'Creating tagged Github release.'
    try {
        $result = Invoke-RestMethod @releaseParams -OutVariable $errorMsg
    }
    catch {
        throw "Could not create tagged GitHub release - '$_'"
    }
    $uploadUri = $result | Select-Object -ExpandProperty upload_url
    $uploadUri = $uploadUri -replace '\{\?name.*\}', "?name=$artifact"

    $uploadParams = @{
        Uri         = $uploadUri
        Method      = 'POST'
        Headers     = @{
            Authorization = 'Basic ' + [Convert]::ToBase64String(
                [Text.Encoding]::ASCII.GetBytes($GitHubApiKey + ":x-oauth-basic"));
        }
        ContentType = 'application/zip'
        InFile      = $ArtifactPath
    }

    if ($PSCmdlet.ShouldProcess("ShouldProcess?")) {
        Write-Verbose 'Uploading artifact.'
        $response = Invoke-RestMethod @uploadParams
        Write-Verbose "Response from artifact upload: $response"
    }
}