ModuleRoot.psm1

# srcFile: /drone/src/src/PSScriptAnalyzer/Invoke-Linter.ps1
function Invoke-Linter() {
    <#
    .SYNOPSIS
        Runs all PSScriptAnalyzer Rules within this repo.
 
    .DESCRIPTION
        This Cmdlet is used in Drone pipeline to run the PSScriptAnalyzer rules..
 
    .INPUTS
        [None] No pipeline input.
 
    .OUTPUTS
        [None] No pipeline output.
 
    .EXAMPLE
        Invoke-Linter
    #>

    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute(
        'PSUseConsistentWhitespace',
        '',
        Justification = 'Hashtable bug in ScriptAnalyzer 1.19.1'
    )]
    param()
    process {
        $Repo = Get-RepoPath
        # Use repo local defaults. if not present use the DroneHelper included as defaults.
        if ($Repo.Resources.ScriptAnalyzerSettingsExist) {
            $currentRules = $Repo.Resources.ScriptAnalyzerSettingsPath
        }
        else {
            $currentRules = $repo.DroneHelper.ScriptAnalyzerDefaultsPath
        }
        $AnalyzerParams = @{
            Path          = $Repo.Src.Path
            Recurse       = $true
            Settings      = $currentRules
            Verbose       = $VerbosePreference
            ReportSummary = $true
        }
        $AnalyzerResults = Invoke-ScriptAnalyzer @AnalyzerParams
        if ( $AnalyzerResults ) {
            $AnalyzerResults | Sort-Object -Property @(
                "ScriptName",
                "Line"
            ) | Format-Table @(
                "Severity",
                "ScriptName",
                "Line",
                "RuleName",
                "Message"
            ) -AutoSize | Out-String | Write-Verbose -Verbose
            $ResultParams = @{
                Type        = 'PSScriptAnalyzer'
                Path        = $Repo.Build.ScriptAnalyzerLogPath
                InputObject = $AnalyzerResults
            }
            Write-ResultFile @ResultParams
            Write-FailureStateFile -StepName 'PSScriptAnalyzer'
            throw 'PS Script Analyzer failed!'
        }
        else {
            $ResultParams = @{
                Type        = 'Custom'
                Path        = $Repo.Build.ScriptAnalyzerLogPath
                InputObject = ':heavy_check_mark: No violations found.'
            }
            Write-ResultFile @ResultParams
        }
    }
}


# srcFile: /drone/src/src/State/Invoke-BuidState.ps1
function Invoke-BuildState {
    <#
    .SYNOPSIS
        Sets final Drone pipeline build state.
 
    .DESCRIPTION
        Marks the pipeline ass succeeded of fail based on the custom state file.
 
    .INPUTS
        [None] No pipeline input.
 
    .OUTPUTS
        [None] No pipeline output.
 
    .EXAMPLE
        Invoke-BuildState
    #>

    [CmdletBinding()]
    param()

    process {
        $Repo = Get-RepoPath
        if ( Test-Path -Path $Repo.FailureLogPath ) {
            throw 'One one more pipeline steps failed. Marking the pipeline as failed!'
        }
    }
}


# srcFile: /drone/src/src/State/Write-FailureStateFile.ps1
function Write-FailureStateFile() {
    <#
    .SYNOPSIS
        Writes the current pipeline step into failure log.
 
    .DESCRIPTION
        This Cmdlet is used to mark single steps as failed without stopping the complete pipeline.
 
    .PARAMETER StepName
        The current DroneHelper step name which should be added into to the log.
 
    .INPUTS
        [None] No pipeline input.
 
    .OUTPUTS
        [None] No pipeline output.
 
    .EXAMPLE
        Write-FailureStateFile
    #>

    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute(
        'PSUseConsistentWhitespace',
        '',
        Justification = 'justification'
    )]
    param (
        [Parameter(Mandatory = $true)]
        [string]$StepName
    )

    process {
        $Repo = Get-RepoPath
        $WriteParams = @{
            FilePath    = $Repo.FailureLogPath
            Encoding    = 'utf8'
            NoClobber   = $true
            Force       = $true
            InputObject = $StepName
        }

        if ( Test-Path -Path $Repo.FailureLogPath ) {
            $WriteParams.Append = $true
        }

        Out-File @WriteParams
    }
}


# srcFile: /drone/src/src/State/Write-ResultFile.ps1
function Write-ResultFile {
    <#
    .SYNOPSIS
        Writes the current pipeline step into failure log.
 
    .DESCRIPTION
        This Cmdlet is used to mark single steps as failed without stopping the complete pipeline.
 
    .PARAMETER StepName
        The current DroneHelper step name which should be added into to the log.
 
    .INPUTS
        [None] No pipeline input.
 
    .OUTPUTS
        [None] No pipeline output.
 
    .EXAMPLE
        Write-FailureStateFile
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [PSCustomObject]$InputObject,

        [Parameter(Mandatory = $true)]
        [String]$Path,

        [Parameter(Mandatory = $true)]
        [ValidateSet('Pester', 'PSScriptAnalyzer', 'FileLinter', 'Custom')]
        [String]$Type
    )

    process {
        [String[]]$Output = @()
        if ($BlockDescription -ne "") {
            $BlockDescription | Out-File -FilePath $Path -Encoding utf8 -Force -NoClobber -Append
        }

        switch ($Type) {
            'Pester' {
                $Output = Format-PesterReport -InputObject $InputObject
            }

            'PSScriptAnalyzer' {
                $Output = Format-ScriptAnalyzerReport -InputObject $InputObject
            }

            'FileLinter' {
                $Output = Format-FileLinterReport -InputObject $InputObject
            }
            'Custom' {
                # nothing to do here
                $Output = $InputObject + [Environment]::NewLine
            }
        }
        $Output | Out-File -FilePath $Path -Encoding utf8 -Force -NoClobber -Append
    }
}


# srcFile: /drone/src/src/Helper/Get-RepoPath.ps1
function Get-RepoPath {
    <#
    .SYNOPSIS
        Updates the module manifest file fields to prepare the new build.
 
    .DESCRIPTION
        Replaces the version fields in the manifest file. Uses Drone env vars populated by pushed tags.
 
    .Parameter SubPath
        An optional string array of sub directories relative to the root.
 
    .INPUTS
        [None] No pipeline input.
 
    .OUTPUTS
        [DroneHelper.Repo.Path] Returns a folder structured like object with relevant full paths.s
 
    .EXAMPLE
        Import-Module -Name DroneHelper; Get-RepoPath
    #>

    [CmdletBinding()]
    [OutputType('DroneHelper.Repo.Path')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute(
        'PSUseConsistentWhitespace',
        '',
        Justification = 'Hashtable bug in ScriptAnalyzer 1.19.1'
    )]
    param (
        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [String[]]$SubPath
    )

    process {
        $root = Split-Path -Path (Get-GitDirectory)

        $BaseName = Get-Item -Path ('{0}/src/*.psd1' -f $root) | Select-Object -ExpandProperty 'BaseName'

        $failureLogPath = Join-Path -Path $root -ChildPath 'failure.log'

        # *.psd1 related
        $manifestPath = Join-Path -Path $root -ChildPath 'src/*.psd1'
        $manifest  = Get-Item -Path $manifestPath

        # *.psm1 related
        $scriptModulePath = Join-Path -Path $root -ChildPath 'src/*.psm1'
        $scriptModule = Get-Item -Path $scriptModulePath

        # Subdir related
        $srcPath = Join-Path -Path $root -ChildPath 'src'
        $binPath = Join-Path -Path $root -ChildPath 'bin'
        $buildPath = Join-Path -Path $root -ChildPath 'build'
        $resourcePath = Join-Path -Path $root -ChildPath 'resources'

        # bin + build artifact related
        $mergedScriptModulePath = Join-Path -Path $binPath -ChildPath $scriptModule.Name

        $artifactName = '{0}.zip' -f $BaseName
        $artifactPath = Join-Path -Path $binPath -ChildPath $artifactName
        $expandPath = Join-Path -Path $binPath -ChildPath $BaseName

        # iteration through the optional sub paths
        $formatPath = Join-Path -Path $srcPath -ChildPath 'Formats/'
        $cachePath = Join-Path -Path $srcPath -ChildPath 'Cache/'

        $docsPath = Join-Path -Path $root -ChildPath 'docs'
        $modulePagePath = Join-Path -Path $docsPath -ChildPath 'README.md'
        $docsMarkdownFilter = Join-Path -Path $docsPath -ChildPath '*.md'

        $subDir = @{}
        foreach ($dir in $SubPath) {
            $subDir.$dir = Join-Path -Path $root -ChildPath $dir
        }

        $changelogPath = Join-Path -Path $root -ChildPath 'CHANGELOG.md'
        $changelogExits = Test-Path -Path $changelogPath

        $ps1Filter = Join-Path -Path $srcPath -ChildPath '*.ps1'

        $pesterLogPath = Join-Path -Path $buildPath -ChildPath 'Pester-Results.log'
        $scriptAnalyzerLogPath = Join-Path -Path $buildPath -ChildPath 'ScriptAnalyzer-Results.log'
        $fileLinterLogPath = Join-Path -Path $buildPath -ChildPath 'FileLinter-Results.log'

        $scriptAnalyzerSettingsPath = Join-Path -Path $resourcePath -ChildPath 'PSScriptAnalyzerSettings.psd1'

        # DroneHelper Module specific

        $droneModuleBase = $MyInvocation.MyCommand.Module.ModuleBase
        $PathParams = @{
            Path      = $droneModuleBase
            ChildPath = 'Rules/PSScriptAnalyzerSettings.psd1'
        }
        $droneAnalyzerDefaultPath = Join-Path @PathParams

        if ($changelogExits) {
            $changelog = Get-Item -Path $changelogPath
        }
        else {
            $changelog = $null
        }

        $Path = [PSCustomObject]@{
            Artifact       = $BaseName
            Root           = $root

            Src            = [PSCustomObject]@{
                Path         = $srcPath
                Manifest     = [PSCustomObject] @{
                    Path = $manifestPath
                    Item = $manifest
                }
                ScriptModule = [PSCustomObject]@{
                    Path = $scriptModulePath
                    Item = $scriptModule
                }
                Formats      = [PSCustomObject]@{
                    Path   = $formatPath
                    Exists = Test-Path -Path $formatPath
                }
                Cache        = [PSCustomObject]@{
                    Path   = $cachePath
                    Exists = Test-Path -Path $cachePath
                }
                PS1Filter    = $ps1Filter
            }
            Bin            = [PSCustomObject]@{
                Path             = $binPath
                ScriptModuleName = $mergedScriptModulePath
                ArtifactName     = $artifactName
                ArtifactPath     = $artifactPath
                ExpandPath       = $expandPath

            }
            Build          = [PSCustomObject]@{
                Path                  = $buildPath
                PesterLogPath         = $pesterLogPath
                ScriptAnalyzerLogPath = $scriptAnalyzerLogPath
                FileLinterLogPath     = $fileLinterLogPath
            }
            Changelog      = [PSCustomObject]@{
                Path   = $changelogPath
                Exists = $changelogExits
                Item   = $changelog
            }
            Docs           = [PSCustomObject]@{
                Path           = $docsPath
                ModulePagePath = $modulePagePath
                MarkdownFilter = $docsMarkdownFilter
            }
            DroneHelper    = [PSCustomObject]@{
                ModuleBase                 = $MyInvocation.MyCommand.Module.ModuleBase
                ScriptAnalyzerDefaultsPath = $droneAnalyzerDefaultPath
            }
            Resources      = [PSCustomObject]@{
                Path                        = $resourcePath
                ScriptAnalyzerSettingsPath  = $scriptAnalyzerSettingsPath
                ScriptAnalyzerSettingsExist = Test-Path -Path $scriptAnalyzerSettingsPath

            }
            FailureLogPath = $failureLogPath
            SubDir         = $subDir
        }
        $Path.PSObject.TypeNames.Insert(0, 'DroneHelper.Repo.Path')
        Write-Output -InputObject $Path
    }

}


# srcFile: /drone/src/src/Helper/Set-EOL.ps1
function Set-EOL {
    <#
    .SYNOPSIS
        Helper function to set the EOL sequence to LF or CRLF.
 
    .DESCRIPTION
        Helper for changing the EOL independent to the current OS defaults.
 
    .PARAMETER Style
        Optional style parameter for `unix` or `win.`. Default is `unix`.
 
    .PARAMETER Path
        Mandatory path for target file.
 
    .INPUTS
        [None] No pipeline input.
 
    .OUTPUTS
        [DroneHelper.Repo.Path] Returns a folder structured like object with relevant full paths.s
 
    .EXAMPLE
        Import-Module -Name DroneHelper; Set-EOL -Path './Readme.md'
    #>

    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute(
        'PSUseShouldProcessForStateChangingFunctions',
        '',
        Justification = 'system state does not change permanent in temp build clients.'
    )]
    param (
        [Parameter(Mandatory = $false)]
        [ValidateSet('unix', 'win')]
        [String]$Style = 'unix',

        [Parameter(Mandatory = $true)]
        [System.IO.FileInfo]$Path
    )

    process {
        if (!(Test-Path $Path.FullName)) {
            Write-Error -Message ('{0} not found!' -f $Path.FullName) -ErrorAction Stop
        }
        switch ($Style) {
            'unix' {
                $eol = "`n"
                Write-Verbose -Message ('Reading {0}' -f $Path.FullName)
                $text = [IO.File]::ReadAllText($Path.FullName) -replace "`r`n", $eol
                Write-Debug -Message $text
            }
            'win' {
                $eol = "`r`n"
                $text = [IO.File]::ReadAllText($Path.FullName) -replace "`n", $eol
            }
        }
        Write-Verbose -Message ("Writing back {0}" -f $Path.FullName)
        [IO.File]::WriteAllText($Path.FullName, $text)

    }
}


# srcFile: /drone/src/src/Docs/New-Docs.ps1
function New-Docs {
    <#
    .SYNOPSIS
        Creates a ne set of markdown based help in the docs folder.
 
    .DESCRIPTION
        This Cmdlet should be used once locally, or after adding new functions. The function `Update-Docs`
        can be used via pipeline to keep the docs up to date.
    .INPUTS
        [None] No pipeline input.
 
    .OUTPUTS
        [None] No pipeline output.
 
    .EXAMPLE
        New-Docs
    #>

    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute(
        'PSUseSingularNouns',
        '',
        Justification = 'New-Doc already in use by other popular modules.'
    )]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute(
        'PSUseShouldProcessForStateChangingFunctions',
        '',
        Justification = 'system state does not change permanent in temp build clients.'
    )]
    param ()

    process {
        $Repo = Get-RepoPath
        Import-Module $Repo.Src.Manifest.Item.FullName -Global -Force
        Import-Module -Name 'platyPS'
        $MarkdownParams = @{
            Module         = $Repo.Artifact
            OutputFolder   = $Repo.Docs.Path
            WithModulePage = $true
            ModulePagePath = $Repo.Docs.ModulePagePath
            Force          = $true
        }
        New-MarkdownHelp @MarkdownParams

        $Docs = Get-Item -Path $Repo.Docs.MarkdownFilter
        foreach ($Doc in $Docs) {
            Write-Verbose -Message ('Converting {0}' -f $Doc.FullName)
            Set-EOL -Path $Doc
        }
    }
}


# srcFile: /drone/src/src/Docs/Update-Docs.ps1
function Update-Docs {
    <#
    .SYNOPSIS
        Publishes powershell module to internal Nexus repository.
 
    .DESCRIPTION
        This Cmdlet is used to publish the module via Drone pipeline.
 
    .INPUTS
        [None] No pipeline input.
 
    .OUTPUTS
        [None] No pipeline output.
 
    .EXAMPLE
        Update-Docs
    #>

    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute(
        'PSUseShouldProcessForStateChangingFunctions',
        '',
        Justification = 'Underlying platyPS can not be mocked.'
    )]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute(
        'PSUseSingularNouns',
        '',
        Justification = 'New-Doc already in use by other popular modules.'
    )]
    param ()

    process {
        $Repo = Get-RepoPath
        Import-Module $Repo.Src.Manifest.Item.FullName -Global -Force
        Import-Module -Name 'platyPS'
        $MarkdownParams = @{
            Path              = $Repo.Docs.Path
            RefreshModulePage = $true
            ModulePagePath    = $Repo.Docs.ModulePagePath
            Force             = $true
        }
        Update-MarkdownHelpModule @MarkdownParams

        $Docs = Get-Item -Path $Repo.Docs.MarkdownFilter
        foreach ($Doc in $Docs) {
            Write-Verbose -Message ('Converting {0}' -f $Doc.FullName)
            Set-EOL -Path $Doc
        }
    }
}


# srcFile: /drone/src/src/FileLinter/Invoke-FileLinter.ps1
function Invoke-FileLinter {
    <#
    .SYNOPSIS
        Runs the file linter for all src files found in current repository.
 
    .DESCRIPTION
        Invoke-FileLinter runs the basic file tests and generates a report file for furher usage in the
        drone pipeline.
 
    .INPUTS
        [None]
 
    .OUTPUTS
        [DroneHelper.FileLinter.Report]
 
    .EXAMPLE
        Invoke-FileLinter
 
    .NOTES
    #>


    [CmdletBinding()]
    [OutputType('DroneHelper.FileLinter.Report')]

    param (
        [Parameter(Mandatory = $false)]
        [switch]$PassThru
    )

    begin {
    }

    process {
        $FileSet = @()
        $Repo = Get-RepoPath
        $RawFiles = (Get-ChildItem -Path $Repo.src.Path -Recurse -File).FullName
        Write-Debug -Message ('EXCLUDE Filter. {0}' -f $Env:EXCLUDE)
        if ($Env:EXCLUDE) {
            $Files = $RawFiles -notmatch $Env:EXCLUDE
            Write-Debug -Message ('Raw File List: {0} | Filtered Files: {1}' -f $RawFiles.Count, $Files.Count)
        }
        else {
            $Files = $RawFiles
        }
        foreach ($file in $Files) {
            Write-Verbose -Message ('Running FileLinter tests for: {0}' -f $file)
            $FileResults = [PSCustomObject]@{
                Name        = $file
                FailedCount = 0
                Tests       = [ordered]@{
                    Encoding     = (Test-FileEncoding -Path $file)
                    BOM          = (Test-FileBOM -Path $file)
                    EOL          = (Test-FileEOL -Path $file)
                    EOF          = (Test-FileEOF -Path $file)
                    TAB          = (Test-FileTab -Path $file)
                    TailingWhite = (Test-FileTailingWhitespace -Path $file)
                }
            }
            Write-Verbose -Message ('Populating property FailedCount for current file.')
            foreach ($item in $FileResults.Tests.Keys.GetEnumerator()) {
                if (($FileResults.Tests.$item) -ne $true) {
                    $FileResults.FailedCount++
                }
            }
            $FileSet += [PSCustomObject]$FileResults
        }
        $LinterReport = [PSCustomObject]@{
            Success     = $null
            FilesCount  = ($FileSet | Measure-Object).Count
            FailedCount = 0
            Files       = $FileSet

        }
        Write-Verbose -Message ('Populating total FailedCount property.')
        foreach ($i in $LinterReport.Files.FailedCount) {
            if ($i -ne 0) {
                $LinterReport.FailedCount = $LinterReport.FailedCount + $i
            }
        }
        if ($LinterReport.FailedCount -eq 0) {
            $LinterReport.Success = $true
        }
        else {
            $LinterReport.Success = $false
        }
        $LinterReport.PSObject.TypeNames.Insert(0, 'DroneHelper.FileLinter.Report')

        $LinterReport.Files | Out-String | Write-Verbose -Verbose

        $LinterReport | Format-Table -Property @(
            'Success',
            'FilesCount',
            'FailedCount'
        ) | Out-String | Write-Verbose -Verbose


        $ResultParams = @{
            Type        = 'FileLinter'
            Path        = $Repo.Build.FileLinterLogPath
            InputObject = $LinterReport
        }
        Write-ResultFile @ResultParams

        if (-not ($LinterReport.Success)) {
            Write-FailureStateFile -StepName 'FileLinter'
            throw 'FileLinter failed!'
        }
    }

    end {
        if ($PassThru.IsPresent) {
            Write-Output $LinterReport
        }
    }
}


# srcFile: /drone/src/src/FileLinter/Test-FileBOM.ps1
function Test-FileBOM {
    <#
    .SYNOPSIS
        Tests given file if native utf8 w/o BOM is used. Returns false if BOM is present.
 
    .DESCRIPTION
        This function is used to test for a valid encoding without BOM.
 
    .PARAMETER Path
        Full or relative path to existing file.
 
    .INPUTS
        [None]
 
    .OUTPUTS
        [bool]
 
    .EXAMPLE
        Test-FileBOM -Path './Testfile.txt'
 
    .NOTES
    #>


    [CmdletBinding()]
    [OutputType([bool])]

    param (
        [Parameter(Mandatory = $true)]
        [ValidateScript(
            {
                Test-Path -Path $_
            }
        )]
        [string]$Path
    )

    begin {
    }

    process {
        try {
            $contents = [byte[]]::new(3)
            $stream = [System.IO.File]::OpenRead($Path)
            $stream.Read($contents, 0, 3) | Out-Null
            $stream.Close()
        }
        catch {
            Write-Error -Message 'Could not read the given file!' -ErrorAction 'Stop'
        }
        Write-Debug -Message ('BOM Content was: {0}' -f ([System.BitConverter]::ToString($contents)))
        if ( $contents[0] -eq 0xEF -and $contents[1] -eq 0xBB -and $contents -eq 0xBF ) {
            Write-Output $false
        }
        else {
            Write-Output $true
        }
    }

    end {
    }
}


# srcFile: /drone/src/src/FileLinter/Test-FileEncoding.ps1
function Test-FileEncoding {
    <#
    .SYNOPSIS
        Returns true if the given file is written in a valid encoding
 
    .DESCRIPTION
        Test the given file against the encoding regex and returns true or false
 
    .PARAMETER Path
        Relative or full path to an existing file.
 
    .PARAMETER Encoding
        Optional custom encoding regex string. Default is (utf8|ascii|xml).
 
    .INPUTS
        [none]
 
    .OUTPUTS
        [bool]
 
    .EXAMPLE
        Test-FileEncoding -Path './testfile.txt'
 
    .NOTES
    #>


    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute(
        'PSAvoidUsingInvokeExpression',
        '',
        Justification = 'static input without user manipulation'
    )]
    [OutputType([bool])]

    param (
        [Parameter(Mandatory = $true)]
        [ValidateScript(
            {
                Test-Path -Path $_
            }
        )]
        [string]$Path,

        [Parameter(Mandatory = $false)]
        [string]$Encoding = '(utf8|utf-8|ascii|xml)'
    )

    begin {
    }

    process {
        try {
            Get-Command -Name 'file' -ErrorAction 'Stop' | Out-Null
        }
        catch {
            Write-Error -Message "Could not find command called 'file'!" -ErrorAction 'Stop'
        }
        $Res = Invoke-Expression -Command ("file '{0}' " -f $Path)
        # Remove the file from matching. Use the latest array element if split doesn't work.
        $ParsedResult = ($Res -split ':')[-1]
        Write-Debug -Message ('Encoding: Raw file output {0}' -f $Res)
        Write-Debug -Message ('Parsed match string: {0}' -f $ParsedResult)
        if ($ParsedResult -match $Encoding) {
            Write-Output $true
        }
        else {
            Write-Output $false
        }
    }

    end {
    }
}


# srcFile: /drone/src/src/FileLinter/Test-FileEOF.ps1
function Test-FileEOF {
    <#
    .SYNOPSIS
        Returns false if EOF isn't an empty line.
 
    .DESCRIPTION
        Test the given file against the EOF standard (final empty/blank line + CRLF) and returns true or false.
 
    .PARAMETER Path
        Relative or full path to an existing file.
 
    .INPUTS
        [none]
 
    .OUTPUTS
        [bool]
 
    .EXAMPLE
        Test-FileEOF -Path './testfile.txt'
 
    .NOTES
    #>


    [CmdletBinding()]
    [OutputType([bool])]

    param (
        [Parameter(Mandatory = $true)]
        [string]$Path
    )

    begin {
    }

    process {
        if (-not (Test-FileEOL -Path $Path)) {
            Write-Warning -Message ('The given file does not use CRLF! ({0})' -f $Path)
            Write-Output $false
        }
        $content = Get-Content -Path $Path -Raw -Encoding 'utf8'
        $lastLine = ($content -split "`r`n")[-1].Length
        # Test for multiple lines without content on EOF
        $perLine = ($content -split "`r`n")[-2].Length
        if (($lastLine -eq 0) -and ($perLine -ne 0)) {
            Write-Debug -Message ('EOF: LastLine {0}; PenultimateLine {1} -> true' -f $lastLine, $perLine)
            Write-Output $true
        }
        else {
            Write-Debug -Message ('EOF: LastLine {0}; PenultimateLine {1} -> false' -f $lastLine, $perLine)
            Write-Output $false
        }
    }

    end {
    }
}


# srcFile: /drone/src/src/FileLinter/Test-FileEOL.ps1
function Test-FileEOL {
    <#
    .SYNOPSIS
        Returns false if EOL isn't CRLF
 
    .DESCRIPTION
        Tests given file against valid EOL. Returns true if CRLF is used.
 
    .PARAMETER Path
        Relative or full path to an existing file.
 
    .INPUTS
        [None]
 
    .OUTPUTS
        [bool]
 
    .EXAMPLE
        Test-FileEOL -Path './TestFile.txt'
 
    .NOTES
    #>


    [CmdletBinding()]
    [OutputType([bool])]

    param (
        [Parameter(Mandatory = $true)]
        [ValidateScript(
            {
                Test-Path -Path $_
            }
        )]
        [string]$Path
    )

    begin {
    }

    process {
        $content = Get-Content -Path $Path -Raw -Encoding 'utf8'
        $CRLFCount = ([regex]::Matches($content, "`r`n$")).Count
        $LFCount = ([regex]::Matches($content, "`n$")).Count

        if ($CRLFCount -eq $LFCount) {
            Write-Debug -Message 'EOL: CRLFCount = LFCount -> true'
            Write-Output $true
        }
        elseif ($CRLFCount -gt $LFCount) {
            Write-Debug -Message 'EOL: CRLFCount > LFCount -> false'
            Write-Output $false
        }
        elseif ($LFCount -gt $CRLFCount) {
            Write-Debug -Message 'EOL: CRLFCount < LFCount -> false'
            Write-Output $false
        }
    }

    end {
    }
}


# srcFile: /drone/src/src/FileLinter/Test-FileTab.ps1
function Test-FileTab {
    <#
    .SYNOPSIS
        Returns false if tab char is used in file.
 
    .DESCRIPTION
        Test the given file if tabs are used. Returns false if any tabs were found.
 
    .PARAMETER Path
        elative or full path to an existing file.
 
    .INPUTS
        [none]
 
    .OUTPUTS
        [bool]
 
    .EXAMPLE
        Test-FileTab -Path './testfile.txt'
 
    .NOTES
    #>


    [CmdletBinding()]
    [OutputType([bool])]

    param (
        [Parameter(Mandatory = $true)]
        [ValidateScript(
            {
                Test-Path -Path $_
            }
        )]
        [string]$Path
    )

    begin {
    }

    process {
        $content = Get-Content -Path $Path -Raw -Encoding 'utf8'
        $Tabs = ([regex]::Matches($content, "`t")).Count

        if ($Tabs -ne 0 ) {
            Write-Debug -Message ('Tabs: {0} -> false' -f $Tabs)
            Write-Output $false
        }
        else {
            Write-Debug -Message ('Tabs: {0} -> true' -f $Tabs)
            Write-Output $true
        }
    }

    end {
    }
}


# srcFile: /drone/src/src/FileLinter/Test-FileTailingWhitespace.ps1
function Test-FileTailingWhitespace {
    <#
    .SYNOPSIS
        Returns false if there are any tailing whitespace in lines.
 
    .DESCRIPTION
        Tests the given file for tailing whitespace. Returns true if not found.
 
    .PARAMETER Path
        Relative or full path to an existing file.
 
    .INPUTS
        [none]
 
    .OUTPUTS
        [bool]
 
    .EXAMPLE
        Test-FileTailingWhitespace.ps1 -Path './testfile.txt'
 
    .NOTES
    #>


    [CmdletBinding()]
    [OutputType([Bool])]

    param (
        [Parameter(Mandatory = $true)]
        [ValidateScript(
            {
                Test-Path -Path $_
            }
        )]
        [string]$Path
    )

    begin {
    }

    process {
        $content = Get-Content -Path $Path -Encoding 'utf8'
        $WhiteSpace = 0
        foreach ($line in $content) {
            $c = ([regex]::Matches($line, "\s+$")).Count
            if ( $c -gt 0 ) {
                $WhiteSpace++
            }
        }

        if ($WhiteSpace -ne 0 ) {
            Write-Debug -Message ('WhiteSpace: {0} -> false' -f $WhiteSpace)
            Write-Output $false
        }
        else {
            Write-Debug -Message ('WhiteSpace: {0} -> true' -f $WhiteSpace)
            Write-Output $true
        }
    }

    end {
    }
}


# srcFile: /drone/src/src/Deploy/Invoke-Publish.ps1
function Invoke-Publish {
    <#
    .SYNOPSIS
        Publishes powershell module to internal Nexus repository.
 
    .DESCRIPTION
        This Cmdlet is used to publish the module via Drone pipeline.
 
    .INPUTS
        [None] No pipeline input.
 
    .OUTPUTS
        [None] No pipeline output.
 
    .EXAMPLE
        Invoke-Publish
    #>

    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute(
        'PSUseConsistentWhitespace',
        '',
        Justification = 'Hashtable bug in ScriptAnalyzer 1.19.1'
    )]
    param ()

    process {
        $Repo = Get-RepoPath
        $ExpandParams = @{
            Path            = $Repo.Bin.ArtifactPath
            DestinationPath = $Repo.Bin.ExpandPath
            Force           = $true
            ErrorAction     = 'Stop'
            Verbose         = $VerbosePreference
        }
        Expand-Archive @ExpandParams

        $PublishParams = @{
            Repository  = 'PSGallery'
            Path        = $Repo.Bin.ExpandPath
            NuGetApiKey = $Env:NuGetToken
            Verbose     = $VerbosePreference
            ErrorAction = 'Stop'
        }
        Publish-Module @PublishParams
    }
}


# srcFile: /drone/src/src/Reports/Format-FileLinterReport.ps1
function Format-FileLinterReport {
    <#
    .SYNOPSIS
        Private helper function used by Write-ResultFile.
    #>


    [CmdletBinding()]
    [OutputType([string])]

    param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [PSTypeName('DroneHelper.FileLinter.Report')]$InputObject
    )

    begin {
    }

    process {
        $Output = @()
        if ($InputObject.Success) {
            $Output += ':heavy_check_mark: No FileLinter violations in {0} files found.' -f $InputObject.FilesCount
        }
        else {
            $Output += "| Result | File | Failed |"
            $Output += "| :----: | :--- | -----: |"
            foreach ($file in $InputObject.Files) {
                if ($file.FailedCount -gt 0) {
                    $failedTestNames = (
                        $file.Tests.GetEnumerator() | Where-Object {
                            $_.Value -eq $false
                        } | Select-Object -ExpandProperty 'Name'
                    ) -join ', '
                    $Output += "| :heavy_exclamation_mark: | ``{0}`` | ``{1}`` |" -f $file.Name, $failedTestNames
                }
            }
        }
        Write-Output $Output
    }

    end {
    }
}


# srcFile: /drone/src/src/Reports/Format-PesterReport.ps1
function Format-PesterReport {
    <#
    .SYNOPSIS
        Private helper function used by Write-ResultFile.
    #>


    [CmdletBinding()]
    [OutputType([string])]

    param (
        [Parameter(Mandatory = $true)]
        [PSCustomObject]$InputObject,

        [Parameter(Mandatory = $false)]
        [ValidateSet('Normal', 'Detailed')]
        [string]$Verbosity = 'Normal'
    )

    begin {
    }

    process {
        $Output = @()
        $Output += "| Result | Test | Duration |"
        $Output += "| :----: | :--- | -------: |"
        foreach ($Result in $InputObject.Tests) {
            switch ($Result.Result) {
                'Passed' {
                    if ($Verbosity -eq 'Detailed') {
                        $RawString = "| :heavy_check_mark: | ``{0}`` | *{1}ms* |"
                        $Output += $RawString -f $Result.ExpandedPath, $Result.UserDuration.Milliseconds
                    }
                }
                'Failed' {
                    $RawString = "| :heavy_exclamation_mark: | ``{0}`` | *{1}ms* |"
                    $Output += $RawString -f $Result.ExpandedPath, $Result.UserDuration.Milliseconds
                    $Parsed = $Result.ErrorRecord.Exception.Message -split "`n" | Select-Object -First 1
                    $Output += "| :fire: | **{0}** | :fire: |" -f $Parsed
                }
                'NotRun' {
                    $RawString = "| :trident: | ``{0}`` | *n/a* |"
                    $Output += $RawString -f $Result.ExpandedPath
                }
                Default {
                    $RawString = "| :warning: | ``{0}`` | *{1}ms* |"
                    $Output += $RawString -f $Result.ExpandedPath, $Result.UserDuration.Milliseconds
                }
            }
        }
        $Output += [Environment]::NewLine
        # Writing test result summary
        $Output += @(
            ':test_tube: **{0}** Total Tests (' -f $InputObject.TotalCount +
            ':heavy_check_mark: ``{0} Passed`` :white_small_square:' -f $InputObject.PassedCount +
            ':trident: ``{0} Skipped / NotRun`` :white_small_square: ' -f (
                $InputObject.SkippedCount + $InputObject.NotRunCount
            ) +
            ':warning: ``Unknown`` :white_small_square: ' +
            ':heavy_exclamation_mark: ``{0} Failed``)' -f $InputObject.FailedCount
        )
        # Writing code coverage summary
        # Covered 37,38% / 75%. 610 analyzed Commands in 26 Files.
        $Output += @(
            ':bookmark_tabs: Covered **{0}%** / ' -f [Math]::Round($InputObject.CodeCoverage.CoveragePercent, 2) +
            '{0}%. (' -f $InputObject.CodeCoverage.CoveragePercentTarget +
            ':bookmark: ``{0} analyzed Commands`` ' -f $InputObject.CodeCoverage.CommandsAnalyzedCount +
            ':page_facing_up: ``in {0} Files``)' -f $InputObject.CodeCoverage.FilesAnalyzedCount
        )
        Write-Output $Output
    }

    end {
    }
}


# srcFile: /drone/src/src/Reports/Format-ScriptAnalyzerReport.ps1
function Format-ScriptAnalyzerReport {
    <#
    .SYNOPSIS
        Private helper function used by Write-ResultFile.
    #>


    [CmdletBinding()]
    [OutputType([string])]

    param (
        [Parameter(Mandatory = $true)]
        [PSCustomObject]$InputObject
    )

    begin {
    }

    process {
        $Output = @()
        $Output += "| Severity | ScriptName | Line | RuleName | Message |"
        $Output += "| :------: | :--------- | :--: | :------- | :------ |"

        foreach ( $v in $InputObject ) {
            switch ($v.Severity) {
                'Warning' { $Emoji = ':warning:' }
                'Error' { $Emoji = ':heavy_exclamation_mark:' }
                'Information' { $Emoji = ':mag:' }
                Default { $Emoji = ':fried_egg:' }
            }
            $RawString = "| {0} | {1} | {2} | {3} | {4} |"
            $Output += $RawString -f $Emoji, $v.ScriptName, $v.Line, $v.RuleName, $v.Message
        }
        $RuleURL = 'https://github.com/PowerShell/PSScriptAnalyzer/tree/master/RuleDocumentation'
        $Output += "`n> See [RuleDocumentation]({0}) for additional help.`n" -f $RuleURL
        Write-Output $Output
    }

    end {
    }
}


# srcFile: /drone/src/src/Deps/Install-ModuleDependency.ps1
function Install-ModuleDependency {
    <#
    .SYNOPSIS
        Install required modules of the module manifest file.
 
    .DESCRIPTION
        Use this cmdlet to install required modules of the module manifest file.
 
    .INPUTS
        [None]
 
    .OUTPUTS
        [None]
 
    .EXAMPLE
        Install-ModuleDependency
 
    .NOTES
    #>


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

    begin {
    }

    process {
        $Repo = Get-RepoPath
        $ManifestContent = Import-PowerShellDataFile -Path $Repo.Src.Manifest.Item.FullName
        if ($ManifestContent.RequiredModules) {
            foreach ($Module in $ManifestContent.RequiredModules) {
                if ($Module.RequiredVersion) {
                    $ParamsInstallModule = @{
                        Name            = $Module.ModuleName
                        Scope           = 'AllUsers'
                        RequiredVersion = $Module.RequiredVersion
                        Force           = $true
                        AllowClobber    = $true
                        Verbose         = $VerbosePreference
                        ErrorAction     = 'Stop'
                    }
                }
                else {
                    $ParamsInstallModule = @{
                        Name           = $Module.ModuleName
                        Scope          = 'AllUsers'
                        MinimumVersion = $Module.ModuleVersion
                        Force          = $true
                        AllowClobber   = $true
                        Verbose        = $VerbosePreference
                        ErrorAction    = 'Stop'
                    }
                }
                try {
                    Install-Module @ParamsInstallModule
                    $Message = 'Module <{0}> successfully installed' -f $Module.ModuleName
                    Write-Verbose -Message $Message
                }
                catch {
                    $Message = 'Module <{0}> could not be installed! ' -f $Module.ModuleName
                    $Message += $_.Exception.Message
                    Write-Error -Message $Message -ErrorAction 'Stop'
                }
            }
        }
        else {
            Write-Verbose -Message 'no required modules found...'
        }
    }

    end {
    }
}


# srcFile: /drone/src/src/Deps/Invoke-InstallDependency.ps1
function Invoke-InstallDependency {
    <#
    .SYNOPSIS
        Install required modules for executing the DroneHelper pipeline helpers.
 
    .DESCRIPTION
        This can be used in drone.io docker pipeline if the modules are not integrated in the build image.
 
    .INPUTS
        [None] No Input required.
 
    .OUTPUTS
        [None] No Output
 
    .EXAMPLE
        Import-Module -Name DroneHelper; Invoke-Install-Dependency
    #>

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

    process {
        try {
            $PSScriptParams = @{
                Name               = 'PSScriptAnalyzer'
                Scope              = 'CurrentUser'
                RequiredVersion    = '1.20.0'
                Force              = $true
                SkipPublisherCheck = $true
                AllowClobber       = $true
                Verbose            = $VerbosePreference
                ErrorAction        = 'Stop'
            }
            Install-Module @PSScriptParams

            $PesterParams = @{
                Name               = 'Pester'
                Scope              = 'CurrentUser'
                RequiredVersion    = '5.3.1'
                Force              = $true
                SkipPublisherCheck = $true
                AllowClobber       = $true
                Verbose            = $VerbosePreference
                ErrorAction        = 'Stop'
            }
            Install-Module @PesterParams

            $PoshParams = @{
                Name               = 'posh-git'
                Scope              = 'CurrentUser'
                RequiredVersion    = '1.1.0'
                Force              = $true
                SkipPublisherCheck = $true
                AllowClobber       = $true
                Verbose            = $VerbosePreference
                ErrorAction        = 'Stop'
            }
            Install-Module @PoshParams
        }
        catch {
            $ExecParams = @{
                Exception   = [System.Exception]::new(
                    'Could not install required build dependencies!',
                    $PSItem.Exception
                )
                ErrorAction = 'Stop'
            }
            Write-Error @ExecParams
        }

    }
}


# srcFile: /drone/src/src/PRComment/Send-PRComment.ps1
function Send-PRComment {
    <#
    .SYNOPSIS
        Sends build report as Gitea PR comment.
 
    .DESCRIPTION
        Send-PRComment is used to report the build details from drone.io pipeline.
 
    .PARAMETER Mode
        Sets the report mode. Default is 'Renew'. This mode deletes the old pr comments and creates a new onw.
        Also available:
          - 'Add' -> simply adds new pr comments.
          - 'Edit' -> Edits the last known pr comment. Doesn't clean old ones.
 
    .INPUTS
        [None].
 
    .OUTPUTS
        [None]
 
    .EXAMPLE
        Send-PRComment
        Depends on Drone.IO injected environment vars. Doesn't work locally on dev systems.
    .NOTES
    #>


    [CmdletBinding()]
    #[OutputType([string])]
    param (
        [Parameter(Mandatory = $false, HelpMessage = 'HelpMessage')]
        [ValidateSet('Add', 'Edit', 'Renew')]
        [string]$Mode = 'Renew',

        [Parameter(Mandatory = $false, HelpMessage = 'Gitea user for drone bot')]
        [ValidateNotNullOrEmpty()]
        [string]$GiteaUser = 'drone-bot'

    )

    begin {
        # workaround for false positive PSReviewUnusedParameter
        $null = $GiteaUser
    }

    process {
        $Repo = Get-RepoPath
        $Workspace = $Repo.Root
        Write-Debug -Message ('Workspace: {0}' -f $Workspace)
        $PRCommentFile = Join-Path -Path $Workspace -ChildPath 'pr_comment.log'
        Write-Debug -Message ('PRCommentFile: {0}' -f $PRCommentFile)
        $PipelineStateFile = Join-Path -Path $Workspace -ChildPath 'failure.log'
        Write-Debug -Message ('PipelineStateFile: {0}' -f $PipelineStateFile)

        Write-Debug -Message ('CUSTOM_PIPELINE_STATE: {0}' -f $Env:CUSTOM_PIPELINE_STATE)
        if ($Env:CUSTOM_PIPELINE_STATE -eq $true) {
            if (Test-Path $PipelineStateFile) {
                Write-Debug -Message ('Setting custom pipeline status to failed')
                $PipelineState = 'failed'
            }
            else {
                Write-Debug -Message ('Setting custom pipeline status to success')
                $PipelineState = 'success'
            }
        }
        else {
            Write-Debug -Message ('Setting global drone status {0}' -f $Env:DRONE_BUILD_STATUS)
            $PipelineState = $Env:DRONE_BUILD_STATUS
        }

        if ($Env:GITEA_BASE) {
            $GiteaBase = $Env:GITEA_BASE
        }
        else {
            $GiteaBase = 'https://gitea.ocram85.com'
        }

        $APIHeaders = @{
            accept         = 'application/json'
            'Content-Type' = 'application/json'
        }

        # Can be used with POST method to add new comment. Used with GET method returns all comments.
        $CommentAPICall = ('{0}/api/v1/repos/{1}/{2}/issues/{3}/comments?access_token={4}' -f
            $GiteaBase,
            $Env:DRONE_REPO_OWNER,
            $Env:DRONE_REPO_NAME,
            $Env:DRONE_PULL_REQUEST,
            $Env:GITEA_TOKEN
        )

        # Update Comment API endpoint: 0 - GiteaBase, 1 - Owner, 2- Repo, 3 - PR, 4 - Token
        # Method Delete - removes the given comment. Patch - updates the given comment.
        $UpdateAPICall = '{0}/api/v1/repos/{1}/{2}/issues/comments/{3}?access_token={4}'


        if ($Mode -eq 'Renew') {
            $Comments = Invoke-RestMethod -Method 'Get' -Headers $APIHeaders -Uri $CommentAPICall
            $DroneComments = $Comments | Where-Object {
                $_.user.login -eq $GiteaUser
            } | Select-Object -ExpandProperty 'id'
            Write-Debug -Message ('Found Drone comments: {0}.' -f ($DroneComments -join ', '))
            foreach ($id in $DroneComments) {
                $ExtAPI = $UpdateAPICall -f @(
                    $GiteaBase,
                    $Env:DRONE_REPO_OWNER,
                    $Env:DRONE_REPO_NAME,
                    $id,
                    $Env:GITEA_TOKEN
                )
                Write-Debug -Message ('Exec API Call: {0}' -f $ExtAPI)
                Invoke-RestMethod -Method 'Delete' -Headers $APIHeaders -Uri $ExtAPI
            }
        }

        if ($Mode -eq 'Edit') {
            $Comments = Invoke-RestMethod -Method 'Get' -Headers $APIHeaders -Uri $CommentAPICall
            $DroneComments = $Comments | Where-Object {
                $_.user.login -eq 'drone'
            } | Select-Object -ExpandProperty 'id'
            Write-Debug -Message ('Found Drone comments: {0}.' -f ($DroneComments -join ', '))
            $EditId = $DroneComments | Sort-Object | Select-Object -Last 1
            Write-Debug -Message ('Edit Comment with id {0}' -f $EditId)
        }

        $PRCommentHeader = ('> Drone.io PR Build No. [#{0}]({1}://{2}/{3}/{4}): ``{5}``' -f
            $Env:DRONE_BUILD_NUMBER,
            $Env:DRONE_SYSTEM_PROTO,
            $Env:DRONE_SYSTEM_HOST,
            $Env:DRONE_REPO,
            $Env:DRONE_BUILD_NUMBER,
            $PipelineState

        )
        $PRCommentHeader | Out-File -FilePath $PRCommentFile -Encoding 'utf8'

        $LogFiles = (Get-ChildItem -Path $Env:LOG_FILES -File).FullName
        foreach ($file in $LogFiles) {
            if (Test-Path -Path $file) {
                ('#### ``{0}``' -f $file) | Out-File -FilePath $PRCommentFile -Append -Encoding 'utf8'
                $fileContent = Get-Content -Path $file -Raw -Encoding utf8
                $fileContent | Out-File -FilePath $PRCommentFile -Append -Encoding 'utf8'
                [Environment]::NewLine | Out-File -FilePath $PRCommentFile -Append -Encoding 'utf8' -NoNewline

            }
            else {
                Write-Warning -Message ('Given file {0} not found!' -f $file)
                ('##### ``{0}`` not found!' -f $file) | Out-File -FilePath $PRCommentFile -Append -Encoding 'utf8'
            }
        }

        if ($Mode -eq 'Edit') {
            'Last mod: {0}' -f (Get-Date -Format 'u') | Out-File -FilePath $PRCommentFile -Append -Encoding 'utf8'
        }
        'end.' | Out-File -FilePath $PRCommentFile -Append -Encoding 'utf8'

        $PRCommentJSON = ConvertTo-Json -InputObject @{
            Body = Get-Content -Path $PRCommentFile -Encoding utf8 -Raw
        }
        Write-Debug -Message ('PR JSON body has a size of {0} chars' -f $PRCommentJSON.length)

        if ($Mode -ne 'Edit') {
            Write-Debug -Message 'Adding new Comment.'
            Invoke-RestMethod -Method 'Post' -Headers $APIHeaders -Uri $CommentAPICall -Body $PRCommentJSON
        }
        else {
            $ExtAPI = $UpdateAPICall -f @(
                $GiteaBase,
                $Env:DRONE_REPO_OWNER,
                $Env:DRONE_REPO_NAME,
                $EditId,
                $Env:GITEA_TOKEN
            )
            Write-Debug -Message 'Edit last comment.'
            Invoke-RestMethod -Method 'Patch' -Headers $APIHeaders -Uri $ExtAPI -Body $PRCommentJSON
        }
    }

    end {
    }
}


# srcFile: /drone/src/src/Pester/Invoke-UnitTest.ps1
function Invoke-UnitTest {
    <#
    .SYNOPSIS
        Runs all Pester tests within this repo.
 
    .DESCRIPTION
        This Cmdlet is used in Drone pipeline to perform the Pester based unit tests.
 
    .PARAMETER CoverageFormat
        Pester provides the formats JaCoCo ans CoverageGutters. Default is JaCoCo.
        These are the known use cases:
        - JaCoCo -> Used as standard coverage report used by sonar
        - CoverageGutters -> Custom Format to show coverage in VSCode.
 
    .PARAMETER Verbosity
        This parameter sets the Pester detail level. Default is 'Normal.' Available values are:
        'None', 'Normal', 'Detailed', 'Diagnostic'
 
    .PARAMETER PassThru
        Tells Invoke-UnitTest to write back the Pester results into your variable / output.
 
    .PARAMETER Tag
        Pester build in tag filter as string array.
 
    .PARAMETER ExcludeTag
        Pester build in exclude filter for tests as string array.
 
    .INPUTS
        [None] No pipeline input.
 
    .OUTPUTS
        [None] No pipeline output.
 
    .EXAMPLE
        Invoke-UnitTest
    #>

    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute(
        'PSUseConsistentWhitespace',
        '',
        Justification = 'Hashtable bug in ScriptAnalyzer 1.19.1'
    )]
    param (
        [Parameter( Mandatory = $false )]
        [ValidateSet('JaCoCo', 'CoverageGutters')]
        [string]$CoverageFormat = 'JaCoCo',

        [Parameter(Mandatory = $false)]
        [ValidateSet('None', 'Normal', 'Detailed', 'Diagnostic')]
        [string]$Verbosity = 'Normal',

        [Parameter(Mandatory = $false)]
        [switch]$PassThru,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [string[]]$Tag,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [string[]]$ExcludeTag
    )

    process {
        $Repo = Get-RepoPath
        Write-Verbose -Message '===== Running Pester =====' -Verbose:$VerbosePreference
        $PesterConf = New-PesterConfiguration
        $PesterConf.Run.Path = (Resolve-Path -Path './src').Path
        $PesterConf.Run.Exit = $false
        $PesterConf.Run.PassThru = $true
        $PesterConf.CodeCoverage.Enabled = $true
        $PesterConf.CodeCoverage.OutputFormat = $CoverageFormat
        $PesterConf.TestResult.Enabled = $true
        $CovFiles = Get-ChildItem -Path $Repo.Src.PS1Filter -Recurse | Where-Object {
            $_.BaseName -notmatch '.Tests'
        } | Select-Object -ExpandProperty 'FullName'
        $PesterConf.CodeCoverage.Path = $CovFiles
        $PesterConf.Output.Verbosity = $Verbosity

        # Set Tags if given
        if ($Tag) {
            $PesterConf.Filter.Tag = $Tag
        }
        if ($ExcludeTag) {
            $PesterConf.Filter.ExcludeTag = $ExcludeTag
        }
        $TestResults = Invoke-Pester -Configuration $PesterConf -ErrorAction 'Stop'
        try {
            $ResFileParams = @{
                InputObject = $TestResults
                Path        = $Repo.Build.PesterLogPath
                Type        = 'Pester'
                ErrorAction = 'Stop'
            }
            Write-ResultFile @ResFileParams
        }
        catch {
            Write-FailureStateFile -StepName 'Pester'
            throw ('{0} tests failed!' -f $TestResults.FailedCount)
        }

        if ($TestResults.FailedCount -gt 0) {
            Write-FailureStateFile -StepName 'Pester'
            throw ('{0} tests failed!' -f $TestResults.FailedCount)
        }
        if ($PassThru.IsPresent) {
            Write-Output -InputObject $TestResults
        }
    }
}


# srcFile: /drone/src/src/Build/Merge-ModuleRoot.ps1
function Merge-ModuleRoot {
    <#
    .SYNOPSIS
        Merges single ps1 files into one module script file.
 
    .DESCRIPTION
        This Cmdlet is used in build pipeline to reduce the file load and import performance to the target module.
 
    .INPUTS
        [None] No pipeline input.
 
    .OUTPUTS
        [None] No pipeline output.
 
    .EXAMPLE
        Import-Module -Name DroneHelper; Merge-ModuleRoot
    #>

    [CmdletBinding()]
    param ()

    process {
        $Repo = Get-RepoPath

        $srcFiles = Get-ChildItem -Path $Repo.Src.Path -Recurse -File |  Where-Object {
            ($_.Name -notmatch '.Tests.') -and ($_.Name -match '.ps1') -and ($_.Name -notmatch '.ps1xml')
        }
        $Output = @()
        foreach ($psFile in $srcFiles) {
            $fileContent = Get-Content -Path $psFile.FullName -Raw -Encoding 'utf8'
            $Output += '# srcFile: {0}' -f $psFile.FullName
            $Output += $fileContent.TrimEnd()
            $Output += '{0}' -f [Environment]::NewLine
        }
        try {
            $Output | Out-File -FilePath $Repo.Bin.ScriptModuleName -Encoding 'utf8' -Force -ErrorAction Stop
        }
        catch {
            Write-FailureStateFile -StepName 'MergeModuleRoot'
            throw 'Could not write the final module root script file!'
        }
    }
}


# srcFile: /drone/src/src/Build/New-BuildPackage.ps1
function New-BuildPackage {
    <#
    .SYNOPSIS
        Creates a new module package as compressed archive.
 
    .DESCRIPTION
        This function is used in build pipeline to create an uploadable module version for the Gitea release page.
 
    .PARAMETER AdditionalPath
        You can provide additional paths to add files or folders in published module.
 
    .INPUTS
        [None] No pipeline input.
 
    .OUTPUTS
        [None] No pipeline output.
 
    .EXAMPLE
        Import-Module -Name DroneHelper; New-BuildPackage
    #>

    [CmdletBinding()]
    [OutputType()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute(
        'PSUseConsistentWhitespace',
        '',
        Justification = 'Hashtable bug in ScriptAnalyzer 1.19.1'
    )]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute(
        'PSUseShouldProcessForStateChangingFunctions',
        '',
        Justification = 'system state does not change permanent in temp build clients.'
    )]

    param (
        [Parameter(Mandatory = $false)]
        [String[]]$AdditionalPath
    )

    process {
        $Repo = Get-RepoPath
        $res = @()
        foreach ($item in $AdditionalPath) {
            try {
                $res += Resolve-Path -Path $item -ErrorAction Stop
            }
            catch {
                Write-Error -Message ('The given additional path does not exist! ({0})' -f $item) -ErrorAction Stop
            }
        }
        Merge-ModuleRoot -ErrorAction Stop
        $CompressParams = @{
            Path            = @(
                # psm1 file
                $Repo.Bin.ScriptModuleName
                # psd1 file
                $Repo.Src.Manifest.Item.FullName
                # Formats/ folder
                $Repo.Src.Formats.Path
            )
            DestinationPath = $Repo.Bin.ArtifactPath
            Force           = $true
            ErrorAction     = 'Stop'
            Verbose         = $VerbosePreference
        }
        $CompressParams.Path += $res
        try {
            Compress-Archive @CompressParams
        }
        catch {
            Write-FailureStateFile -StepName 'BuildPackage'
            throw $_.Exception.Message
        }
    }
}


# srcFile: /drone/src/src/Build/Update-ModuleMeta.ps1
function Update-ModuleMeta {
    <#
    .SYNOPSIS
        Updates the module manifest file fields to prepare the new build.
 
    .DESCRIPTION
        Replaces the version fields in the manifest file. Uses Drone env vars populated by pushed tags.
 
    .INPUTS
        [None] No pipeline input.
 
    .OUTPUTS
        [None] No pipeline output.
 
    .EXAMPLE
        Import-Module -Name DroneHelper; Update-ModuleMeta
    #>

    [CmdletBinding()]
    [OutputType()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute(
        'PSUseConsistentWhitespace',
        '',
        Justification = 'Hashtable bug in ScriptAnalyzer 1.19.1'
    )]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute(
        'PSUseShouldProcessForStateChangingFunctions',
        '',
        Justification = 'system state does not change permanent in temp build clients.'
    )]
    param ()

    process {
        if ($Env:DRONE) {
            $Repo = Get-RepoPath
            if ($Env:DRONE_BUILD_EVENT -eq 'tag') {
                if ($null -ne $Env:DRONE_SEMVER) {
                    $nVersion = $Env:DRONE_SEMVER_SHORT

                    if ($null -ne $Env:DRONE_SEMVER_PRERELEASE) {
                        $nPreRelease = $Env:DRONE_SEMVER_PRERELEASE
                    }
                    $ModManifestParams = @{
                        Path          = $Repo.Src.Manifest.Item.FullName
                        ModuleVersion = $nVersion
                        ErrorAction   = 'Stop'
                    }
                    if ($nPreRelease) {
                        $ModManifestParams.PreRelease = $nPreRelease
                    }

                    $ManifestData = Test-ModuleManifest -Path $Repo.Src.Manifest.Item.FullName

                    if (
                        ($nVersion -ne $ManifestData.Version) -or
                        ($nVersion -ne $ManifestData.PrivateData.PSData.Prerelease)
                    ) {
                        Update-ModuleManifest @ModManifestParams
                    }
                    else {
                        Write-Verbose -Message 'Identical version given. Skipping update.'
                    }
                }
                else {
                    Write-Verbose -Message 'Could not read the new Tag / Semver!'
                }
            }
            else {
                Write-Verbose -Message 'This pipeline was not triggered by a tag.'
            }
        }
        else {
            Write-Verbose -Message 'Running outside of drone.io pipeline. Skipping module update!'
        }
    }
}


# srcFile: /drone/src/src/Changelog/Update-Changelog.ps1
function Update-Changelog {
    <#
    .SYNOPSIS
        Updates the changelog file with recent commits
 
    .DESCRIPTION
        This helper function is used to insert recent changes for an upcoming release.
 
    .Parameter NewVersion
        Provide a valid semver based version tag for the upcoming release like:
 
        - `v0.0.1-dev1`
        - `v1.0.0`
 
    .Parameter SkipCleanup
        You can skip the tag update and additional test.
 
    .INPUTS
        [None] No pipeline input.
 
    .OUTPUTS
        [None] no pipeline putput.
 
    .EXAMPLE
        Import-Module -Name DroneHelper; Update-Changelog -NewVersion '0.0.1-dev5'
    #>

    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute(
        'PSAvoidUsingInvokeExpression',
        '',
        Justification = 'raw git commands needed'
    )]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute(
        'PSUseShouldProcessForStateChangingFunctions',
        '',
        Justification = 'system state does not change permanent in temp build clients.'
    )]
    param (
        [Parameter(Mandatory = $true)]
        [ValidatePattern('^(v)?([0-9]+)\.([0-9]+)\.([0-9]+)(-([A-Za-z]+)([0-9]+))?$')]
        [String]$NewVersion,

        [Parameter(Mandatory = $false)]
        [Switch]$SkipCleanup
    )
    process {
        if (-not $SkipCleanup.IsPresent) {
            Invoke-Expression -Command 'git tag -d $(git tag -l)' | Write-Verbose
            Invoke-Expression -Command 'git fetch --tags --prune' | Write-Verbose
            $GitState = Get-GitStatus

            if ($GitState.Branch -eq 'master') {
                Write-Error -Message 'You can nor update the changelog within the master branch!' -ErrorAction Stop
            }
            if (
                ($GitState.BehindBy -ne 0) -or
                ($GitState.AheadBy -ne 0) -or
                ($GitState.HasUntracked -ne $false) -or
                ($GitState.HasWorking -ne $false)
            ) {
                Write-Error -Message 'Your branch is a mess! Cleanup and try it again.' -ErrorAction Stop
            }
        }

        $Repo = Get-RepoPath
        $Tags = Invoke-Expression -Command 'git tag'
        $NormalizedTags = $Tags | Where-Object { $_ -notmatch '-' } | ForEach-Object {
            [PSCustomObject]@{
                tag        = $_
                rawVersion = (
                    ($_ -split '-')[0] -replace 'v', ''
                )
            }
        }
        $LTag = $NormalizedTags | Sort-Object -Property 'rawVersion' | Select-Object -ExpandProperty 'tag' -Last 1
        Write-Debug -Message ('Last tag: {0}' -f $LTag)
        if ($null -eq $LTag) {
            Write-Error -Message 'No tags found!' -ErrorAction 'Stop'
        }
        $Expr = "git log {0}..HEAD --format='- (%h) %s'" -f $LTag
        $Res = Invoke-Expression -Command $Expr
        Write-Debug -Message ('New Changelog: {0}' -f $Res)
        if ($Repo.Changelog.Exists) {
            $Content = Get-Content -Path $Repo.Changelog.Item.FullName
            $Content[2] += "{0}## ``{2}``{0}{0}{1}" -f [Environment]::NewLine, ($Res | Out-String), $NewVersion
            $Content | Out-File -FilePath $Repo.Changelog.Item.FullName -Encoding utf8
            Set-EOL -Path $Repo.Changelog.Item.FullName
        }
        else {
            Write-Error -Message 'Changelog file does not exist!' -ErrorAction Stop
        }
    }
}