Whiskey.psm1


$startedAt = Get-Date
function Write-Timing
{
    param(
        [Parameter(Position=0)]
        $Message
    )

    $now = Get-Date
    Write-Debug -Message ('[{0}] [{1}] {2}' -f $now,($now - $startedAt),$Message)
}

$events = @{ }

$powerShellModulesDirectoryName = 'PSModules'

$whiskeyScriptRoot = $PSScriptRoot
$whiskeyModulesRoot = Join-Path -Path $whiskeyScriptRoot -ChildPath 'Modules' -Resolve
$whiskeyBinPath = Join-Path -Path $whiskeyScriptRoot -ChildPath 'bin' -Resolve
$whiskeyNuGetExePath = Join-Path -Path $whiskeyBinPath -ChildPath 'NuGet.exe' -Resolve

$buildStartedAt = [DateTime]::MinValue

$PSModuleAutoLoadingPreference = 'None'

$supportsWriteInformation = Get-Command -Name 'Write-Information' -ErrorAction Ignore

Write-Timing 'Updating serialiazation depths on Whiskey objects.'
# Make sure our custom objects get serialized/deserialized correctly, otherwise they don't get passed to PowerShell tasks correctly.
Update-TypeData -TypeName 'Whiskey.BuildContext' -SerializationDepth 50 -ErrorAction Ignore
Update-TypeData -TypeName 'Whiskey.BuildInfo' -SerializationDepth 50 -ErrorAction Ignore
Update-TypeData -TypeName 'Whiskey.BuildVersion' -SerializationDepth 50 -ErrorAction Ignore

Write-Timing 'Testing that correct Whiskey assembly is loaded.'
$attr = New-Object -TypeName 'Whiskey.TaskAttribute' -ArgumentList 'Whiskey' -ErrorAction Ignore
if( -not ($attr | Get-Member 'Platform') )
{
    Write-Error -Message ('You''ve got an old version of Whiskey loaded. Please open a new PowerShell session.') -ErrorAction Stop
}

function Assert-Member
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [object]
        $Object,

        [Parameter(Mandatory)]
        [string[]]
        $Property
    )

    foreach( $propertyToCheck in $Property )
    {
        if( -not ($Object | Get-Member $propertyToCheck) )
        {
            Write-Debug -Message ('Object "{0}" is missing member "{1}".' -f $Object.GetType().FullName,$propertyToCheck)
            Write-Error -Message ('You''ve got an old version of Whiskey loaded. Please open a new PowerShell session.') -ErrorAction Stop
        }
    }
}

$context = New-Object -TypeName 'Whiskey.Context'
Assert-Member -Object $context -Property @( 'TaskPaths', 'MSBuildConfiguration', 'ApiKeys' )

$taskAttribute = New-Object -TypeName 'Whiskey.TaskAttribute' -ArgumentList 'Fubar'
Assert-Member -Object $taskAttribute -Property @( 'Aliases', 'WarnWhenUsingAlias', 'Obsolete', 'ObsoleteMessage' )

[Type]$apiKeysType = $context.ApiKeys.GetType()
$apiKeysDictGenericTypes = $apiKeysType.GenericTypeArguments
if( -not $apiKeysDictGenericTypes -or $apiKeysDictGenericTypes.Count -ne 2 -or $apiKeysDictGenericTypes[1].FullName -ne [SecureString].FullName )
{
    Write-Error -Message ('You''ve got an old version of Whiskey loaded. Please open a new PowerShell session.') -ErrorAction Stop 
}

Write-Timing ('Creating internal module variables.')

# PowerShell 5.1 doesn't have these variables so create them if they don't exist.
if( -not (Get-Variable -Name 'IsLinux' -ErrorAction Ignore) )
{
    $IsLinux = $false
    $IsMacOS = $false
    $IsWindows = $true
}

$dotNetExeName = 'dotnet'
$nodeExeName = 'node'
$nodeDirName = 'bin'
if( $IsWindows )
{
    $dotNetExeName = '{0}.exe' -f $dotNetExeName
    $nodeExeName = '{0}.exe' -f $nodeExeName
    $nodeDirName = ''
}

$CurrentPlatform = [Whiskey.Platform]::Unknown
if( $IsLinux )
{
    $CurrentPlatform = [Whiskey.Platform]::Linux
}
elseif( $IsMacOS )
{
    $CurrentPlatform = [Whiskey.Platform]::MacOS
}
elseif( $IsWindows )
{
    $CurrentPlatform = [Whiskey.Platform]::Windows
}

Write-Timing -Message ('Dot-sourcing files.')
$count = 0
& {
        Join-Path -Path $PSScriptRoot -ChildPath 'Functions'
        Join-Path -Path $PSScriptRoot -ChildPath 'Tasks'
    } |
    Where-Object { Test-Path -Path $_ } |
    Get-ChildItem -Filter '*.ps1' |
    ForEach-Object { 
        $count += 1
        . $_.FullName 
    }
Write-Timing -Message ('Finished dot-sourcing {0} files.' -f $count)


function Add-WhiskeyApiKey
{
    <#
    .SYNOPSIS
    Adds an API key to Whiskey's API key store.
 
    .DESCRIPTION
    The `Add-WhiskeyApiKey` function adds an API key to Whiskey's API key store. Tasks that need API keys usually have a property where you provide the ID of the API key to use. You provide Whiskey the value of the API Key with this function.
 
    For example, if you are publishing a PowerShell module, your `whiskey.yml` file will look something like this:
 
        Publish:
        - PublishPowerShellModule:
            RepositoryName: PSGallery
            Path: Whiskey
            ApiKeyID: PSGalleryApiKey
 
    After you create your build's context with `New-WhiskeyContext`, you would then call `Add-WhiskeyApiKey` to add the actual API key:
 
        $context = New-WhiskeyContext
        Add-WhiskeyApiKey -Context $context -ID 'PSGalleryApiKey' -Value '901a072f-fe5e-44ec-8546-029ffbec0687'
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [object]
        # The context of the build that needs the API key.
        $Context,

        [Parameter(Mandatory=$true)]
        [string]
        # The ID of the API key. This should match the ID given in your `whiskey.yml` for the API key ID property of the task that needs it.
        $ID,

        [Parameter(Mandatory=$true)]
        # The value of the API key. Can be a string or a SecureString.
        $Value
    )

    Set-StrictMode -Version 'Latest'

    if( $Value -isnot [securestring] )
    {
        $Value = ConvertTo-SecureString -String $Value -AsPlainText -Force
    }

    $Context.ApiKeys[$ID] = $Value
}


function Add-WhiskeyCredential
{
    <#
    .SYNOPSIS
    Adds credential to Whiskey's credential store.
 
    .DESCRIPTION
    The `Add-WhiskeyCredential` function adds a credential to Whiskey's credential store. Tasks that need credentials usually have a property where you provide the ID of the credential. You provide Whiskey the value of that credential with this function.
 
    For example, if you are publishing a ProGet universal pakcage, your `whiskey.yml` file will look something like this:
 
        Publish:
        - PublishProGetUniversalPackage:
            Uri: https://proget.example.com
            FeedName: 'upack'
            CredentialID: ProgetExampleCom
 
    After you create your build's context with `New-WhiskeyContext`, you would then call `Add-WhiskeyCredential` to add the actual credential:
 
        $context = New-WhiskeyContext
        Add-WhiskeyCredential -Context $context -ID 'ProgetExampleCom' -Credential $credential
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [object]
        # The context of the build that needs the API key.
        $Context,

        [Parameter(Mandatory=$true)]
        [string]
        # The ID of the credential. This should match the ID given in your `whiskey.yml` of credential ID property of the task that needs it.
        $ID,

        [Parameter(Mandatory=$true)]
        [pscredential]
        # The value of the credential.
        $Credential
    )

    Set-StrictMode -Version 'Latest'

    $Context.Credentials[$ID] = $Credential
}


function Add-WhiskeyTaskDefault
{
    <#
    .SYNOPSIS
    Sets default values for task parameters.
 
    .DESCRIPTION
    The `Add-WhiskeyTaskDefault` functions sets default properties for tasks. These defaults are only used if the property is missing from the task in your `whiskey.yml` file, i.e. properties defined in your whiskey.yml file take precedence over task defaults.
 
    `TaskName` must be the name of an existing task. Otherwise, `Add-WhiskeyTaskDefault` will throw an terminating error.
 
    By default, existing defaults are left in place. To override any existing defaults, use the `-Force`... switch.
 
    .EXAMPLE
    Add-WhiskeyTaskDefault -Context $context -TaskName 'MSBuild' -PropertyName 'Version' -Value 12.0
 
    Demonstrates setting the default value of the `MSBuild` task's `Version` property to `12.0`.
 
    .EXAMPLE
    Add-WhiskeyTaskDefault -Context $context -TaskName 'MSBuild' -PropertyName 'Version' -Value 15.0 -Force
 
    Demonstrates overwriting the current default value for `MSBuild` task's `Version` property to `15.0`.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true,ValueFromPipeline=$true)]
        [object]
        # The current build context. Use `New-WhiskeyContext` to create context objects.
        $Context,

        [Parameter(Mandatory=$true)]
        [string]
        # The name of the task that a default parameter value will be set.
        $TaskName,

        [Parameter(Mandatory=$true)]
        [string]
        # The name of the task parameter to set a default value for.
        $PropertyName,

        [Parameter(Mandatory=$true)]
        # The default value for the task parameter.
        $Value,

        [switch]
        # Overwrite an existing task default with a new value.
        $Force
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    if (-not ($Context | Get-Member -Name 'TaskDefaults'))
    {
        throw 'The given ''Context'' object does not contain a ''TaskDefaults'' property. Create a proper Whiskey context object using the ''New-WhiskeyContext'' function.'
    }

    if ($TaskName -notin (Get-WhiskeyTask | Select-Object -ExpandProperty 'Name'))
    {
        throw 'Task ''{0}'' does not exist.' -f $TaskName
    }

    if ($context.TaskDefaults.ContainsKey($TaskName))
    {
        if ($context.TaskDefaults[$TaskName].ContainsKey($PropertyName) -and -not $Force)
        {
            throw 'The ''{0}'' task already contains a default value for the property ''{1}''. Use the ''Force'' parameter to overwrite the current value.' -f $TaskName,$PropertyName
        }
        else
        {
            $context.TaskDefaults[$TaskName][$PropertyName] = $Value
        }
    }
    else
    {
        $context.TaskDefaults[$TaskName] = @{ $PropertyName = $Value }
    }
}



function Add-WhiskeyVariable
{
    <#
    .SYNOPSIS
    Adds a variable to the current build.
 
    .DESCRIPTION
    The `Add-WhiskeyVariable` adds a variable to the current build. Variables can be used in task properties and at runtime are replaced with their values. Variables syntax is `$(VARIABLE_NAME)`. Variable names are case-insensitive.
 
    .EXAMPLE
    Add-WhiskeyVariable -Context $context -Name 'Timestamp' -Value (Get-Date).ToString('o')
 
    Demonstrates how to add a variable. In this example, Whiskey will replace any `$(Timestamp)` variables it finds in any task properties with the value returned by `(Get-Date).ToString('o')`.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [object]
        # The context of the build here you want to add a variable.
        $Context,

        [Parameter(Mandatory=$true)]
        [string]
        # The name of the variable.
        $Name,

        [Parameter(Mandatory=$true)]
        [AllowEmptyString()]
        [object]
        $Value
    )

    Set-StrictMode -Version 'Latest'

    $Context.Variables[$Name] = $Value

}



function Assert-WhiskeyNodeModulePath
{
    <#
    .SYNOPSIS
    Asserts that the path to a Node module directory exists.
 
    .DESCRIPTION
    The `Assert-WhiskeyNodeModulePath` function asserts that a Node module directory exists. If the directory doesn't exist, the function writes an error with details on how to solve the problem. It returns the path if it exists. Otherwise, it returns nothing.
 
    This won't fail a build. To fail a build if the path doesn't exist, pass `-ErrorAction Stop`.
 
    .EXAMPLE
    Assert-WhiskeyNodeModulePath -Path $TaskParameter['NspPath']
 
    Demonstrates how to check that a Node module directory exists.
 
    .EXAMPLE
    Assert-WhiskeyNodeModulePath -Path $TaskParameter['NspPath'] -ErrorAction Stop
 
    Demonstrates how to fail a build if a Node module directory doesn't exist.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [string]
        # The path to check.
        $Path,

        [string]
        # The path to a command inside the module path.
        $CommandPath
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    $moduleName = $Path | Split-Path
    
    if( -not (Test-Path -Path $Path -PathType Container) )
    {
        Write-Error -Message ('Node module ''{0}'' does not exist at ''{1}''. Whiskey or NPM maybe failed to install this module correctly. Clean your build then re-run your build normally. If the problem persists, it might be a task authoring error. Please see the `about_Whiskey_Writing_Tasks` help topic for more information.' -f $moduleName,$Path)        
        return
    }

    if( -not $CommandPath )
    {
        return $Path
    }

    $commandName = $CommandPath | Split-Path -Leaf

    $fullCommandPath = Join-Path -Path $Path -ChildPath $CommandPath
    if( -not (Test-Path -Path $fullCommandPath -PathType Leaf) )
    {
        Write-Error -Message ('Node module ''{0}'' does not contain command ''{1}'' at ''{2}''. Whiskey or NPM maybe failed to install this module correctly or that command doesn''t exist in this version of the module. Clean your build then re-run your build normally. If the problem persists, it might be a task authoring error. Please see the `about_Whiskey_Writing_Tasks` help topic for more information.' -f $moduleName,$commandName,$fullCommandPath)
        return
    }

    return $fullCommandPath
}


function Assert-WhiskeyNodePath
{
    <#
    .SYNOPSIS
    Asserts that the path to a node executable exists.
 
    .DESCRIPTION
    The `Assert-WhiskeyNodePath` function asserts that a path to a Node executable exists. If it doesn't, it writes an error with details on how to solve the problem. It returns the path if it exists. Otherwise, it returns nothing.
 
    This won't fail a build. To fail a build if the path doesn't exist, pass `-ErrorAction Stop`.
 
    .EXAMPLE
    Assert-WhiskeyNodePath -Path $TaskParameter['NodePath']
 
    Demonstrates how to check that Node exists.
 
    .EXAMPLE
    Assert-WhiskeyNodePath -Path $TaskParameter['NodePath'] -ErrorAction Stop
 
    Demonstrates how to fail a build if the path to Node doesn't exist.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [string]
        # The path to check.
        $Path
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
    
    if( -not (Test-Path -Path $Path -PathType Leaf) )
    {
        Write-Error -Message ('Node executable ''{0}'' does not exist. Whiskey maybe failed to install Node correctly. Clean your build then re-run your build normally. If the problem persists, it might be a task authoring error. Please see the `about_Whiskey_Writing_Tasks` help topic for more information.' -f $Path)        
        return
    }

    return $Path
}


function ConvertFrom-WhiskeyContext
{
    <#
    .SYNOPSIS
    Converts a `Whiskey.Context` into a generic object that can be serialized across platforms.
     
    .DESCRIPTION
    Some tasks need to run in background jobs and need access to Whiskey's context. This function converts a `Whiskey.Context` object into an object that can be serialized by PowerShell across platforms. The object returned by this function can be passed to a `Start-Job` script block. Inside that script block you should import Whiskey and pass the serialized context to `ConvertTo-WhiskeyContext`.
 
        $serializableContext = $TaskContext | ConvertFrom-WhiskeyContext
        $job = Start-Job {
                    Invoke-Command -ScriptBlock {
                                            $VerbosePreference = 'SilentlyContinue';
                                            # Or wherever your project keeps Whiskey relative to your task definition.
                                            Import-Module -Name (Join-Path -Path $using:PSScriptRoot -ChildPath '..\Whiskey' -Resolve -ErrorAction Stop)
                                        }
                    $context = $using:serializableContext | ConvertTo-WhiskeyContext
                    # Run your task
              }
 
    You should create a new serializable context for each job you are running. Whiskey generates a temporary encryption key so it can encrypt/decrypt credentials. Once it decrypts the credentials, it deletes the key from memory. If you use the same context object between jobs, one job will clear the key and other jobs will fail because the key will be gone.
 
    .EXAMPLE
    $TaskContext | ConvertFrom-WhiskeyContext
 
    Demonstrates how to call `ConvertFrom-WhiskeyContext`. See the description for a full example.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory,ValueFromPipeline)]
        [Whiskey.Context]
        # The context to convert. You can pass an existing context via the pipeline.
        $Context
    )

    begin 
    {
        Set-StrictMode -Version 'Latest'
        Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

        $key = New-Object 'byte[]' (256/8)
        $rng = New-Object 'Security.Cryptography.RNGCryptoServiceProvider'
        $rng.GetBytes($key)
    }

    process 
    {
        # PowerShell on Linux/MacOS can't serialize SecureStrings. So, we have to encrypt and serialize them.
        $serializableCredentials = @{ }
        foreach( $credentialID in $Context.Credentials.Keys )
        {
            [pscredential]$credential = $Context.Credentials[$credentialID]
            $serializableCredential = [pscustomobject]@{ 
                                                            UserName = $credential.UserName;
                                                            Password = ConvertFrom-SecureString -SecureString $credential.Password -Key $key
                                                        }
            $serializableCredentials[$credentialID] = $serializableCredential
        }

        $serializableApiKeys = @{ }
        foreach( $apiKeyID in $Context.ApiKeys.Keys )
        {
            [securestring]$apiKey = $Context.ApiKeys[$apiKeyID]
            $serializableApiKey = ConvertFrom-SecureString -SecureString $apiKey -Key $key
            $serializableApiKeys[$apiKeyID] = $serializableApiKey
        }

        $Context | 
            Select-Object -Property '*' -ExcludeProperty 'Credentials','ApiKeys' | 
            Add-Member -MemberType NoteProperty -Name 'Credentials' -Value $serializableCredentials -PassThru |
            Add-Member -MemberType NoteProperty -Name 'ApiKeys' -Value $serializableApiKeys -PassThru |
            Add-Member -MemberType NoteProperty -Name 'CredentialKey' -Value $key.Clone() -PassThru
    }

    end
    {
        [Array]::Clear($key,0,$key.Length)
    }
}


function ConvertFrom-WhiskeyYamlScalar
{
    <#
    .SYNOPSIS
    Converts a string that came from a YAML configuation file into a strongly-typed object.
 
    .DESCRIPTION
    The `ConvertFrom-WhiskeyYamlScalar` function converts a string that came from a YAML configuration file into a strongly-typed object according to the parsing rules in the YAML specification. It converts strings into booleans, integers, floating-point numbers, and timestamps. See the YAML specification for examples on how to represent these values in your YAML file.
     
    It will convert:
 
    * `y`, `yes`, `true`, and `on` to `$true`
    * `n`, `no`, `false`, and `off` to `$false`
    * Numbers to `int32` or `int64` types. Numbers can be prefixed with `0x` (for hex), `0b` (for bits), or `0` for octal.
    * Floating point numbers to `double`, or `single` types. Floating point numbers can be expressed as decimals (`1.5`), or with scientific notation (`6.8523015e+5`).
    * `~`, `null`, and `` to `$null`
    * timestamps (e.g. `2001-12-14t21:59:43.10-05:00`) to date
 
    If it can't convert a string into a known type, `ConvertFrom-WhiskeyYamlScalar` writes an error.
 
    .EXAMPLE
    $value | ConvertFrom-WhiskeyYamlScalar
 
    Demonstrates how to pipe values to `ConvertFrom-WhiskeyYamlScalar`.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true,ValueFromPipeline=$true)]
        [AllowEmptyString()]
        [AllowNull()]
        [string]
        # The object to convert.
        $InputObject
    )

    process
    {
        Set-StrictMode -Version 'Latest'
        Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

        if( [string]::IsNullOrEmpty($InputObject)  -or $InputObject -match '^(~|null)' )
        {
            return $null
        }

        if( $InputObject -match '^(y|yes|n|no|true|false|on|off)$' )
        {
            return $InputObject -match '^(y|yes|true|on)$'
        }

        # Integer
        $regex = @'
^(
 [-+]?0b[0-1_]+ # (base 2)
|[-+]?0[0-7_]+ # (base 8)
|[-+]?(0|[1-9][0-9_]*) # (base 10)
|[-+]?0x[0-9a-fA-F_]+ # (base 16)
|[-+]?[1-9][0-9_]*(:[0-5]?[0-9])+ # (base 60)
)$
'@

        if( [Text.RegularExpressions.Regex]::IsMatch($InputObject, $regex, [Text.RegularExpressions.RegexOptions]::IgnorePatternWhitespace) ) 
        {
            [int64]$int64 = 0

            $value = $InputObject -replace '_',''

            if( $value -match '^0x' -and [int64]::TryParse(($value -replace '0x',''), [Globalization.NumberStyles]::HexNumber, $null,[ref]$int64))
            {
            }
            elseif( $value -match '^0b' )
            {
                $int64 = [Convert]::ToInt64(($value -replace ('^0b','')),2)
            }
            elseif( $value -match '^0' )
            {
                $int64 = [Convert]::ToInt64($value,8)
            }
            elseif( [int64]::TryParse($value,[ref]$int64) )
            {
            }
            
            if( $int64 -gt [Int32]::MaxValue )
            {
                return $int64
            }

            return [int32]$int64
        }    

        $regex = @'
^(
 [-+]?([0-9][0-9_]*)?\.[0-9_]*([eE][-+][0-9]+)? # (base 10)
|[-+]?[0-9][0-9_]*(:[0-5]?[0-9])+\.[0-9_]* # (base 60)
|[-+]?\.(inf|Inf|INF) # (infinity)
|\.(nan|NaN|NAN) # (not a number)
)$
'@

        if( [Text.RegularExpressions.Regex]::IsMatch($InputObject, $regex, [Text.RegularExpressions.RegexOptions]::IgnorePatternWhitespace) ) 
        {
            $value = $InputObject -replace '_',''
            [double]$double = 0.0
            if( $value -eq '.NaN' )
            {
                return [double]::NaN
            }

            if( $value -match '-\.inf' )
            {
                return [double]::NegativeInfinity
            }

            if( $value -match '\+?.inf' )
            {
                return [double]::PositiveInfinity
            }

            if( [double]::TryParse($value,[ref]$double) )
            {
                return $double
            }
        }

        $regex = '^\d\d\d\d-\d\d?-\d\d?(([Tt]|[ \t]+)\d\d?\:\d\d\:\d\d(\.\d+)?(Z|\ *[-+]\d\d?(:\d\d)?)?)?$'
        if( [Text.RegularExpressions.Regex]::IsMatch($InputObject, $regex, [Text.RegularExpressions.RegexOptions]::IgnorePatternWhitespace) ) 
        {
            [DateTime]$datetime = [DateTime]::MinValue
            if( ([DateTime]::TryParse(($InputObject -replace 'T',' '),[ref]$datetime)) ) 
            {
                return $datetime
            }
        }

        Write-Error -Message ('Unable to convert scalar value ''{0}''. See http://yaml.org/type/ for documentation on YAML''s scalars.' -f $InputObject) -ErrorAction $ErrorActionPreference
    }

}


function ConvertTo-WhiskeyContext
{
    <#
    .SYNOPSIS
    Converts an `Whiskey.Context` returned by `ConvertFrom-WhiskeyContext` back into a `Whiskey.Context` object.
     
    .DESCRIPTION
    Some tasks need to run in background jobs and need access to Whiskey's context. This function converts an object returned by `ConvertFrom-WhiskeyContext` back into a `Whiskey.Context` object.
 
        $serializableContext = $TaskContext | ConvertFrom-WhiskeyContext
        $job = Start-Job {
                    Invoke-Command -ScriptBlock {
                                            $VerbosePreference = 'SilentlyContinue';
                                            # Or wherever your project keeps Whiskey relative to your task definition.
                                            Import-Module -Name (Join-Path -Path $using:PSScriptRoot -ChildPath '..\Whiskey' -Resolve -ErrorAction Stop)
                                        }
                    [Whiskey.Context]$context = $using:serializableContext | ConvertTo-WhiskeyContext
                    # Run your task
              }
 
    .EXAMPLE
    $serializedContext | ConvertTo-WhiskeyContext
 
    Demonstrates how to call `ConvertTo-WhiskeyContext`. See the description for a full example.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory,ValueFromPipeline)]
        [object]
        # The context to convert. You can pass an existing context via the pipeline.
        $InputObject
    )

    process 
    {
        Set-StrictMode -Version 'Latest'
        Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

        try
        {
            function Sync-ObjectProperty
            {
                param(
                    [Parameter(Mandatory=$true)]
                    [object]
                    $Source,

                    [Parameter(Mandatory=$true)]
                    [object]
                    $Destination,

                    [string[]]
                    $ExcludeProperty
                )

                $destinationType = $Destination.GetType()
                $destinationType.DeclaredProperties |
                    Where-Object { $ExcludeProperty -notcontains $_.Name } |
                    Where-Object { $_.GetSetMethod($false) } |
                    Select-Object -ExpandProperty 'Name' |
                    ForEach-Object { 
                        Write-Debug ('{0} {1} -> {2}' -f $_,$Destination.$_,$Source.$_)
                                
                        $propertyType = $destinationType.GetProperty($_).PropertyType
                        if( $propertyType.IsSubclassOf([IO.FileSystemInfo]) )
                        {
                            if( $Source.$_ )
                            {
                                Write-Debug -Message ('{0} {1} = {2}' -f $propertyType.FullName,$_,$Source.$_)
                                $Destination.$_ = New-Object $propertyType.FullName -ArgumentList $Source.$_.FullName
                            }
                        }
                        else
                        {
                            $Destination.$_ = $Source.$_ 
                        }
                    }

                Write-Debug ('Source -eq $null ? {0}' -f ($Source -eq $null))
                if( $Source -ne $null )
                {
                    Write-Debug -Message 'Source'
                    Get-Member -InputObject $Source | Out-String | Write-Debug
                }

                Write-Debug ('Destination -eq $null ? {0}' -f ($Destination -eq $null))
                if( $Destination -ne $null )
                {
                    Write-Debug -Message 'Destination'
                    Get-Member -InputObject $Destination | Out-String | Write-Debug
                }

                Get-Member -InputObject $Destination -MemberType Property |
                    Where-Object { $ExcludeProperty -notcontains $_.Name } |
                    Where-Object {
                        $name = $_.Name
                        if( -not $name )
                        {
                            return
                        }

                        $value = $Destination.$name
                        if( $value -eq $null )
                        {
                            return
                        }

                        Write-Debug ('Destination.{0,-20} -eq $null ? {1}' -f $name,($value -eq $null))
                        Write-Debug (' .{0,-20} is {1}' -f $name,$value.GetType())
                        return (Get-Member -InputObject $value -Name 'Keys') -or ($value -is [Collections.IList])
                    } |
                    ForEach-Object {
                        $propertyName = $_.Name
                        Write-Debug -Message ('{0}.{1} -> {2}.{1}' -f $Source.GetType(),$propertyName,$Destination.GetType())
                        $destinationObject = $Destination.$propertyName
                        $sourceObject = $source.$propertyName
                        if( (Get-Member -InputObject $destinationObject -Name 'Keys') )
                        {
                            $keys = $sourceObject.Keys
                            foreach( $key in $keys )
                            {
                                $value = $sourceObject[$key]
                                Write-Debug (' [{0,-20}] -> {1}' -f $key,$value)
                                $destinationObject[$key] = $sourceObject[$key]
                            }
                        }
                        elseif( $destinationObject -is [Collections.IList] )
                        {
                            $idx = 0
                            foreach( $item in $sourceObject )
                            {
                                Write-Debug(' [{0}] {1}' -f $idx++,$item)
                                $destinationObject.Add($item)
                            }
                        }
                    }
            }

            $buildInfo = New-WhiskeyBuildMetadataObject
            Sync-ObjectProperty -Source $InputObject.BuildMetadata -Destination $buildInfo -Exclude @( 'BuildServer' )
            if( $InputObject.BuildMetadata.BuildServer )
            {
                $buildInfo.BuildServer = $InputObject.BuildMetadata.BuildServer
            }

            $buildVersion = New-WhiskeyVersionObject
            Sync-ObjectProperty -Source $InputObject.Version -Destination $buildVersion -ExcludeProperty @( 'SemVer1', 'SemVer2', 'SemVer2NoBuildMetadata' )
            if( $InputObject.Version )
            {
                if( $InputObject.Version.SemVer1 )
                {
                    $buildVersion.SemVer1 = $InputObject.Version.SemVer1.ToString()
                }

                if( $InputObject.Version.SemVer2 )
                {
                    $buildVersion.SemVer2 = $InputObject.Version.SemVer2.ToString()
                }

                if( $InputObject.Version.SemVer2NoBuildMetadata )
                {
                    $buildVersion.SemVer2NoBuildMetadata = $InputObject.Version.SemVer2NoBuildMetadata.ToString()
                }
            }

            [Whiskey.Context]$context = New-WhiskeyContextObject
            Sync-ObjectProperty -Source $InputObject -Destination $context -ExcludeProperty @( 'BuildMetadata', 'Configuration', 'Version', 'Credentials', 'TaskPaths', 'ApiKeys' )
            if( $context.ConfigurationPath )
            {
                $context.Configuration = Import-WhiskeyYaml -Path $context.ConfigurationPath
            }

            $context.BuildMetadata = $buildInfo
            $context.Version = $buildVersion

            foreach( $credentialID in $InputObject.Credentials.Keys )
            {
                $serializedCredential = $InputObject.Credentials[$credentialID]
                $username = $serializedCredential.UserName
                $password = ConvertTo-SecureString -String $serializedCredential.Password -Key $InputObject.CredentialKey
                [pscredential]$credential = New-Object 'pscredential' $username,$password
                Add-WhiskeyCredential -Context $context -ID $credentialID -Credential $credential
            }

            foreach( $apiKeyID in $InputObject.ApiKeys.Keys )
            {
                $serializedApiKey = $InputObject.ApiKeys[$apiKeyID]
                $apiKey = ConvertTo-SecureString -String $serializedApiKey -Key $InputObject.CredentialKey
                Add-WhiskeyApiKey -Context $context -ID $apiKeyID -Value $apiKey
            }

            foreach( $path in $InputObject.TaskPaths )
            {
                $context.TaskPaths.Add((New-Object -TypeName 'IO.FileInfo' -ArgumentList $path))
            }

            Write-Debug 'Variables'
            $context.Variables | ConvertTo-Json -Depth 50 | Write-Debug
            Write-Debug 'ApiKeys'
            $context.ApiKeys | ConvertTo-Json -Depth 50 | Write-Debug
            Write-Debug 'Credentials'
            $context.Credentials | ConvertTo-Json -Depth 50 | Write-Debug
            Write-Debug 'TaskDefaults'
            $context.TaskDefaults | ConvertTo-Json -Depth 50 | Write-Debug
            Write-Debug 'TaskPaths'
            $context.TaskPaths | ConvertTo-Json | Write-Debug

            return $context
        }
        finally
        {
            # Don't leave the decryption key lying around.
            [Array]::Clear($InputObject.CredentialKey,0,$InputObject.CredentialKey.Length)
        }
    }
}


function ConvertTo-WhiskeySemanticVersion
{
    <#
    .SYNOPSIS
    Converts an object to a semantic version.
 
    .DESCRIPTION
    The `ConvertTo-WhiskeySemanticVersion` function converts strings, numbers, and date/time objects to semantic versions. If the conversion fails, it writes an error and you get nothing back.
 
    .EXAMPLE
    '1.2.3' | ConvertTo-WhiskeySemanticVersion
 
    Demonstrates how to convert an object into a semantic version.
    #>

    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline=$true)]
        [object]
        # The object to convert to a semantic version. Can be a version string, number, or date/time.
        $InputObject
    )

    process
    {
        Set-StrictMode -Version 'Latest'
        Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

        [version]$asVersion = $null
        if( $InputObject -is [string] )
        {
            [int]$asInt = 0
            [double]$asDouble = 0.0
            [SemVersion.SemanticVersion]$semVersion = $null
            if( [SemVersion.SemanticVersion]::TryParse($InputObject,[ref]$semVersion) )
            {
                return $semVersion
            }

            if( [version]::TryParse($InputObject,[ref]$asVersion) )
            {
                $InputObject = $asVersion
            }
            elseif( [int]::TryParse($InputObject,[ref]$asInt) )
            {
                $InputObject = $asInt
            }
        }
        
        if( $InputObject -is [SemVersion.SemanticVersion] )
        {
            return $InputObject
        }
        elseif( $InputObject -is [datetime] )
        {
            $InputObject = '{0}.{1}.{2}' -f $InputObject.Month,$InputObject.Day,$InputObject.Year
        }
        elseif( $InputObject -is [double] )
        {
            $major,$minor = $InputObject.ToString('g') -split '\.'
            if( -not $minor )
            {
                $minor = '0'
            }
            $InputObject = '{0}.{1}.0' -f $major,$minor
        }
        elseif( $InputObject -is [int] )
        {
            $InputObject = '{0}.0.0' -f $InputObject
        }
        elseif( $InputObject -is [version] )
        {
            if( $InputObject.Build -le -1 )
            {
                $InputObject = '{0}.0' -f $InputObject
            }
            else
            {
                $InputObject = $InputObject.ToString()
            }
        }
        else
        {
            Write-Error -Message ('Unable to convert ''{0}'' to a semantic version. We tried parsing it as a version, date, double, and integer. Sorry. But we''re giving up.' -f $PSBoundParameters['InputObject']) -ErrorAction $ErrorActionPreference
            return
        }

        $semVersion = $null
        if( ([SemVersion.SemanticVersion]::TryParse($InputObject,[ref]$semVersion)) )
        {
            return $semVersion
        }

        Write-Error -Message ('Unable to convert ''{0}'' of type ''{1}'' to a semantic version.' -f $PSBoundParameters['InputObject'],$PSBoundParameters['InputObject'].GetType().FullName) -ErrorAction $ErrorActionPreference
    }
}




function ConvertTo-WhiskeyTask
{
    <#
    .SYNOPSIS
    Converts an object parsed from a whiskey.yml file into a task name and task parameters.
 
    .DESCRIPTION
    The `ConvertTo-WhiskeyTask` function takes an object parsed from a whiskey.yml file and converts it to a task name and hashtable of parameters and returns both in that order.
 
    .EXAMPLE
    $name,$parameter = ConvertTo-WhiskeyTask -InputObject $parsedTask
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [AllowNull()]
        [object]
        $InputObject
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
    
    if( $InputObject -is [string] )
    {
        $InputObject
        @{ }
        return
    }
    elseif( $InputObject -and ($InputObject | Get-Member -Name 'Keys') )
    {
        $taskName = $InputObject.Keys | Select-Object -First 1
        $parameter = $InputObject[$taskName]
        if( -not $parameter )
        {
            $parameter = @{ }
        }
        elseif( -not ($parameter | Get-Member -Name 'Keys') )
        {
            $parameter = @{ '' = $parameter }
        }
        $taskName
        $parameter
        return
    }

    # Convert back to YAML to display its invalidness to the user.
    $builder = New-Object 'YamlDotNet.Serialization.SerializerBuilder'
    $yamlWriter = New-Object "System.IO.StringWriter"
    $serializer = $builder.Build()
    $serializer.Serialize($yamlWriter, $InputObject)
    $yaml = $yamlWriter.ToString()
    $yaml = $yaml -split [regex]::Escape([Environment]::NewLine) |
                Where-Object { @( '...', '---' ) -notcontains $_ } |
                ForEach-Object { ' {0}' -f $_ }
    Write-Error -Message ('Invalid task YAML:{0} {0}{1}{0}A task must have a name followed by optional parameters, e.g.
  
    Build:
    - Task1
    - Task2:
        Parameter1: Value1
        Parameter2: Value2
  
    '
 -f [Environment]::NewLine,($yaml -join [Environment]::NewLine))
}


function Format-WhiskeyMessage
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [Whiskey.Context]$Context,

        [Parameter(Mandatory=$true,ValueFromPipeline=$true)]
        [AllowEmptyString()]
        [AllowNull()]
        [string]$Message,

        [int]$Indent = 0
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    ('[{0}][{1}][{2}] {3}{4}' -f $Context.PipelineName,$Context.TaskIndex,$Context.TaskName,(' ' * ($Indent * 2)),$Message)
}



function Get-MSBuild
{
    [CmdletBinding()]
    param(
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    function Resolve-MSBuildToolsPath
    {
        param(
            [Microsoft.Win32.RegistryKey]
            $Key
        )

        $toolsPath = Get-ItemProperty -Path $Key.PSPath -Name 'MSBuildToolsPath' -ErrorAction Ignore | Select-Object -ExpandProperty 'MSBuildToolsPath' -ErrorAction Ignore
        if( -not $toolsPath )
        {
            return ''
        }

        $path = Join-Path -Path $toolsPath -ChildPath 'MSBuild.exe'
        if( (Test-Path -Path $path -PathType Leaf) )
        {
            return $path
        }

        return ''
    }

    filter Test-Version
    {
        param(
            [Parameter(Mandatory=$true,ValueFromPipeline=$true)]
            $InputObject
        )

        [version]$version = $null
        [version]::TryParse($InputObject,[ref]$version)

    }

    $toolsVersionRegPath = 'hklm:\software\Microsoft\MSBuild\ToolsVersions'
    $toolsVersionRegPath32 = 'hklm:\software\Wow6432Node\Microsoft\MSBuild\ToolsVersions'
    $tools32Exists = Test-Path -Path $toolsVersionRegPath32 -PathType Container

    foreach( $key in (Get-ChildItem -Path $toolsVersionRegPath) )
    {
        $name = $key.Name | Split-Path -Leaf
        if( -not ($name | Test-Version) )
        {
            continue
        }

        $msbuildPath = Resolve-MSBuildToolsPath -Key $key
        if( -not $msbuildPath )
        {
            continue
        }

        $msbuildPath32 = $msbuildPath
        if( $tools32Exists )
        {
            $key32 = Get-ChildItem -Path $toolsVersionRegPath32 | Where-Object { ($_.Name | Split-Path -Leaf) -eq $name }
            if( $key32 )
            {
                $msbuildPath32 = Resolve-MSBuildToolsPath -Key $key32
            }
            else
            {
                $msbuildPath32 = ''
            }
        }

        [pscustomobject]@{
            Name = $name;
            Version = [version]$name;
            Path = $msbuildPath;
            Path32 = $msbuildPath32;
        }
    }

    foreach( $instance in (Get-VSSetupInstance) )
    {
        $msbuildRoot = Join-Path -Path $instance.InstallationPath -ChildPath 'MSBuild'
        if( -not (Test-Path -Path $msbuildRoot -PathType Container) )
        {
            Write-Verbose -Message ('Skipping {0} {1}: its MSBuild directory ''{2}'' doesn''t exist.' -f $instance.DisplayName,$instance.InstallationVersion,$msbuildRoot)
            continue
        }

        $versionRoots = Get-ChildItem -Path $msbuildRoot -Directory | 
                            Where-Object { Test-Version $_.Name }

        foreach( $versionRoot in $versionRoots )
        {
            $path = Join-Path -Path $versionRoot.FullName -ChildPath 'Bin\amd64\MSBuild.exe'
            $path32 = Join-Path -Path $versionRoot.FullName -ChildPath 'Bin\MSBuild.exe'
            if( -not (Test-Path -Path $path -PathType Leaf) )
            {
                $path = $path32
            }

            if( -not (Test-Path -Path $path -PathType Leaf) )
            {
                continue
            }

            if( -not (Test-Path -Path $path32 -PathType Leaf) )
            {
                $path32 = ''
            }

            [pscustomobject]@{
                                Name =  $versionRoot.Name;
                                Version = [version]$versionRoot.Name;
                                Path = $path;
                                Path32 = $path32;
                            }
        }
    }
}



function Get-TaskParameter
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]
        # The name of the command.
        $Name,

        [Parameter(Mandatory)]
        [hashtable]
        # The properties from the tasks's YAML.
        $TaskProperty,

        [Parameter(Mandatory)]
        [Whiskey.Context]
        # The current context.
        $Context
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
    
    # Parameters of the actual command.
    $cmdParameters = Get-Command -Name $task.CommandName | Select-Object -ExpandProperty 'Parameters'

    # Parameters to pass to the command.
    $taskParameters = @{ }

    [Management.Automation.ParameterMetadata]$cmdParameter = $null

    foreach( $cmdParameter in $cmdParameters.Values )
    {
        $propertyName = $cmdParameter.Name

        $value = $TaskProperty[$propertyName]

        # PowerShell can't implicitly convert strings to bool/switch values so we have to do it.
        if( $cmdParameter.ParameterType -eq [Switch] -or $cmdParameter.ParameterType -eq [bool] )
        {
            $value = $value | ConvertFrom-WhiskeyYamlScalar
        }

        [Whiskey.Tasks.ParameterValueFromVariableAttribute]$valueFromVariableAttr = $cmdParameter.Attributes | Where-Object { $_ -is [Whiskey.Tasks.ParameterValueFromVariableAttribute] }
        if( $valueFromVariableAttr )
        {
            $value = Resolve-WhiskeyVariable -InputObject ('$({0})' -f $valueFromVariableAttr.VariableName) -Context $Context
        }

        [Whiskey.Tasks.ValidatePathAttribute]$validateAsPathAttr = $cmdParameter.Attributes | Where-Object { $_ -is [Whiskey.Tasks.ValidatePathAttribute] }
        if( $validateAsPathAttr )
        {
            $optionalParams = @{ }
            if( $validateAsPathAttr.PathType )
            {
                $optionalParams['PathType'] = $validateAsPathAttr.PathType
            }

            if( $value )
            {
                $value = $value | Resolve-WhiskeyTaskPath -TaskContext $Context -PropertyName $propertyName @optionalParams
            }
            if( -not $value -and $validateAsPathAttr.Mandatory )
            {
                $errorMsg = 'path "{0}" does not exist.' -f $TaskProperty[$propertyName]
                if( -not $TaskProperty[$propertyName] )
                {
                    $errorMsg = 'is mandatory.'
                }
                Stop-WhiskeyTask -TaskContext $Context -PropertyName $cmdParameter.Name -Message $errorMsg
            }

            $expectedPathType = $validateAsPathAttr.PathType 
            if( $value -and $expectedPathType )
            {
                $pathType = 'Leaf'
                if( $expectedPathType -eq 'Directory' )
                {
                    $pathType = 'Container'
                }
                $invalidPaths = 
                    $value | 
                    Where-Object { -not (Test-Path -Path $_ -PathType $pathType) }
                if( $invalidPaths )
                {
                    Stop-WhiskeyTask -TaskContext $Context -PropertyName $cmdParameter.Name -Message (@'
must be a {0}, but found {1} path(s) that are not:
  
* {2}
  
'@
 -f $expectedPathType.ToLowerInvariant(),($invalidPaths | Measure-Object).Count,($invalidPaths -join ('{0}* ' -f [Environment]::NewLine)))
                }
            }

            $pathCount = $value | Measure-Object | Select-Object -ExpandProperty 'Count'
            if( $cmdParameter.ParameterType -ne ([string[]]) -and $pathCount -gt 1 )
            {
                Stop-WhiskeyTask -TaskContext $Context -PropertyName $cmdParameter.Name -Message (@'
The value "{1}" resolved to {2} paths [1] but this task requires a single path. Please change "{1}" to a value that resolves to a single item.
  
If you are this task''s author, and you want this property to accept multiple paths, please update the "{3}" command''s "{0}" property so it''s type is "[string[]]".
  
[1] The {1} path resolved to:
  
* {4}
  
'@
 -f $cmdParameter.Name,$TaskProperty[$propertyName],$pathCount,$task.CommandName,($value -join ('{0}* ' -f [Environment]::NewLine)))
            }
        }

        # If the user didn't provide a value and we couldn't find one, don't pass anything.
        if( -not $TaskProperty.ContainsKey($propertyName) -and -not $value )
        {
            continue
        }

        $taskParameters[$propertyName] = $value
        $TaskProperty.Remove($propertyName)
    }

    foreach( $name in @( 'TaskContext', 'Context' ) )
    {
        if( $cmdParameters.ContainsKey($name) )
        {
            $taskParameters[$name] = $Context
        }
    }

    foreach( $name in @( 'TaskParameter', 'Parameter' ) )
    {
        if( $cmdParameters.ContainsKey($name) )
        {
            $taskParameters[$name] = $TaskProperty
        }
    }

    return $taskParameters
}



function Get-WhiskeyApiKey
{
    <#
    .SYNOPSIS
    Gets an API key from the Whiskey API key store.
 
    .DESCRIPTION
    The `Get-WhiskeyApiKey` function returns an API key from Whiskey's API key store. If the API key doesn't exist, the current build stops (i.e. a terminating exception is thrown).
 
    Credentials are identified by an ID that you create. Credentials are added using `Add-WhiskeyCredential`. Credentials are used by tasks. You specify the credential's ID in tasks section of the `whiskey.yml` file. See the documentation for each task for more details.
    API keys are identified by an ID that you create. API keys are added using `Add-WhiskeyApiKey`. API keys are used by tasks. You specify the API keys' ID in the task's section of the `whiskey.yml` file. See the documentation for each task for more details.
 
    .EXAMPLE
    Get-WhiskeyApiKey -Context $context -ID 'nuget.org' -PropertyName 'ApiKeyID'
 
    Demonstrates how to get an API key. IN this case, retrieves the API key that was added with the ID `nuget.org`.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [object]
        # The current build context. Use `New-WhiskeyContext` to create context objects.
        $Context,

        [Parameter(Mandatory=$true)]
        [string]
        # The ID of the API key. You make this up.
        $ID,

        [Parameter(Mandatory=$true)]
        [string]
        # The property name in the task that needs this API key. Used in error messages to help users pinpoint what task and property might be misconfigured.
        $PropertyName
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    if( -not $Context.ApiKeys.ContainsKey($ID) )
    {
        Stop-WhiskeyTask -TaskContext $Context `
                         -Message ('API key ''{0}'' does not exist in Whiskey''s API key store. Use the `Add-WhiskeyApiKey` function to add this API key, e.g. `Add-WhiskeyApiKey -Context $context -ID ''{0}'' -Value $apikey`.' -f $ID) `
                         -PropertyName $PropertyName
        return
    }

    $secureString = $Context.ApiKeys[$ID]
    $stringPtr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecureString)
    return [Runtime.InteropServices.Marshal]::PtrToStringAuto($stringPtr)
}


function Get-WhiskeyBuildMetadata
{
    <#
    SYNOPSIS
    Gets metadata about the current build.
 
    .DESCRIPTION
    The `Get-WhiskeyBuildMetadata` function gets information about the current build. It is exists to hide what CI server the current build is running under. It returns an object with the following properties:
 
    * `ScmUri`: the URI to the source control repository used in this build.
    * `BuildNumber`: the build number of the current build. This is the incrementing number most CI servers used to identify a build of a specific job.
    * `BuildID`: this unique identifier for this build. Usually, this is used by CI servers to distinguish this build from builds across all jobs.
    * `ScmCommitID`: the full ID of the commit that is being built.
    * `ScmBranch`: the branch name of the commit that is being built.
    * `JobName`: the name of the job that is running the build.
    * `BuildUri`: the URI to this build's results.
 
    #>

    [CmdletBinding()]
    param(
    )

    Set-StrictMode -Version 'Latest'

    function Get-EnvironmentVariable
    {
        param(
            $Name
        )

        Get-Item -Path ('env:{0}' -f $Name) | Select-Object -ExpandProperty 'Value'
    }

    $buildInfo = New-WhiskeyBuildMetadataObject

    if( (Test-Path -Path 'env:JENKINS_URL') )
    {
        $buildInfo.BuildNumber = Get-EnvironmentVariable 'BUILD_NUMBER'
        $buildInfo.BuildID = Get-EnvironmentVariable 'BUILD_TAG'
        $buildInfo.BuildUri = Get-EnvironmentVariable 'BUILD_URL'
        $buildInfo.JobName = Get-EnvironmentVariable 'JOB_NAME'
        $buildInfo.JobUri = Get-EnvironmentVariable 'JOB_URL'
        $buildInfo.ScmUri = Get-EnvironmentVariable 'GIT_URL'
        $buildInfo.ScmCommitID = Get-EnvironmentVariable 'GIT_COMMIT'
        $buildInfo.ScmBranch = Get-EnvironmentVariable 'GIT_BRANCH'
        $buildInfo.ScmBranch = $buildInfo.ScmBranch -replace '^origin/',''
        $buildInfo.BuildServer = [Whiskey.BuildServer]::Jenkins
    }
    elseif( (Test-Path -Path 'env:APPVEYOR') )
    {
        $buildInfo.BuildNumber = Get-EnvironmentVariable 'APPVEYOR_BUILD_NUMBER'
        $buildInfo.BuildID = Get-EnvironmentVariable 'APPVEYOR_BUILD_ID'
        $accountName = Get-EnvironmentVariable 'APPVEYOR_ACCOUNT_NAME'
        $projectSlug = Get-EnvironmentVariable 'APPVEYOR_PROJECT_SLUG'
        $projectUri = 'https://ci.appveyor.com/project/{0}/{1}' -f $accountName,$projectSlug
        $buildVersion = Get-EnvironmentVariable 'APPVEYOR_BUILD_VERSION'
        $buildUri = '{0}/build/{1}' -f $projectUri,$buildVersion
        $buildInfo.BuildUri = $buildUri
        $buildInfo.JobName = Get-EnvironmentVariable 'APPVEYOR_PROJECT_NAME'
        $buildInfo.JobUri = $projectUri
        $baseUri = ''
        switch( (Get-EnvironmentVariable 'APPVEYOR_REPO_PROVIDER') )
        {
            'gitHub'
            {
                $baseUri = 'https://github.com'
            }
            default
            {
                Write-Error -Message ('Unsupported AppVeyor source control provider ''{0}''. If you''d like us to add support for this provider, please submit a new issue at https://github.com/webmd-health-services/Whiskey/issues. Copy/paste your environment variables from this build''s output into your issue.' -f $_)
            }
        }
        $repoName = Get-EnvironmentVariable 'APPVEYOR_REPO_NAME'
        $buildInfo.ScmUri = '{0}/{1}.git' -f $baseUri,$repoName
        $buildInfo.ScmCommitID = Get-EnvironmentVariable 'APPVEYOR_REPO_COMMIT'
        $buildInfo.ScmBranch = Get-EnvironmentVariable 'APPVEYOR_REPO_BRANCH'
        $buildInfo.BuildServer = [Whiskey.BuildServer]::AppVeyor
    }
    elseif( (Test-Path -Path 'env:TEAMCITY_BUILD_PROPERTIES_FILE') )
    {
        function Import-TeamCityProperty
        {
            [OutputType([hashtable])]
            param(
                $Path
            )

            $properties = @{ }
            Get-Content -Path $Path |
                Where-Object { $_ -match '^([^=]+)=(.*)$' } |
                ForEach-Object { $properties[$Matches[1]] = $Matches[2] -replace '\\(.)','$1' }
            $properties
        }

        $buildInfo.BuildNumber = Get-EnvironmentVariable 'BUILD_NUMBER'
        $buildInfo.ScmCommitID = Get-EnvironmentVariable 'BUILD_VCS_NUMBER'
        $buildPropertiesPath = Get-EnvironmentVariable 'TEAMCITY_BUILD_PROPERTIES_FILE'

        $buildProperties = Import-TeamCityProperty -Path $buildPropertiesPath
        $buildInfo.BuildID = $buildProperties['teamcity.build.id']
        $buildInfo.JobName = $buildProperties['teamcity.buildType.id']
        
        $configProperties = Import-TeamCityProperty -Path $buildProperties['teamcity.configuration.properties.file']
        $buildInfo.ScmBranch = $configProperties['teamcity.build.branch'] -replace '^refs/heads/',''
        $buildInfo.ScmUri = $configProperties['vcsroot.url']
        $buildInfo.BuildServer = [Whiskey.BuildServer]::TeamCity

        $serverUri = $configProperties['teamcity.serverUrl']
        $buildInfo.JobUri = '{0}/viewType.html?buildTypeId={1}' -f $serverUri,$buildInfo.JobName
        $buildInfo.BuildUri = '{0}/viewLog.html?buildId={1}&buildTypeId={2}' -f $serverUri,$buildInfo.BuildID,$buildInfo.JobName
    }

    return $buildInfo
}




function Get-WhiskeyCredential
{
    <#
    .SYNOPSIS
    Gets a credential from the Whiskey credential store.
 
    .DESCRIPTION
    The `Get-WhiskeyCredential` function returns a credential from Whiskey's credential store. If the credential doesn't exist, the current build stops (i.e. a terminating exception is thrown).
 
    Credentials are identified by an ID that you create. Credentials are added using `Add-WhiskeyCredential`. Credentials are used by tasks. You specify the credential's ID in the task's section of the `whiskey.yml` file. See the documentation for each task for more details.
 
    .EXAMPLE
    Get-WhiskeyCredential -Context $context -ID 'bitbucketserver.example.com' -PropertyName 'CredentialID'
 
    Demonstrates how to get a credential. IN this case, retrieves the credential that was added with the ID `bitbucketserver.example.com`. #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [object]
        # The current build context. Use `New-WhiskeyContext` to create context objects.
        $Context,

        [Parameter(Mandatory=$true)]
        [string]
        # The ID of the credential. You make this up.
        $ID,

        [Parameter(Mandatory=$true)]
        [string]
        # The property name in the task that needs this credential. Used in error messages to help users pinpoint what task and property might be misconfigured.
        $PropertyName,

        [string]
        # INTERNAL. DO NOT USE.
        $PropertyDescription
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    if( -not $Context.Credentials.ContainsKey($ID) )
    {
        $propertyDescriptionParam = @{ }
        if( $PropertyDescription )
        {
            $propertyDescriptionParam['PropertyDescription'] = $PropertyDescription
        }
        Stop-WhiskeyTask -TaskContext $Context `
                         -Message ('Credential "{0}" does not exist in Whiskey''s credential store. Use the `Add-WhiskeyCredential` function to add this credential, e.g. `Add-WhiskeyCredential -Context $context -ID ''{0}'' -Credential $credential`.' -f $ID) `
                         @propertyDescriptionParam
        return
    }

    return $Context.Credentials[$ID]
}


function Get-WhiskeyMSBuildConfiguration
{
    <#
    .SYNOPSIS
    Gets the configuration to use when running any MSBuild-based task/tool.
 
    .DESCRIPTION
    The `Get-WhiskeyMSBuildConfiguration` function gets the configuration to use when running any MSBuild-based task/tool (e.g. the `MSBuild`, `DotNetBuild`, `DotNetPublish`, etc.). By default, the value is `Debug` when the build is being run by a developer and `Release` when run by a build server.
 
    Use `Set-WhiskeyMSBuildConfiguration` to change the current configuration.
 
    .EXAMPLE
    Get-WhiskeyMSBuildConfiguration -Context $Context
 
    Gets the configuration to use when runinng an MSBuild-based task/tool
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [Whiskey.Context]
        # The context of the current build.
        $Context
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    if( -not $Context.MSBuildConfiguration )
    {
        $configuration = 'Debug'
        if( $Context.ByBuildServer )
        {
            $configuration = 'Release'
        }
        Set-WhiskeyMSBuildConfiguration -Context $Context -Value $configuration
    }
    return $Context.MSBuildConfiguration
}


function Get-WhiskeyTask
{
    <#
    .SYNOPSIS
    Returns a list of available Whiskey tasks.
 
    .DESCRIPTION
    The `Get-WhiskeyTask` function returns a list of all available Whiskey tasks. Obsolete tasks are not returned. If you also want obsolete tasks returned, use the `-Force` switch.
 
    .EXAMPLE
    Get-WhiskeyTask
 
    Demonstrates how to get a list of all non-obsolete Whiskey tasks.
 
    .EXAMPLE
    Get-WhiskeyTask -Force
 
    Demonstrates how to get a list of all Whiskey tasks, including those that are obsolete.
    #>

    [CmdLetBinding()]
    [OutputType([Whiskey.TaskAttribute])]
    param(
        [Switch]
        # Return tasks that are obsolete. Otherwise, no obsolete tasks are returned.
        $Force
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
    
    [Management.Automation.FunctionInfo]$functionInfo = $null;
    
    foreach( $functionInfo in (Get-Command -CommandType Function) )
    {
        $functionInfo.ScriptBlock.Attributes | 
            Where-Object { $_ -is [Whiskey.TaskAttribute] } |
            ForEach-Object {
                $_.CommandName = $functionInfo.Name
                $_
            } |
            Where-Object {
                if( $Force )
                {
                    $true
                }
                return -not $_.Obsolete
            }
    }
}


function Import-WhiskeyPowerShellModule
{
    <#
    .SYNOPSIS
    Imports a PowerShell module.
 
    .DESCRIPTION
    The `Import-WhiskeyPowerShellModule` function imports a PowerShell module that is needed/used by a Whiskey task. Since Whiskey tasks all run in the module's scope, the imported modules are imported into the global scope. If a module with the same name is currently loaded, it is removed and re-imported.
 
    The module must be installed in Whiskey's PowerShell modules directory. Use the `RequiresTool` attribute on a task to have Whiskey install a module in this directory or the `GetPowerShellModule` task to install a module in the appropriate place.
 
    Pass the name of the modules to the `Name` parameter.
 
    .EXAMPLE
    Import-WhiskeyPowerShellModule -Name 'BuildMasterAutomtion'
 
    Demonstrates how to use this method to import a single module.
 
    .EXAMPLE
    Import-WhiskeyPowerShellModule -Name 'BuildMasterAutomtion','ProGetAutomation'
 
    Demonstrates how to use this method to import multiple modules.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string[]]
        # The module names to import.
        $Name
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    & {
        $VerbosePreference = 'SilentlyContinue'
        Get-Module -Name $Name | Remove-Module -Force -WhatIf:$false
    }

    $searchPaths = & {
                        Join-Path -Path (Get-Location).ProviderPath -ChildPath $powerShellModulesDirectoryName
                        $whiskeyModulesRoot
                   }

    foreach( $moduleName in $Name )
    {
        foreach( $searchDir in  $searchPaths )
        {
            $moduleDir = Join-Path -Path $searchDir -ChildPath $moduleName
            if( (Test-Path -Path $moduleDir -PathType Container) )
            {
                Write-Debug -Message ('PSModuleAutoLoadingPreference = "{0}"' -f $PSModuleAutoLoadingPreference)
                Write-Verbose -Message ('Import PowerShell module "{0}" from "{1}".' -f $moduleName,$searchDir)
                $numErrorsBefore = $Global:Error.Count
                & {
                    $VerbosePreference = 'SilentlyContinue'
                    Import-Module -Name $moduleDir -Global -Force -ErrorAction Stop
                } 4> $null
                # Some modules (...cough...PowerShellGet...cough...) write silent errors during import. This causes our tests
                # to fail. I know this is a little extreme.
                $numErrorsAfter = $Global:Error.Count - $numErrorsBefore
                for( $idx = 0; $idx -lt $numErrorsAfter; ++$idx )
                {
                    $Global:Error.RemoveAt(0)
                }
                break
            }
        }

        if( -not (Get-Module -Name $moduleName) )
        {
            Write-Error -Message ('Module "{0}" does not exist. Make sure your task uses the "RequiresTool" attribute so that the module gets installed automatically.' -f $moduleName) -ErrorAction Stop
        }
    }
}


function Import-WhiskeyYaml
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true,ParameterSetName='FromFile')]
        [string]
        $Path,

        [Parameter(Mandatory=$true,ParameterSetName='FromString')]
        [string]
        $Yaml
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    if( $PSCmdlet.ParameterSetName -eq 'FromFile' )
    {
        $Yaml = Get-Content -Path $Path -Raw 
    }

    $builder = New-Object 'YamlDotNet.Serialization.DeserializerBuilder'
    $deserializer = $builder.Build()

    $reader = New-Object 'IO.StringReader' $Yaml
    $config = @{}
    try
    {
        $config = $deserializer.Deserialize( $reader )
    }
    finally
    {
        $reader.Close()
    }
    
    if( -not $config )
    {
        $config = @{} 
    }

    if( $config -is [string] )
    {
        $config = @{ $config = '' }
    }

    return $config
}


function Install-WhiskeyDotNetSdk
{
    <#
    .SYNOPSIS
    Installs the .NET Core SDK tooling.
 
    .DESCRIPTION
    The `Install-WhiskeyDotNetSdk` function will install the .NET Core SDK tools and return the path to the installed `dotnet.exe` command. If you specify the `Global` switch then the function will first look for any globally installed .NET Core SDK's with the desired version already installed. If one is found, then install is skipped and the path to the global install is returned. The function uses the `dotnet-install.ps1` script from the [dotnet-cli](https://github.com/dotnet/cli) GitHub repository to download and install the SDK.
 
    .EXAMPLE
    Install-WhiskeyDotNetSdk -InstallRoot 'C:\Build\.dotnet' -Version '2.1.4'
 
    Demonstrates installing .NET Core SDK version 2.1.4 to the 'C:\Build\.dotnet' directory. After install the function will return the path 'C:\Build\.dotnet\dotnet.exe'.
 
    .EXAMPLE
    Install-WhiskeyDotNetSdk -InstallRoot 'C:\Build\.dotnet' -Version '2.1.4' -Global
 
    Demonstrates searching for an existing global install of the .NET Core SDK version '2.1.4'. If not found globally, the SDK will be installed to 'C:\Build\.dotnet'.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [string]
        # Directory where the .NET Core SDK will be installed.
        $InstallRoot,

        [Parameter(Mandatory=$true)]
        [string]
        # Version of the .NET Core SDK to install.
        $Version,

        [switch]
        # Search for the desired version from existing global installs of the .NET Core SDK. If found, the install is skipped and the path to the global install is returned.
        $Global
    )

    Set-StrictMode -version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    if ($Global)
    {
        $dotnetGlobalInstalls = Get-Command -Name 'dotnet' -All -ErrorAction Ignore | Select-Object -ExpandProperty 'Path'
        if ($dotnetGlobalInstalls)
        {
            Write-Verbose -Message ('[{0}] Found global installs of .NET Core SDK: "{1}"' -f $MyInvocation.MyCommand,($dotnetGlobalInstalls -join '","'))

            Write-Verbose -Message ('[{0}] Checking global installs for SDK version "{1}"' -f $MyInvocation.MyCommand,$Version)
            foreach ($dotnetPath in $dotnetGlobalInstalls)
            {
                $sdkPath = Join-Path -Path ($dotnetPath | Split-Path -Parent) -ChildPath ('sdk\{0}' -f $Version)

                if (Test-Path -Path $sdkPath -PathType Container)
                {
                    Write-Verbose ('[{0}] Found SDK version "{1}" at "{2}"' -f $MyInvocation.MyCommand,$Version,$sdkPath)
                    return $dotnetPath
                }
            }
        }

        Write-Verbose -Message ('[{0}] .NET Core SDK version "{1}" not found globally' -f $MyInvocation.MyCommand,$Version)
    }

    $verboseParam = @{}
    if ($VerbosePreference -eq 'Continue')
    {
        $verboseParam['Verbose'] = $true
    }

    Write-Verbose -Message ('[{0}] Installing .NET Core SDK version "{1}" to "{2}"' -f $MyInvocation.MyCommand,$Version,$InstallRoot)

    $dotnetInstallScript = Join-Path -Path $whiskeyBinPath -ChildPath 'dotnet-install.ps1' -Resolve
    $errorActionParam = @{ ErrorAction = 'Stop' }
    $installingWithShell = $false
    $executableName = 'dotnet.exe'
    if( $IsLinux -or $IsMacOS )
    {
        $dotnetInstallScript = Join-Path -Path $whiskeyBinPath -ChildPath 'dotnet-install.sh' -Resolve
        $errorActionParam = @{ }
        $installingWithShell = $true
        $executableName = 'dotnet'  
    }
    Invoke-Command -NoNewScope -ArgumentList $dotnetInstallScript,$InstallRoot,$Version,$verboseParam -ScriptBlock {
        param(
            $dotnetInstall,
            $InstallDir,
            $VersionNumber,
            $Verbose
        )

        $errCount = $Global:Error.Count
        & {
            if( $installingWithShell )
            {
                Write-Verbose ('bash {0} -InstallDir "{1}" -Version "{2}" -NoPath' -f $dotnetInstall,$InstallDir,$VersionNumber)
                bash $dotnetInstall -InstallDir $InstallDir -Version $VersionNumber -NoPath
            }
            else 
            {
                Write-Verbose ('{0} -InstallDir "{1}" -Version "{2}" -NoPath' -f $dotnetInstall,$InstallDir,$VersionNumber)
                & $dotnetInstall -InstallDir $InstallDir -Version $VersionNumber -NoPath @Verbose @errorActionParam
            }
        } | Write-Verbose -Verbose
        if( $installingWithShell -and $LASTEXITCODE )
        {
            Write-Error -Message ('{0} exited with code "{1}". Failed to install .NET Core.' -f $dotnetInstall,$LASTEXITCODE) -ErrorAction Stop
            return
        }
        $newErrCount = $Global:Error.Count
        for( $count = 0; $count -lt $newErrCount; ++$count )
        {
            $Global:Error.RemoveAt(0)
        }
    }

    $dotnetPath = Join-Path -Path $InstallRoot -ChildPath $executableName -Resolve -ErrorAction Ignore
    if (-not $dotnetPath)
    {
        Write-Error -Message ('After attempting to install .NET Core SDK version "{0}", the "{1}" executable was not found in "{2}"' -f $Version,$executableName,$InstallRoot)
        return
    }

    $sdkPath = Join-Path -Path $InstallRoot -ChildPath ('sdk\{0}' -f $Version)
    if (-not (Test-Path -Path $sdkPath -PathType Container))
    {
        Write-Error -Message ('The "{0}" command was installed but version "{1}" of the SDK was not found at "{2}"' -f $executableName,$Version,$sdkPath)
        return
    }

    return $dotnetPath
}



function Install-WhiskeyDotNetTool
{
    <#
    .SYNOPSIS
    Installs the .NET Core SDK tooling for a Whiskey task.
 
    .DESCRIPTION
    The `Install-WhiskeyDotNetTool` function installs the desired version of the .NET Core SDK for a Whiskey task. When given a `Version` the function will attempt to resolve that version to a valid released version of the SDK. If `Version` is null the function will search for a `global.json` file, first in the `WorkingDirectory` and then the `InstallRoot`, and if found it will look for the desired SDK verson in the `sdk.version` property of that file. After installing the SDK the function will update the `global.json`, creating it in the `InstallRoot` if it doesn't exist, `sdk.version` property with the installed version of the SDK. The function returns the path to the installed `dotnet.exe` command.
 
    .EXAMPLE
    Install-WhiskeyDotNetTool -InstallRoot 'C:\Build\Project' -WorkingDirectory 'C:\Build\Project\src' -Version '2.1.4'
     
    Demonstrates installing version '2.1.4' of the .NET Core SDK to a '.dotnet' directory in the 'C:\Build\Project' directory.
 
    .EXAMPLE
    Install-WhiskeyDotNetTool -InstallRoot 'C:\Build\Project' -WorkingDirectory 'C:\Build\Project\src' -Version '2.*'
     
    Demonstrates installing the latest '2.*' version of the .NET Core SDK to a '.dotnet' directory in the 'C:\Build\Project' directory.
 
    .EXAMPLE
    Install-WhiskeyDotNetTool -InstallRoot 'C:\Build\Project' -WorkingDirectory 'C:\Build\Project\src'
     
    Demonstrates installing the version of the .NET Core SDK specified in the `sdk.version` property of the `global.json` file found in either the `WorkingDirectory` or the `InstallRoot` paths.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [string]
        # Path where the `.dotnet` directory will be installed containing the .NET Core SDK.
        $InstallRoot,

        [Parameter(Mandatory=$true)]
        [string]
        # The working directory of the task requiring the .NET Core SDK tool. This path is used for searching for an existing `global.json` file containing an SDK version value.
        $WorkingDirectory,

        [AllowEmptyString()]
        [AllowNull()]
        [string]
        # The version of the .NET Core SDK to install. Accepts wildcards.
        $Version
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    $globalJsonPath = Join-Path -Path $WorkingDirectory -ChildPath 'global.json'
    if (-not (Test-Path -Path $globalJsonPath -PathType Leaf))
    {
        $globalJsonPath = Join-Path -Path $InstallRoot -ChildPath 'global.json'
    }
    
    $sdkVersion = $null
    if ($Version)
    {
        Write-Verbose -Message ('[{0}] .NET Core SDK version ''{1}'' found in whiskey.yml' -f $MyInvocation.MyCommand,$Version)
        $sdkVersion = Resolve-WhiskeyDotNetSdkVersion -Version $Version
    }
    elseif (Test-Path -Path $globalJsonPath -PathType Leaf)
    {
        try
        {
            $globalJson = Get-Content -Path $globalJsonPath -Raw | ConvertFrom-Json
        }
        catch
        {
            Write-Error -Message ('global.json file ''{0}'' contains invalid JSON.' -f $globalJsonPath)
            return
        }

        $globalJsonVersion = $globalJson |
                                 Select-Object -ExpandProperty 'sdk' -ErrorAction Ignore |
                                 Select-Object -ExpandProperty 'version' -ErrorAction Ignore
        
        if ($globalJsonVersion)
        {
            Write-Verbose -Message ('[{0}] .NET Core SDK version ''{1}'' found in ''{2}''' -f $MyInvocation.MyCommand,$globalJsonVersion,$globalJsonPath)
            $sdkVersion = Resolve-WhiskeyDotNetSdkVersion -Version $globalJsonVersion
        }
    }

    if (-not $sdkVersion)
    {
        Write-Verbose -Message ('[{0}] No specific .NET Core SDK version found in whiskey.yml or global.json. Using latest LTS version.' -f $MyInvocation.MyCommand)
        $sdkVersion = Resolve-WhiskeyDotNetSdkVersion -LatestLTS
    }

    $dotnetPath = Install-WhiskeyDotNetSdk -InstallRoot (Join-Path -Path $InstallRoot -ChildPath '.dotnet') -Version $sdkVersion -Global

    Set-WhiskeyDotNetGlobalJson -Directory ($globalJsonPath | Split-Path -Parent) -SdkVersion $sdkVersion

    return $dotnetPath
}



function Install-WhiskeyNode
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]
        # The directory where Node should be installed. Will actually be installed into `Join-Path -Path $InstallRoot -ChildPath '.node'`.
        $InstallRoot,

        [Switch]
        # Are we running in clean mode? If so, don't re-install the tool.
        $InCleanMode,

        [string]
        # The version of Node to install. If not provided, will use the version defined in the package.json file. If that isn't supplied, will install the latest LTS version.
        $Version
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    $nodePath = Resolve-WhiskeyNodePath -BuildRootPath $InstallRoot -ErrorAction Ignore

    if( $InCleanMode )
    {
        if( $nodePath )
        {
            return $nodePath
        }
        return
    }

    $npmVersionToInstall = $null
    $nodeVersionToInstall = $null
    $nodeVersions = Invoke-RestMethod -Uri 'https://nodejs.org/dist/index.json' | ForEach-Object { $_ }
    if( $Version )
    {
        $nodeVersionToInstall = $nodeVersions | Where-Object { $_.version -like 'v{0}' -f $Version } | Select-Object -First 1
        if( -not $nodeVersionToInstall )
        {
            throw ('Node v{0} does not exist.' -f $Version)
        }
    }
    else
    {
        $packageJsonPath = Join-Path -Path (Get-Location).ProviderPath -ChildPath 'package.json'
        if( -not (Test-Path -Path $packageJsonPath -PathType Leaf) )
        {
            $packageJsonPath = Join-Path -Path $InstallRoot -ChildPath 'package.json'
        }

        if( (Test-Path -Path $packageJsonPath -PathType Leaf) )
        {
            Write-Verbose -Message ('Reading ''{0}'' to determine Node and NPM versions to use.' -f $packageJsonPath)
            $packageJson = Get-Content -Raw -Path $packageJsonPath | ConvertFrom-Json
            if( $packageJson -and ($packageJson | Get-Member 'engines') )
            {
                if( ($packageJson.engines | Get-Member 'node') -and $packageJson.engines.node -match '(\d+\.\d+\.\d+)' )
                {
                    $nodeVersionToInstall = 'v{0}' -f $Matches[1]
                    $nodeVersionToInstall =  $nodeVersions |
                                                Where-Object { $_.version -eq $nodeVersionToInstall } |
                                                Select-Object -First 1
                }

                if( ($packageJson.engines | Get-Member 'npm') -and $packageJson.engines.npm -match '(\d+\.\d+\.\d+)' )
                {
                    $npmVersionToInstall = $Matches[1]
                }
            }
        }
    }

    if( -not $nodeVersionToInstall )
    {
        $nodeVersionToInstall = $nodeVersions |
                                    Where-Object { ($_ | Get-Member 'lts') -and $_.lts } |
                                    Select-Object -First 1
    }

    if( -not $npmVersionToInstall )
    {
        $npmVersionToInstall = $nodeVersionToInstall.npm
    }

    $installNode = $false
    if( $nodePath )
    {
        $currentNodeVersion = & $nodePath '--version'
        if( $currentNodeVersion -ne $nodeVersionToInstall.version )
        {
            Uninstall-WhiskeyTool -Name 'Node' -InstallRoot $InstallRoot
            $installNode = $true
        }
    }
    else
    {
        $installNode = $true
    }

    $nodeRoot = Join-Path -Path $InstallRoot -ChildPath '.node'
    if( -not (Test-Path -Path $nodeRoot -PathType Container) )
    {
        New-Item -Path $nodeRoot -ItemType 'Directory' -Force | Out-Null
    }

    $platform = 'win'
    $packageExtension = 'zip'
    if( $IsLinux )
    {
        $platform = 'linux'
        $packageExtension = 'tar.xz'
    }
    elseif( $IsMacOS )
    {
        $platform = 'darwin'
        $packageExtension = 'tar.gz'
    }

    $extractedDirName = 'node-{0}-{1}-x64' -f $nodeVersionToInstall.version,$platform
    $filename = '{0}.{1}' -f $extractedDirName,$packageExtension
    $nodeZipFile = Join-Path -Path $nodeRoot -ChildPath $filename
    if( -not (Test-Path -Path $nodeZipFile -PathType Leaf) )
    {
        $uri = 'https://nodejs.org/dist/{0}/{1}' -f $nodeVersionToInstall.version,$filename
        try
        {
            Invoke-WebRequest -Uri $uri -OutFile $nodeZipFile
        }
        catch
        {
            $responseStatus = $_.Exception.Response.StatusCode
            $errorMsg = 'Failed to download Node {0}. Received a {1} ({2}) response when retreiving URI {3}.' -f $nodeVersionToInstall.version,$responseStatus,[int]$responseStatus,$uri
            if( $responseStatus -eq [Net.HttpStatusCode]::NotFound )
            {
                $errorMsg = '{0} It looks like this version of Node wasn''t packaged as a ZIP file. Please use Node v4.5.0 or newer.' -f $errorMsg
            }
            throw $errorMsg
        }
    }

    if( $installNode )
    {
        if( $IsWindows )
        {
            # Windows/.NET can't handle the long paths in the Node package, so on that platform, we need to download 7-zip. It can handle paths that long.
            $7zipPackageRoot = Install-WhiskeyTool -NuGetPackageName '7-Zip.CommandLine' -DownloadRoot $InstallRoot
            $7z = Join-Path -Path $7zipPackageRoot -ChildPath 'tools\x64\7za.exe' -Resolve -ErrorAction Stop
            Write-Verbose -Message ('{0} x {1} -o{2} -y' -f $7z,$nodeZipFile,$nodeRoot)
            & $7z 'x' $nodeZipFile ('-o{0}' -f $nodeRoot) '-y' | Write-Verbose

            Get-ChildItem -Path $nodeRoot -Filter 'node-*' -Directory |
                Get-ChildItem |
                Move-Item -Destination $nodeRoot
        }
        else
        {
            Write-Verbose -Message ('tar -xJf "{0}" -C "{1}" --strip-components=1' -f $nodeZipFile,$nodeRoot)
            tar -xJf $nodeZipFile -C $nodeRoot '--strip-components=1' | Write-Verbose
            if( $LASTEXITCODE )
            {
                Write-Error -Message ('Failed to extract Node.js {0} package "{1}" to "{2}".' -f $nodeVersionToInstall.version,$nodeZipFile,$nodeRoot)
                return
            }
        }

        $nodePath = Resolve-WhiskeyNodePath -BuildRootPath $InstallRoot -ErrorAction Stop
    }

    $npmPath = Resolve-WhiskeyNodeModulePath -Name 'npm' -NodeRootPath $nodeRoot -ErrorAction Stop
    $npmPath = Join-Path -Path $npmPath -ChildPath 'bin\npm-cli.js'
    $npmVersion = & $nodePath $npmPath '--version'
    if( $npmVersion -ne $npmVersionToInstall )
    {
        Write-Verbose ('Installing npm@{0}.' -f $npmVersionToInstall)
        # Bug in NPM 5 that won't delete these files in the node home directory.
        Get-ChildItem -Path (Join-Path -Path $nodeRoot -ChildPath '*') -Include 'npm.cmd','npm','npx.cmd','npx' | Remove-Item
        & $nodePath $npmPath 'install' ('npm@{0}' -f $npmVersionToInstall) '-g' | Write-Verbose
        if( $LASTEXITCODE )
        {
            throw ('Failed to update to NPM {0}. Please see previous output for details.' -f $npmVersionToInstall)
        }
    }

    return $nodePath
}



function Install-WhiskeyNodeModule
{
    <#
    .SYNOPSIS
    Installs Node.js modules
     
    .DESCRIPTION
    The `Install-WhiskeyNodeModule` function installs Node.js modules to the `node_modules` directory located in the current working directory. The path to the module's directory is returned.
 
    Failing to install a module does not cause a bulid to fail. If you want a build to fail if the module fails to install, you must pass `-ErrorAction Stop`.
     
    .EXAMPLE
    Install-WhiskeyNodeModule -Name 'rimraf' -Version '^2.0.0' -NodePath $TaskParameter['NodePath']
 
    This example will install the Node module `rimraf` at the latest `2.x.x` version in the `node_modules` directory located in the current directory.
     
    .EXAMPLE
    Install-WhiskeyNodeModule -Name 'rimraf' -Version '^2.0.0' -NodePath $TaskParameter['NodePath -ErrorAction Stop
 
    Demonstrates how to fail a build if installing the module fails by setting the `ErrorAction` parameter to `Stop`.
    #>

    
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [string]
        # The name of the module to install.
        $Name,

        [string]
        # The version of the module to install.
        $Version,

        [switch]
        # Node modules are being installed on a developer computer.
        $ForDeveloper,

        [Parameter(Mandatory)]
        [string]
        # The path to the build root.
        $BuildRootPath,

        [Switch]
        # Whether or not to install the module globally.
        $Global,

        [Switch]
        # Are we running in clean mode?
        $InCleanMode
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    $npmArgument = & {
                        if( $Version )
                        {
                            ('{0}@{1}' -f $Name,$Version)
                        }
                        else
                        {
                            $Name
                        }
                        if( $Global )
                        {
                            '-g'
                        }
                    }

    $modulePath = Resolve-WhiskeyNodeModulePath -Name $Name -BuildRootPath $BuildRootPath -Global:$Global -ErrorAction Ignore
    if( $modulePath )
    {
        return $modulePath
    }
    elseif( $InCleanMode )
    {
        return
    }

    Invoke-WhiskeyNpmCommand -Name 'install' -ArgumentList $npmArgument -BuildRootPath $BuildRootPath -ForDeveloper:$ForDeveloper | Write-Verbose
    if( $LASTEXITCODE )
    {
        return
    }

    $modulePath = Resolve-WhiskeyNodeModulePath -Name $Name -BuildRootPath $BuildRootPath -Global:$Global -ErrorAction Ignore
    if( -not $modulePath )
    {
        Write-Error -Message ('NPM executed successfully when attempting to install "{0}" but the module was not found anywhere in the build root "{1}"' -f ($npmArgument -join ' '),$BuildRootPath)
        return
    }

    return $modulePath
}



function Install-WhiskeyNuGet
{
    <#
    .SYNOPSIS
    Installs NuGet from its NuGet package.
 
    .DESCRIPTION
    The `Install-WhiskeyNuGet` function installs NuGet from its NuGet package. It returns the path to the `NuGet.exe` from the package.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [string]
        # Where to install NuGet.
        $DownloadRoot,

        [string]
        # The version to download.
        $Version
    )

    Set-StrictMode -version 'Latest'  
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    $versionParam = @{ }
    if( $Version )
    {
        $versionParam['Version'] = $Version
    }

    $nuGetPath = Install-WhiskeyTool -NuGetPackageName 'NuGet.CommandLine' -DownloadRoot $DownloadRoot @versionParam
    return Join-Path -Path $nuGetPath -ChildPath 'tools\NuGet.exe' -Resolve
}


function Install-WhiskeyPowerShellModule
{
    <#
    .SYNOPSIS
    Installs a PowerShell module.
 
    .DESCRIPTION
    The `Install-WhiskeyPowerShellModule` function installs a PowerShell module into a "PSModules" directory in the current working directory. It returns the path to the module.
 
    .EXAMPLE
    Install-WhiskeyPowerShellModule -Name 'Pester' -Version '4.3.0'
 
    This example will install the PowerShell module `Pester` at version `4.3.0` version in the `PSModules` directory.
 
    .EXAMPLE
    Install-WhiskeyPowerShellModule -Name 'Pester' -Version '4.*'
 
    Demonstrates that you can use wildcards to choose the latest minor version of a module.
 
    .EXAMPLE
    Install-WhiskeyPowerShellModule -Name 'Pester' -Version '4.3.0' -ErrorAction Stop
 
    Demonstrates how to fail a build if installing the module fails by setting the `ErrorAction` parameter to `Stop`.
    #>


    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [string]
        # The name of the module to install.
        $Name,

        [string]
        # The version of the module to install.
        $Version,

        [string]
        # Modules are saved into a PSModules directory. The "Path" parameter is the path where this PSModules directory should be, *not* the path to the PSModules directory itself, i.e. this is the path to the "PSModules" directory's parent directory.
        $Path = (Get-Location).ProviderPath
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    Import-WhiskeyPowerShellModule -Name 'PackageManagement','PowerShellGet'

    Get-PackageProvider -Name 'NuGet' -ForceBootstrap | Out-Null

    $modulesRoot = Join-Path -Path $Path -ChildPath $powerShellModulesDirectoryName
    if( -not (Test-Path -Path $modulesRoot -PathType Container) )
    {
        New-Item -Path $modulesRoot -ItemType 'Directory' -ErrorAction Stop | Out-Null
    }

    $expectedPath = Join-Path -Path $modulesRoot -ChildPath $Name

    if( (Test-Path -Path $expectedPath -PathType Container) -and (Get-ChildItem -Path $expectedPath -File -Filter ('{0}.psd1' -f $Name) -Recurse))
    {
        Resolve-Path -Path $expectedPath | Select-Object -ExpandProperty 'ProviderPath'
        return
    }

    $module = Resolve-WhiskeyPowerShellModule -Name $Name -Version $Version
    if( -not $module )
    {
        return
    }

    Write-Verbose -Message ('Saving PowerShell module {0} {1} to "{2}" from repository {3}.' -f $Name,$module.Version,$modulesRoot,$module.Repository)
    Save-Module -Name $Name -RequiredVersion $module.Version -Repository $module.Repository -Path $modulesRoot

    if( -not (Test-Path -Path $expectedPath -PathType Container) )
    {
        Write-Error -Message ('Failed to download {0} {1} from {2} ({3}). Either the {0} module does not exist, or it does but version {1} does not exist. Browse the PowerShell Gallery at https://www.powershellgallery.com/' -f $Name,$Version,$module.Repository,$module.RepositorySourceLocation)
    }

    return $expectedPath
}



function Install-WhiskeyTool
{
    <#
    .SYNOPSIS
    Downloads and installs tools needed by the Whiskey module.
 
    .DESCRIPTION
    The `Install-WhiskeyTool` function downloads and installs PowerShell modules or NuGet Packages needed by functions in the Whiskey module. PowerShell modules are installed to a `Modules` directory in your build root. A `DirectoryInfo` object for the downloaded tool's directory is returned.
 
    `Install-WhiskeyTool` also installs tools that are needed by tasks. Tasks define the tools they need with a [Whiskey.RequiresTool()] attribute in the tasks function. Supported tools are 'Node', 'NodeModule', and 'DotNet'.
 
    Users of the `Whiskey` API typcially won't need to use this function. It is called by other `Whiskey` function so they ahve the tools they need.
 
    .EXAMPLE
    Install-WhiskeyTool -NugetPackageName 'NUnit.Runners' -version '2.6.4'
 
    Demonstrates how to install a specific version of a NuGet Package. In this case, NUnit Runners version 2.6.4 would be installed.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true,ParameterSetName='Tool')]
        [Whiskey.RequiresToolAttribute]
        # The attribute that defines what tool is necessary.
        $ToolInfo,

        [Parameter(Mandatory=$true,ParameterSetName='Tool')]
        [string]
        # The directory where you want the tools installed.
        $InstallRoot,

        [Parameter(Mandatory=$true,ParameterSetName='Tool')]
        [hashtable]
        # The task parameters for the currently running task.
        $TaskParameter,

        [Parameter(ParameterSetName='Tool')]
        [Switch]
        # Running in clean mode, so don't install the tool if it isn't installed.
        $InCleanMode,

        [Parameter(Mandatory=$true,ParameterSetName='NuGet')]
        [string]
        # The name of the NuGet package to download.
        $NuGetPackageName,

        [Parameter(ParameterSetName='NuGet')]
        [string]
        # The version of the package to download. Must be a three part number, i.e. it must have a MAJOR, MINOR, and BUILD number.
        $Version,

        [Parameter(Mandatory=$true,ParameterSetName='NuGet')]
        [string]
        # The root directory where the tools should be downloaded. The default is your build root.
        #
        # PowerShell modules are saved to `$DownloadRoot\Modules`.
        #
        # NuGet packages are saved to `$DownloadRoot\packages`.
        $DownloadRoot
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    $mutexName = $InstallRoot
    if( $DownloadRoot )
    {
        $mutexName = $DownloadRoot
    }
    # Back slashes in mutex names are reserved.
    $mutexName = $mutexName -replace '\\','/'
    $mutexName = $mutexName -replace '/','-'
    $startedWaitingAt = Get-Date
    $startedUsingAt = Get-Date
    Write-Debug -Message ('Creating mutex "{0}".' -f $mutexName)
    $installLock = New-Object 'Threading.Mutex' $false,$mutexName
    #$DebugPreference = 'Continue'
    Write-Debug -Message ('[{0:yyyy-MM-dd HH:mm:ss}] Process "{1}" is waiting for mutex "{2}".' -f (Get-Date),$PID,$mutexName)

    try
    {
        try
        {
            [void]$installLock.WaitOne()
        }
        catch [Threading.AbandonedMutexException]
        {
            Write-Debug -Message ('[{0:yyyy-MM-dd HH:mm:ss}] Process "{1}" caught "{2}" exception waiting to acquire mutex "{3}": {4}.' -f (Get-Date),$PID,$_.Exception.GetType().FullName,$mutexName,$_)
            $Global:Error.RemoveAt(0)
        }

        $waitedFor = (Get-Date) - $startedWaitingAt
        Write-Debug -Message ('[{0:yyyy-MM-dd HH:mm:ss}] Process "{1}" obtained mutex "{2}" in {3}.' -f (Get-Date),$PID,$mutexName,$waitedFor)
        #$DebugPreference = 'SilentlyContinue'
        $startedUsingAt = Get-Date

        if( $PSCmdlet.ParameterSetName -eq 'NuGet' )
        {
            if( -not $IsWindows )
            {
                Write-Error -Message ('Unable to install NuGet-based package {0} {1}: NuGet.exe is only supported on Windows.' -f $NuGetPackageName,$Version) -ErrorAction Stop
                return
            }
            $packagesRoot = Join-Path -Path $DownloadRoot -ChildPath 'packages'
            $version = Resolve-WhiskeyNuGetPackageVersion -NuGetPackageName $NuGetPackageName -Version $Version -NugetPath $whiskeyNuGetExePath
            if( -not $Version )
            {
                return
            }

            $nuGetRootName = '{0}.{1}' -f $NuGetPackageName,$Version
            $nuGetRoot = Join-Path -Path $packagesRoot -ChildPath $nuGetRootName
            Set-Item -Path 'env:EnableNuGetPackageRestore' -Value 'true'
            if( -not (Test-Path -Path $nuGetRoot -PathType Container) )
            {
               & $whiskeyNuGetExePath install $NuGetPackageName -version $Version -outputdirectory $packagesRoot | Write-CommandOutput -Description ('nuget.exe install')
            }
            return $nuGetRoot
        }
        elseif( $PSCmdlet.ParameterSetName -eq 'Tool' )
        {
            $provider,$name = $ToolInfo.Name -split '::'
            if( -not $name )
            {
                $name = $provider
                $provider = ''
            }

            $version = $TaskParameter[$ToolInfo.VersionParameterName]
            if( -not $version )
            {
                $version = $ToolInfo.Version
            }

            switch( $provider )
            {
                'NodeModule'
                {
                    $nodePath = Resolve-WhiskeyNodePath -BuildRootPath $InstallRoot
                    if( -not $nodePath )
                    {
                        Write-Error -Message ('It looks like Node isn''t installed in your repository. Whiskey usually installs Node for you into a .node directory. If this directory doesn''t exist, this is most likely a task authoring error and the author of your task needs to add a `WhiskeyTool` attribute declaring it has a dependency on Node. If the .node directory exists, the Node package is most likely corrupt. Please delete it and re-run your build.') -ErrorAction stop
                        return
                    }
                    $moduleRoot = Install-WhiskeyNodeModule -Name $name `
                                                            -BuildRootPath $InstallRoot `
                                                            -Version $version `
                                                            -Global `
                                                            -InCleanMode:$InCleanMode `
                                                            -ErrorAction Stop
                    $TaskParameter[$ToolInfo.PathParameterName] = $moduleRoot
                }
                'PowerShellModule'
                {
                    $moduleRoot = Install-WhiskeyPowerShellModule -Name $name -Version $version -ErrorAction Stop
                    $TaskParameter[$ToolInfo.PathParameterName] = $moduleRoot
                }
                default
                {
                    switch( $name )
                    {
                        'Node'
                        {
                            $TaskParameter[$ToolInfo.PathParameterName] = Install-WhiskeyNode -InstallRoot $InstallRoot -Version $version -InCleanMode:$InCleanMode
                        }
                        'DotNet'
                        {
                            $TaskParameter[$ToolInfo.PathParameterName] = Install-WhiskeyDotNetTool -InstallRoot $InstallRoot -WorkingDirectory (Get-Location).ProviderPath -Version $version -ErrorAction Stop
                        }
                        default
                        {
                            throw ('Unknown tool ''{0}''. The only supported tools are ''Node'' and ''DotNet''.' -f $name)
                        }
                    }
                }
            }
        }
    }
    finally
    {
        #$DebugPreference = 'Continue'
        $usedFor = (Get-Date) - $startedUsingAt
        Write-Debug -Message ('[{0:yyyy-MM-dd HH:mm:ss}] Process "{1}" releasing mutex "{2}" after using it for {3}.' -f (Get-Date),$PID,$mutexName,$usedFor)
        $startedReleasingAt = Get-Date
        $installLock.ReleaseMutex();
        $installLock.Dispose()
        $installLock.Close()
        $installLock = $null
        $releasedDuration = (Get-Date) - $startedReleasingAt
        Write-Debug -Message ('[{0:yyyy-MM-dd HH:mm:ss}] Process "{1}" released mutex "{2}" in {3}.' -f (Get-Date),$PID,$mutexName,$releasedDuration)
        #$DebugPreference = 'SilentlyContinue'
    }
}



function Invoke-WhiskeyBuild
{
    <#
    .SYNOPSIS
    Runs a build.
 
    .DESCRIPTION
    The `Invoke-WhiskeyBuild` function runs a build as defined by your `whiskey.yml` file. Use the `New-WhiskeyContext` function to create a context object, then pass that context object to `Invoke-WhiskeyBuild`. `New-WhiskeyContext` takes the path to the `whiskey.yml` file you want to run:
 
        $context = New-WhiskeyContext -Environment 'Developer' -ConfigurationPath 'whiskey.yml'
        Invoke-WhiskeyBuild -Context $context
 
    Builds can run in three modes: `Build`, `Clean`, and `Initialize`. The default behavior is `Build` mode.
 
    In `Build` mode, each task in the `Build` pipeline is run. If you're on a publishing branch, and being run on a build server, each task in the `Publish` pipeline is also run.
 
    In `Clean` mode, each task that supports clean mode is run. In this mode, tasks clean up any build artifacts they create. Tasks opt-in to this mode. If a task isn't cleaning up, it should be updated to support clean mode.
 
    In `Initialize` mode, each task that suppors initialize mode is run. In this mode, tasks download, install, and configure any tools or other dependencies needed. This mode is intended to be used by developers so they can get any tools needed to start developing without having to run an entire build, which may take a long time. Tasks opt-in to this mode. If a task uses an external tool or dependences, and they don't exist after running in `Initialize` mode, it should be updated to support `Initialize` mode.
 
    (Task authors should see the `about_Whiskey_Writing_Tasks` for information about how to opt-in to `Clean` and `Initialize` mode.)
 
    Your `whiskey.yml` file can contain multiple pipelines (see `about_Whiskey.yml` for information about `whiskey.yml syntax). Usually, there is a pipeline for each application you want to build. To build specific pipelines, pass the pipeline names to the `PipelineName` parameter. Just those pipeline will be run. The `Publish` pipeline will *not* run unless it is one of the names you pass to the `PipelineName` parameter.
 
    .LINK
    about_Whiskey.yml
 
    .LINK
    New-WhiskeyContext
 
    .LINK
    about_Whiskey_Writing_Tasks
 
    .EXAMPLE
    Invoke-WhiskeyBuild -Context $context
 
    Demonstrates how to run a complete build. In this example, the `Build` pipeline is run, and, if running on a build server and on a publishing branch, the `Publish` pipeline is run.
 
    .EXAMPLE
    Invoke-WhiskeyBuild -Context $context -Clean
 
    Demonstrates how to run a build in `Clean` mode. In this example, each task in the `Build` and `Publish` pipelines that support `Clean` mode is run so they can delete any build output, downloaded depedencies, etc.
 
    .EXAMPLE
    Invoke-WhiskeyBuild -Context $context -Initialize
 
    Demonstrates how to run a build in `Initialize` mode. In this example, each task in the `Build` and `Publish` pipelines that supports `Initialize` mode is run so they can download/install/configure any tools or dependencies.
 
    .EXAMPLE
    Invoke-WhiskeyBuild -Context $context -PipelineName 'App1','App2'
 
    Demonstrates how to run specific pipelines. In this example, all the tasks in the `App1` and `App2` pipelines are run. See `about_Whiskey.yml` for information about how to define pipelines.
    #>

    [CmdletBinding(DefaultParameterSetName='Build')]
    param(
        [Parameter(Mandatory=$true)]
        [Whiskey.Context]
        # The context for the build. Use `New-WhiskeyContext` to create context objects.
        $Context,

        [string[]]
        # The name(s) of any pipelines to run. Default behavior is to run the `Build` pipeline and, if on a publishing branch, the `Publish` pipeline.
        #
        # If you pass a value to this parameter, the `Publish` pipeline is *not* run implicitly. You must pass its name to run it.
        $PipelineName,

        [Parameter(Mandatory=$true,ParameterSetName='Clean')]
        [Switch]
        # Runs the build in clean mode. In clean mode, tasks delete any artifacts they create, including downloaded tools and dependencies. This is opt-in, so if a task is not deleting its artifacts, it needs to be updated to support clean mode.
        $Clean,

        [Parameter(Mandatory=$true,ParameterSetName='Initialize')]
        [Switch]
        # Runs the build in initialize mode. In initialize mode, tasks download/install/configure any tools/dependencies they use/need during the build. Initialize mode is intended to be used by developers so that any tools/dependencies they need can be downloaded/installe/configured without needing to run an entire build, which can sometimes take a long time.
        $Initialize
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    $Context.StartedAt = $script:buildStartedAt = Get-Date

    # If there are older versions of the PackageManagement and/or PowerShellGet
    # modules available on this system, the modules that ship with Whiskey will use
    # those global versions instead of the versions we load from inside Whiskey. So,
    # we have to put the ones that ship with Whiskey first. See
    # https://github.com/PowerShell/PowerShellGet/issues/446 .
    $originalPSModulesPath = $env:PSModulePath
    $env:PSModulePath = '{0};{1};{2}' -f $whiskeyModulesRoot,(Join-Path -Path $Context.BuildRoot -ChildPath $powerShellModulesDirectoryName),$env:PSModulePath

    Set-WhiskeyBuildStatus -Context $Context -Status Started

    $succeeded = $false
    Push-Location -Path $Context.BuildRoot
    try
    {
        $Context.RunMode = $PSCmdlet.ParameterSetName

        if( $PipelineName )
        {
            foreach( $name in $PipelineName )
            {
                Invoke-WhiskeyPipeline -Context $Context -Name $name
            }
        }
        else
        {
            $config = $Context.Configuration

            $buildPipelineName = 'Build'
            if( $config.ContainsKey('BuildTasks') )
            {
                $buildPipelineName = 'BuildTasks'
            }

            Invoke-WhiskeyPipeline -Context $Context -Name $buildPipelineName

            $publishPipelineName = 'Publish'
            if( $config.ContainsKey('PublishTasks') )
            {
                $publishPipelineName = 'PublishTasks'
            }

            Write-Verbose -Message ('Publish? {0}' -f $Context.Publish)
            Write-Verbose -Message ('Publish Pipeline? {0}' -f $config.ContainsKey($publishPipelineName))
            if( $Context.Publish -and $config.ContainsKey($publishPipelineName) )
            {
                Invoke-WhiskeyPipeline -Context $Context -Name $publishPipelineName
            }
        }

        $succeeded = $true
    }
    finally
    {
        if( $Clean )
        {
            Remove-Item -path $Context.OutputDirectory -Recurse -Force | Out-String | Write-Verbose
        }
        Pop-Location

        $status = 'Failed'
        if( $succeeded )
        {
            $status = 'Completed'
        }
        Set-WhiskeyBuildStatus -Context $Context -Status $status

        $env:PSModulePath = $originalPSModulesPath
    }
}



function Invoke-WhiskeyDotNetCommand
{
    <#
    .SYNOPSIS
    Runs `dotnet.exe` with a given SDK command and arguments.
 
    .DESCRIPTION
    The `Invoke-WhiskeyDotNetCommand` function runs the `dotnet.exe` executable with a given SDK command and any optional arguments. Pass the path to the `dotnet.exe` to the `DotNetPath` parameter. Pass the name of the SDK command to the `Name` parameter.
 
    You may pass a list of arguments to the `dotnet.exe` command with the `ArgumentList` parameter. By default, the `dotnet.exe` command runs with any solution or .csproj files found in the current directory. To run the `dotnet.exe` command with a specific solution or .csproj file pass the path to that file to the `ProjectPath` parameter.
 
    .EXAMPLE
    Invoke-WhiskeyDotNetCommand -TaskContext $TaskContext -DotNetPath 'C:\Program Files\dotnet\dotnet.exe' -Name 'build' -ArgumentList '--verbosity minimal','--no-incremental' -ProjectPath 'C:\Build\DotNetCore.csproj'
 
    Demonstrates running the following command `C:\> & "C:\Program Files\dotnet\dotnet.exe" build --verbosity minimal --no-incremental C:\Build\DotNetCore.csproj`
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [Whiskey.Context]
        # The `Whiskey.Context` object for the task running the command.
        $TaskContext,

        [Parameter(Mandatory=$true)]
        [string]
        # The path to the `dotnet` executable to run the SDK command with.
        $DotNetPath,

        [Parameter(Mandatory=$true)]
        [string]
        # The name of the .NET Core SDK command to run.
        $Name,

        [string[]]
        # A list of arguments to pass to the .NET Core SDK command.
        $ArgumentList,

        [string]
        # The path to a .NET Core solution or project file to pass to the .NET Core SDK command.
        $ProjectPath
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    $dotNetExe = $DotNetPath | Resolve-Path -ErrorAction 'Ignore'
    if (-not $dotNetExe)
    {
        Write-Error -Message ('"{0}" does not exist.' -f $DotNetPath)
        return
    }

    $loggerArgs = & {
        '/filelogger9'
        $logFilePath = ('dotnet.{0}.log' -f $Name.ToLower())
        if( $ProjectPath )
        {
            $logFilePath = 'dotnet.{0}.{1}.log' -f $Name.ToLower(),($ProjectPath | Split-Path -Leaf)
        }
        $logFilePath = Join-Path -Path $TaskContext.OutputDirectory.FullName -ChildPath $logFilePath
        ('/flp9:LogFile={0};Verbosity=d' -f $logFilePath)
    }

    $commandInfoArgList = & {
        $Name
        $ArgumentList
        $loggerArgs
        $ProjectPath
    }

    Write-WhiskeyCommand -Context $TaskContext -Path $dotNetExe -ArgumentList $commandInfoArgList

    Invoke-Command -ScriptBlock {
        param(
            $DotNetExe,
            $Command,
            $DotNetArgs,
            $LoggerArgs,
            $Project
        )

        & $DotNetExe $Command $DotNetArgs $LoggerArgs $Project

    } -ArgumentList $dotNetExe,$Name,$ArgumentList,$loggerArgs,$ProjectPath

    if ($LASTEXITCODE -ne 0)
    {
        Stop-WhiskeyTask -TaskContext $TaskContext -Message ('"{0}" failed with exit code {1}' -f $DotNetExe,$LASTEXITCODE)
        return
    }
}



function Invoke-WhiskeyNpmCommand
{
    <#
    .SYNOPSIS
    Runs `npm` with given command and argument.
     
    .DESCRIPTION
    The `Invoke-WhiskeyNpmCommand` function runs `npm` commands in the current workding directory. Pass the path to the build root to the `BuildRootPath` parameter. The function will use the copy of Node and NPM installed in the `.node` directory in the build root.
 
    Pass the name of the NPM command to run with the `Name` parameter. Pass any arguments to pass to the command with the `ArgumentList`.
 
    Task authors should add the `RequiresTool` attribute to their task functions to ensure that Whiskey installs Node and NPM, e.g.
 
        function MyTask
        {
            [Whiskey.Task('MyTask')]
            [Whiskey.RequiresTool('Node', 'NodePath')]
            param(
            )
        }
 
    .EXAMPLE
    Invoke-WhiskeyNpmCommand -Name 'install' -BuildRootPath $TaskParameter.BuildRoot -ForDeveloper:$Context.ByDeveloper
 
    Demonstrates how to run the `npm install` command from a task.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [string]
        # The NPM command to execute, e.g. `install`, `prune`, `run-script`, etc.
        $Name,
        
        [string[]]
        # An array of arguments to be given to the NPM command being executed.
        $ArgumentList,

        [Parameter(Mandatory=$true)]
        [string]
        $BuildRootPath,

        [switch]
        # NPM commands are being run on a developer computer.
        $ForDeveloper
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    $nodePath = Resolve-WhiskeyNodePath -BuildRootPath $BuildRootPath -ErrorAction Stop
    if( -not $nodePath )
    {
        return
    }

    $npmPath = Resolve-WhiskeyNodeModulePath -Name 'npm' -BuildRootPath $BuildRootPath -Global -ErrorAction Stop
    $npmPath = Join-Path -Path $npmPath -ChildPath 'bin\npm-cli.js'

    if( -not $npmPath -or -not (Test-Path -Path $npmPath -PathType Leaf) )
    {
        Write-Error -Message ('Whiskey failed to install NPM. Something pretty serious has gone wrong.')
        return
    }

    # Assign to new variables otherwise Invoke-Command can't find them.
    $commandName = $Name
    $commandArgs = & {
                        $ArgumentList
                        '--scripts-prepend-node-path=auto'
                        if( -not $ForDeveloper )
                        {
                            '--no-color'
                        }
                    }

    $npmCommandString = ('npm {0} {1}' -f $commandName,($commandArgs -join ' '))

    $originalPath = $env:PATH
    Set-Item -Path 'env:PATH' -Value ('{0}{1}{2}' -f (Split-Path -Path $nodePath -Parent),[IO.Path]::PathSeparator,$env:PATH)
    try
    {
        Write-Progress -Activity $npmCommandString
        Invoke-Command -ScriptBlock {
            # The ISE bails if processes write anything to STDERR. Node writes notices and warnings to
            # STDERR. We only want to stop a build if the command actually fails.
            $originalEap = $ErrorActionPreference
            if( $ErrorActionPreference -ne 'SilentlyContinue' -and $ErrorActionPreference -ne 'Ignore' )
            {
                $ErrorActionPreference = 'Continue'
            }
            try
            {
                Write-Verbose ('{0} {1} {2} {3}' -f $nodePath,$npmPath,$commandName,($commandArgs -join ' '))
                & $nodePath $npmPath $commandName $commandArgs
            }
            finally
            {
                Write-Verbose -Message ($LASTEXITCODE)
                $ErrorActionPreference = $originalEap
            }
        }
        if( $LASTEXITCODE -ne 0 )
        {
            Write-Error -Message ('NPM command "{0}" failed with exit code {1}. Please see previous output for more details.' -f $npmCommandString,$LASTEXITCODE)
        }
    }
    finally
    {
        Set-Item -Path 'env:PATH' -Value $originalPath
        Write-Progress -Activity $npmCommandString -Completed -PercentComplete 100
    }
}



function Invoke-WhiskeyNuGetPush
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [string]
        $Path,

        [Parameter(Mandatory=$true)]
        [string]
        $Uri,

        [Parameter(Mandatory=$true)]
        [string]
        $ApiKey,

        [Parameter(Mandatory=$true)]
        [string]
        $NuGetPath
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    & $NuGetPath push $Path -Source $Uri -ApiKey $ApiKey

}


function Invoke-WhiskeyPipeline
{
    <#
    .SYNOPSIS
    Invokes Whiskey pipelines.
 
    .DESCRIPTION
    The `Invoke-WhiskeyPipeline` function runs the tasks in a pipeline. Pipelines are properties in a `whiskey.yml` under which one or more tasks are defined. For example, this `whiskey.yml` file:
 
        Build:
        - TaskOne
        - TaskTwo
        Publish:
        - TaskOne
        - Task
 
    Defines two pipelines: `Build` and `Publish`.
 
    .EXAMPLE
    Invoke-WhiskeyPipeline -Context $context -Name 'Build'
 
    Demonstrates how to run the tasks in a `Build` pipeline. The `$context` object is created by calling `New-WhiskeyContext`.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [object]
        # The current build context. Use the `New-WhiskeyContext` function to create a context object.
        $Context,

        [Parameter(Mandatory=$true)]
        [string]
        # The name of pipeline to run, e.g. `Build` would run all the tasks under a property named `Build`. Pipelines are properties in your `whiskey.yml` file that are lists of Whiskey tasks to run.
        $Name
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    $config = $Context.Configuration
    $Context.PipelineName = $Name

    if( -not $config.ContainsKey($Name) )
    {
        Stop-Whiskey -Context $Context -Message ('Pipeline ''{0}'' does not exist. Create a pipeline by defining a ''{0}'' property:
         
    {0}:
    - TASK_ONE
    - TASK_TWO
     
'
 -f $Name)
        return
    }

    $taskIdx = -1
    if( -not $config[$Name] )
    {
        Write-Warning -Message ('It looks like pipeline ''{0}'' doesn''t have any tasks.' -f $Context.ConfigurationPath)
        $config[$Name] = @()
    }

    foreach( $taskItem in $config[$Name] )
    {
        $taskIdx++

        $taskName,$taskParameter = ConvertTo-WhiskeyTask -InputObject $taskItem -ErrorAction Stop
        if( -not $taskName )
        {
            continue
        }

        $Context.TaskIndex = $taskIdx

        Invoke-WhiskeyTask -TaskContext $Context -Name $taskName -Parameter $taskParameter
    }
}


function Invoke-WhiskeyRobocopy
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [string]
        $Source,

        [Parameter(Mandatory=$true)]
        [string]
        $Destination,

        [string[]]
        $WhiteList,

        [string[]]
        $Exclude,

        [string]
        $LogPath
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    $numRobocopyThreads = Get-CimInstance -ClassName 'Win32_Processor' | Select-Object -ExpandProperty 'NumberOfLogicalProcessors' | Measure-Object -Sum | Select-Object -ExpandProperty 'Sum'
    $numRobocopyThreads *= 2

    $logParam = ''
    if ($LogPath)
    {
        $logParam = '/LOG:{0}' -f $LogPath
    }

    $excludeParam = $Exclude | ForEach-Object { '/XF' ; $_ ; '/XD' ; $_ }
    robocopy $Source $Destination '/PURGE' '/S' '/NP' '/R:0' '/NDL' '/NFL' '/NS' ('/MT:{0}' -f $numRobocopyThreads) $WhiteList $excludeParam $logParam
}


function Invoke-WhiskeyTask
{
    <#
    .SYNOPSIS
    Runs a Whiskey task.
 
    .DESCRIPTION
    The `Invoke-WhiskeyTask` function runs a Whiskey task.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [Whiskey.Context]
        # The context this task is operating in. Use `New-WhiskeyContext` to create context objects.
        $TaskContext,

        [Parameter(Mandatory=$true)]
        [string]
        # The name of the task.
        $Name,

        [Parameter(Mandatory=$true)]
        [hashtable]
        # The parameters/configuration to use to run the task.
        $Parameter
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    function Invoke-Event
    {
        param(
            $EventName,
            $Property
        )

        if( -not $events.ContainsKey($EventName) )
        {
            return
        }

        foreach( $commandName in $events[$EventName] )
        {
            Write-WhiskeyVerbose -Context $TaskContext -Message ''
            Write-WhiskeyVerbose -Context $TaskContext -Message ('[On{0}] {1}' -f $EventName,$commandName)
            $startedAt = Get-Date
            $result = 'FAILED'
            try
            {
                $TaskContext.Temp = Join-Path -Path $TaskContext.OutputDirectory -ChildPath ('Temp.{0}.On{1}.{2}' -f $Name,$EventName,[IO.Path]::GetRandomFileName())
                if( -not (Test-Path -Path $TaskContext.Temp -PathType Container) )
                {
                    New-Item -Path $TaskContext.Temp -ItemType 'Directory' -Force | Out-Null
                }
                & $commandName -TaskContext $TaskContext -TaskName $Name -TaskParameter $Property
                $result = 'COMPLETED'
            }
            finally
            {
                Remove-WhiskeyFileSystemItem -Path $TaskContext.Temp
                $endedAt = Get-Date
                $duration = $endedAt - $startedAt
                Write-WhiskeyVerbose -Context $TaskContext ('{0} {1} in {2}' -f (' ' * ($EventName.Length + 4)),$result,$duration)
                Write-WhiskeyVerbose -Context $TaskContext -Message ''
            }
        }
    }

    function Merge-Parameter
    {
        param(
            [hashtable]
            $SourceParameter,

            [hashtable]
            $TargetParameter
        )

        foreach( $key in $SourceParameter.Keys )
        {
            $sourceValue = $SourceParameter[$key]
            if( $TargetParameter.ContainsKey($key) )
            {
                $targetValue = $TargetParameter[$key]
                if( ($targetValue | Get-Member -Name 'Keys') -and ($sourceValue | Get-Member -Name 'Keys') )
                {
                    Merge-Parameter -SourceParameter $sourceValue -TargetParameter $targetValue
                }
                continue
            }

            $TargetParameter[$key] = $sourceValue
        }
    }

    function Get-RequiredTool
    {
        param(
            $CommandName
        )

        $cmd = Get-Command -Name $CommandName -ErrorAction Ignore
        if( -not $cmd -or -not (Get-Member -InputObject $cmd -Name 'ScriptBlock') )
        {
            return
        }

        $cmd.ScriptBlock.Attributes |
            Where-Object { $_ -is [Whiskey.RequiresToolAttribute] }
    }

    $knownTasks = Get-WhiskeyTask -Force

    $task = $knownTasks | Where-Object { $_.Name -eq $Name }

    if( -not $task )
    {
        $task = $knownTasks | Where-Object { $_.Aliases -contains $Name }
        $taskCount = ($task | Measure-Object).Count
        if( $taskCount -gt 1 )
        {
            Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Found {0} tasks with alias "{1}". Please update to use one of these task names: {2}.' -f $taskCount,$Name,(($task | Select-Object -ExpandProperty 'Name') -join ', '))
            return
        }
        if( $task -and $task.WarnWhenUsingAlias )
        {
            Write-Warning -Message ('Task "{0}" is an alias to task "{1}". Please update "{2}" to use the task''s actual name, "{1}", instead of the alias.' -f $Name,$task.Name,$TaskContext.ConfigurationPath)
        }
    }

    if( -not $task )
    {
        $knownTaskNames = $knownTasks | Select-Object -ExpandProperty 'Name' | Sort-Object
        throw ('{0}: {1}[{2}]: ''{3}'' task does not exist. Supported tasks are:{4} * {5}' -f $TaskContext.ConfigurationPath,$Name,$TaskContext.TaskIndex,$Name,[Environment]::NewLine,($knownTaskNames -join ('{0} * ' -f [Environment]::NewLine)))
    }

    $taskCount = ($task | Measure-Object).Count
    if( $taskCount -gt 1 )
    {
        Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Found {0} tasks named "{1}". We don''t know which one to use. Please make sure task names are unique.' -f $taskCount,$Name)
        return
    }

    $TaskContext.TaskName = $Name

    if( $task.Obsolete )
    {
        $message = 'The "{0}" task is obsolete and shouldn''t be used.' -f $task.Name
        if( $task.ObsoleteMessage )
        {
            $message = $task.ObsoleteMessage
        }
        Write-WhiskeyWarning -Context $TaskContext -Message $message
    }

    if( -not $task.Platform.HasFlag($CurrentPlatform) )
    {
        Write-Error -Message ('Unable to run task "{0}": it is only supported on the {1} platform(s) and we''re currently running on {2}.' -f $task.Name,$task.Platform,$CurrentPlatform) -ErrorAction Stop
        return
    }

    if( $TaskContext.TaskDefaults.ContainsKey( $Name ) )
    {
        Merge-Parameter -SourceParameter $TaskContext.TaskDefaults[$Name] -TargetParameter $Parameter
    }

    Resolve-WhiskeyVariable -Context $TaskContext -InputObject $Parameter | Out-Null

    [hashtable]$taskProperties = $Parameter.Clone()
    $commonProperties = @{}
    foreach( $commonPropertyName in @( 'OnlyBy', 'ExceptBy', 'OnlyOnBranch', 'ExceptOnBranch', 'OnlyDuring', 'ExceptDuring', 'WorkingDirectory', 'IfExists', 'UnlessExists', 'OnlyOnPlatform', 'ExceptOnPlatform' ) )
    {
        if ($taskProperties.ContainsKey($commonPropertyName))
        {
            $commonProperties[$commonPropertyName] = $taskProperties[$commonPropertyName]
            $taskProperties.Remove($commonPropertyName)
        }
    }

    $workingDirectory = $TaskContext.BuildRoot
    if( $Parameter['WorkingDirectory'] )
    {
        $workingDirectory = $Parameter['WorkingDirectory'] | Resolve-WhiskeyTaskPath -TaskContext $TaskContext -PropertyName 'WorkingDirectory'
    }

    $taskTempDirectory = ''
    $requiredTools = Get-RequiredTool -CommandName $task.CommandName
    $startedAt = Get-Date
    $result = 'FAILED'
    $currentDirectory = [IO.Directory]::GetCurrentDirectory()
    Push-Location -Path $workingDirectory
    [IO.Directory]::SetCurrentDirectory($workingDirectory)
    try
    {
        if( Test-WhiskeyTaskSkip -Context $TaskContext -Properties $commonProperties)
        {
            $result = 'SKIPPED'
            return
        }

        $inCleanMode = $TaskContext.ShouldClean
        if( $inCleanMode )
        {
            if( -not $task.SupportsClean )
            {
                Write-WhiskeyVerbose -Context $TaskContext -Message ('SupportsClean.{0} -ne Build.ShouldClean.{1}' -f $task.SupportsClean,$TaskContext.ShouldClean)
                $result = 'SKIPPED'
                return
            }
        }

        foreach( $requiredTool in $requiredTools )
        {
            Install-WhiskeyTool -ToolInfo $requiredTool `
                                -InstallRoot $TaskContext.BuildRoot `
                                -TaskParameter $taskProperties `
                                -InCleanMode:$inCleanMode `
                                -ErrorAction Stop
        }

        if( $TaskContext.ShouldInitialize -and -not $task.SupportsInitialize )
        {
            Write-WhiskeyVerbose -Context $TaskContext -Message ('SupportsInitialize.{0} -ne Build.ShouldInitialize.{1}' -f $task.SupportsInitialize,$TaskContext.ShouldInitialize)
            $result = 'SKIPPED'
            return
        }

        Invoke-Event -EventName 'BeforeTask' -Property $taskProperties
        Invoke-Event -EventName ('Before{0}Task' -f $Name) -Property $taskProperties

        Write-WhiskeyVerbose -Context $TaskContext -Message ''
        $startedAt = Get-Date
        $taskTempDirectory = Join-Path -Path $TaskContext.OutputDirectory -ChildPath ('Temp.{0}.{1}' -f $Name,[IO.Path]::GetRandomFileName())
        $TaskContext.Temp = $taskTempDirectory
        if( -not (Test-Path -Path $TaskContext.Temp -PathType Container) )
        {
            New-Item -Path $TaskContext.Temp -ItemType 'Directory' -Force | Out-Null
        }

        $parameter = Get-TaskParameter -Name $task.CommandName -TaskProperty $taskProperties -Context $TaskContext
        & $task.CommandName @parameter
        $result = 'COMPLETED'
    }
    finally
    {
        # Clean required tools *after* running the task since the task might need a required tool in order to do the cleaning (e.g. using Node to clean up installed modules)
        if( $TaskContext.ShouldClean )
        {
            foreach( $requiredTool in $requiredTools )
            {
                Uninstall-WhiskeyTool -InstallRoot $TaskContext.BuildRoot -Name $requiredTool.Name
            }
        }

        if( $taskTempDirectory -and (Test-Path -Path $taskTempDirectory -PathType Container) )
        {
            Remove-Item -Path $taskTempDirectory -Recurse -Force -ErrorAction Ignore
        }
        $endedAt = Get-Date
        $duration = $endedAt - $startedAt
        Write-WhiskeyVerbose -Context $TaskContext -Message ('{0} in {1}' -f $result,$duration)
        Write-WhiskeyVerbose -Context $TaskContext -Message ''
        [IO.Directory]::SetCurrentDirectory($currentDirectory)
        Pop-Location
    }

    Invoke-Event -EventName 'AfterTask' -Property $taskProperties
    Invoke-Event -EventName ('After{0}Task' -f $Name) -Property $taskProperties
}


function New-WhiskeyBuildMetadataObject
{
    [CmdletBinding()]
    [OutputType([Whiskey.BuildInfo])]
    param(
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    return New-Object -TypeName 'Whiskey.BuildInfo'
}



function New-WhiskeyContext
{
    <#
    .SYNOPSIS
    Creates a context object to use when running builds.
 
    .DESCRIPTION
    The `New-WhiskeyContext` function creates a `Whiskey.Context` object used when running builds. It:
 
    * Reads in the whiskey.yml file containing the build you want to run.
    * Creates a ".output" directory in the same directory as your whiskey.yml file for storing build output, logs, results, temp files, etc.
    * Reads build metadata created by the current build server (if being run by a build server).
    * Sets the version number to "0.0.0".
 
    ## Whiskey.Context
 
    The `Whiskey.Context` object has the following properties. ***Do not use any property not defined below.*** Also, these properties are ***read-only***. If you write to them, Bad Things (tm) could happen.
 
    * `BuildMetadata`: a `Whiskey.BuildInfo` object representing build metadata provided by the build server.
    * `BuildRoot`: a `System.IO.DirectoryInfo` object representing the directory the YAML configuration file is in.
    * `ByBuildServer`: a flag indicating if the build is being run by a build server.
    * `ByDeveloper`: a flag indicating if the build is being run by a developer.
    * `Environment`: the environment the build is running in.
    * `OutputDirectory`: a `System.IO.DirectoryInfo` object representing the path to a directory where build output, reports, etc. should be saved. This directory is created for you.
    * `ShouldClean`: a flag indicating if the current build is running in clean mode.
    * `ShouldInitialize`: a flag indicating if the current build is running in initialize mode.
    * `Temp`: the temporary work directory for the current task.
    * `Version`: a `Whiskey.BuildVersion` object representing version being built (see below).
 
    Any other property is considered private and may be removed, renamed, and/or reimplemented at our discretion without notice.
 
    ## Whiskey.BuildInfo
 
    The `Whiskey.BuildInfo` object has the following properties. ***Do not use any property not defined below.*** Also, these properties are ***read-only***. If you write to them, Bad Things (tm) could happen.
 
    * `BuildNumber`: the current build number. This comes from the build server. (If the build is being run by a developer, this is always "0".) It increments with every new build (or should). This number is unique only to the current build job.
    * `ScmBranch`: the branch name from which the current build is running.
    * `ScmCommitID`: the unique commit ID from which the current build is running. The commit ID distinguishes the current commit from all others in the source repository and is the same across copies of a repository.
 
    ## Whiskey.BuildVersion
 
    The `Whiskey.BuildVersion` object has the following properties. ***Do not use any property not defined below.*** Also, these properties are ***read-only***. If you write to them, Bad Things (tm) could happen.
 
    * `SemVer2`: the version currently being built.
    * `Version`: a `System.Version` object for the current build. Only major, minor, and patch/build numbers will be filled in.
    * `SemVer1`: a semver version 1 compatible version of the current build.
    * `SemVer2NoBuildMetadata`: the current version without any build metadata.
 
    .EXAMPLE
    New-WhiskeyContext -Path '.\whiskey.yml'
 
    Demonstrates how to create a context for a developer build.
    #>

    [CmdletBinding()]
    [OutputType([Whiskey.Context])]
    param(
        [Parameter(Mandatory=$true)]
        [string]
        # The environment you're building in.
        $Environment,

        [Parameter(Mandatory=$true)]
        [string]
        # The path to the `whiskey.yml` file that defines build settings and tasks.
        $ConfigurationPath,

        [string]
        # The place where downloaded tools should be cached. The default is the build root.
        $DownloadRoot
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    $ConfigurationPath = Resolve-Path -LiteralPath $ConfigurationPath -ErrorAction Ignore
    if( -not $ConfigurationPath )
    {
        throw ('Configuration file path ''{0}'' does not exist.' -f $PSBoundParameters['ConfigurationPath'])
    }

    $config = Import-WhiskeyYaml -Path $ConfigurationPath

    if( $config.ContainsKey('Build') -and $config.ContainsKey('BuildTasks') )
    {
        throw ('{0}: The configuration file contains both "Build" and the deprecated "BuildTasks" pipelines. Move all your build tasks under "Build" and remove the "BuildTasks" pipeline.' -f $ConfigurationPath)
    }

    $buildPipelineName = 'Build'
    if( $config.ContainsKey('BuildTasks') )
    {
        $buildPipelineName = 'BuildTasks'
        Write-Warning ('{0}: The default "BuildTasks" pipeline has been renamed to "Build". Backwards compatibility with "BuildTasks" will be removed in the next major version of Whiskey. Rename your "BuildTasks" pipeline to "Build".' -f $ConfigurationPath)
    }

    if( $config.ContainsKey('Publish') -and $config.ContainsKey('PublishTasks') )
    {
        throw ('{0}: The configuration file contains both "Publish" and the deprecated "PublishTasks" pipelines. Move all your publish tasks under "Publish" and remove the "PublishTasks" pipeline.' -f $ConfigurationPath)
    }

    if( $config.ContainsKey('PublishTasks') )
    {
        Write-Warning ('{0}: The default "PublishTasks" pipeline has been renamed to "Publish". Backwards compatibility with "PublishTasks" will be removed in the next major version of Whiskey. Rename your "PublishTasks" pipeline to "Publish".' -f $ConfigurationPath)
    }

    $buildRoot = $ConfigurationPath | Split-Path
    if( -not $DownloadRoot )
    {
        $DownloadRoot = $buildRoot
    }

    [Whiskey.BuildInfo]$buildMetadata = Get-WhiskeyBuildMetadata
    $publish = $false
    $byBuildServer = $buildMetadata.IsBuildServer
    $prereleaseInfo = ''
    if( $byBuildServer )
    {
        $branch = $buildMetadata.ScmBranch

        if( $config.ContainsKey( 'PublishOn' ) )
        {
            Write-Verbose -Message ('PublishOn')
            foreach( $publishWildcard in $config['PublishOn'] )
            {
                $publish = $branch -like $publishWildcard
                if( $publish )
                {
                    Write-Verbose -Message (' {0} -like {1}' -f $branch,$publishWildcard)
                    break
                }
                else
                {
                    Write-Verbose -Message (' {0} -notlike {1}' -f $branch,$publishWildcard)
                }
            }
        }
    }

    $versionTaskExists = $config[$buildPipelineName] |
                            Where-Object { $_ -and ($_ | Get-Member -Name 'Keys') } |
                            Where-Object { $_.Keys | Where-Object { $_ -eq 'Version' } }
    if( -not $versionTaskExists -and ($config.ContainsKey('PrereleaseMap') -or $config.ContainsKey('Version') -or $config.ContainsKey('VersionFrom')) )
    {
        Write-Warning ('{0}: The ''PrereleaseMap'', ''Version'', and ''VersionFrom'' properties are obsolete and will be removed in Whiskey 1.0. They were replaced with the ''Version'' task. Add a ''Version'' task as the first task in your build pipeline. If your current whiskey.yml file looks like this:
 
    Version: 1.2.3
 
    PrereleaseMap:
    - "alpha/*": alpha
    - "release/*": rc
 
add a Version task to your build pipeline that looks like this:
 
    Build:
    - Version:
        Version: 1.2.3
        Prerelease:
        - "alpha/*": alpha.$(WHISKEY_BUILD_NUMBER)
        - "release/*": rc.$(WHISKEY_BUILD_NUMBER)
 
You must add the ".$(WHISKEY_BUILD_NUMBER)" string to each prerelease version. Whiskey no longer automatically adds a prerelease version number for you.
 
If you use the "VersionFrom" property, your whiskey.yml file looks something like this:
 
    VersionFrom: Whiskey\Whiskey.psd1
 
Update it to look like this:
 
    Build:
    - Version:
        Path: Whiskey\Whiskey.psd1
 
Whiskey also no longer automatically adds build metadata to your version number. To preserve Whiskey''s old default build metadata, add a "Build" property to your new "Version" task that looks like this:
 
    Build:
    - Version:
        Version: 1.2.3
        Build: $(WHISKEY_SCM_BRANCH).$(WHISKEY_SCM_COMMIT_ID.Substring(0,7))
 
    '
 -f $ConfigurationPath)

        $versionTask = $null

        $versionTask = @{
                            Version = ('{0:yyyy.Mdd}.$(WHISKEY_BUILD_NUMBER)' -f (Get-Date))
                            Build = '$(WHISKEY_SCM_BRANCH).$(WHISKEY_SCM_COMMIT_ID)'
                        }

        if( $config['Version'] )
        {
            $versionTask['Version'] = $config['Version']
        }
        elseif( $config['VersionFrom'] )
        {
            $versionTask.Remove('Version')
            $versionTask['Path'] = $config['VersionFrom']
        }

        if( $config['PrereleaseMap'] )
        {
            $versionTask['Prerelease'] = $config['PrereleaseMap'] |
                                            ForEach-Object {
                                                if( -not ($_ | Get-Member 'Keys') )
                                                {
                                                    return $_
                                                }

                                                $newMap = @{ }
                                                foreach( $key in $_.Keys )
                                                {
                                                    $value = $_[$key]
                                                    $newMap[$key] = '{0}.$(WHISKEY_BUILD_NUMBER)' -f $value
                                                }
                                                $newMap
                                            }

        }

        if( $versionTask )
        {
            if( -not $config[$buildPipelineName] )
            {
                $config[$buildPipelineName] = @()
            }

            $config[$buildPipelineName] = & {
                                            @{
                                                Version = $versionTask
                                            }
                                            $config[$buildPipelineName]
                                    }
        }
    }

    $outputDirectory = Join-Path -Path $buildRoot -ChildPath '.output'
    if( -not (Test-Path -Path $outputDirectory -PathType Container) )
    {
        New-Item -Path $outputDirectory -ItemType 'Directory' -Force | Out-Null
    }

    $context = New-WhiskeyContextObject
    $context.BuildRoot = $buildRoot
    $runBy = [Whiskey.RunBy]::Developer
    if( $byBuildServer )
    {
        $runBy = [Whiskey.RunBy]::BuildServer
    }
    $context.RunBy = $runBy
    $context.BuildMetadata = $buildMetadata
    $context.Configuration = $config
    $context.ConfigurationPath = $ConfigurationPath
    $context.DownloadRoot = $DownloadRoot
    $context.Environment = $Environment
    $context.OutputDirectory = $outputDirectory
    $context.Publish = $publish
    $context.RunMode = [Whiskey.RunMode]::Build

    if( $config['Variable'] )
    {
        Write-Error -Message ('{0}: The ''Variable'' property is no longer supported. Use the `SetVariable` task instead. Move your `Variable` property (and values) into your `Build` pipeline as the first task. Rename `Variable` to `SetVariable`.' -f $ConfigurationPath) -ErrorAction Stop
    }

    if( $versionTaskExists )
    {
        $context.Version = New-WhiskeyVersionObject -SemVer '0.0.0'
    }
    else
    {
        Write-Warning ('Whiskey''s default, date-based version number is OBSOLETE. Beginning with Whiskey 1.0, the default Whiskey version number will be 0.0.0. Use the Version task to set your own custom version. For example, this Version task preserves the existing behavior:
 
    Build
    - Version:
        Version: $(WHISKEY_BUILD_STARTED_AT.ToString(''yyyy.Mdd'')).$(WHISKEY_BUILD_NUMBER)
        Build: $(WHISKEY_SCM_BRANCH).$(WHISKEY_SCM_COMMIT_ID)
 '
)
        $rawVersion = '{0:yyyy.Mdd}.{1}' -f (Get-Date),$context.BuildMetadata.BuildNumber
        if( $context.ByBuildServer )
        {
            $branch = $buildMetadata.ScmBranch
            $branch = $branch -replace '[^A-Za-z0-9-]','-'
            $commitID = $buildMetadata.ScmCommitID.Substring(0,7)
            $buildInfo = '{0}.{1}.{2}' -f $buildMetadata.BuildNumber,$branch,$commitID
            $rawVersion = '{0}+{1}' -f $rawVersion,$buildInfo
        }
        $context.Version = New-WhiskeyVersionObject -SemVer $rawVersion
    }

    return $context
}




function New-WhiskeyContextObject
{
    [CmdletBinding()]
    [OutputType([Whiskey.Context])]
    param(
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    return New-Object -TypeName 'Whiskey.Context'
}


function New-WhiskeyVersionObject
{
    [CmdletBinding()]
    [OutputType([Whiskey.BuildVersion])]
    param(
        [SemVersion.SemanticVersion]
        $SemVer
    )

    $whiskeyVersion = New-Object -TypeName 'Whiskey.BuildVersion'
    
    if( $SemVer )
    {
        $major = $SemVer.Major
        $minor = $SemVer.Minor
        $patch = $SemVer.Patch
        $prerelease = $SemVer.Prerelease
        $build = $SemVer.Build

        $version = New-Object -TypeName 'Version' -ArgumentList $major,$minor,$patch
        $semVersionNoBuild = New-Object -TypeName 'SemVersion.SemanticVersion' -ArgumentList $major,$minor,$patch
        $semVersionV1 = New-Object -TypeName 'SemVersion.SemanticVersion' -ArgumentList $major,$minor,$patch
        if( $prerelease )
        {
            $semVersionNoBuild = New-Object -TypeName 'SemVersion.SemanticVersion' -ArgumentList $major,$minor,$patch,$prerelease
            $semVersionV1Prerelease = $prerelease -replace '[^A-Za-z0-90]',''
            $semVersionV1 = New-Object -TypeName 'SemVersion.SemanticVersion' -ArgumentList $major,$minor,$patch,$semVersionV1Prerelease
        }

        $whiskeyVersion.Version = $version
        $whiskeyVersion.SemVer2 = $SemVer
        $whiskeyVersion.SemVer2NoBuildMetadata = $semVersionNoBuild
        $whiskeyVersion.SemVer1 = $semVersionV1
    }

    return $whiskeyVersion
}



function Publish-WhiskeyPesterTestResult
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [string]
        # The path to the Pester test resut.
        $Path
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    if( -not (Test-Path -Path 'env:APPVEYOR_JOB_ID') )
    {
        return
    }

    $webClient = New-Object 'Net.WebClient'
    $uploadUri = 'https://ci.appveyor.com/api/testresults/nunit/{0}' -f $env:APPVEYOR_JOB_ID
    Resolve-Path -Path $Path -ErrorAction Stop |
        Select-Object -ExpandProperty 'ProviderPath' |
        ForEach-Object { 
            $resultPath = $_
            Write-Verbose -Message ('Uploading Pester test result file ''{0}'' to AppVeyor at ''{1}''.' -f $resultPath,$uploadUri)
            $webClient.UploadFile($uploadUri, $resultPath)
        }
}


function Register-WhiskeyEvent
{
    <#
    .SYNOPSIS
    Registers a command to call when specific events happen during a build.
 
    .DESCRIPTION
    The `Register-WhiskeyEvent` function registers a command to run when a specific event happens during a build. Supported events are:
     
    * `BeforeTask` which runs before each task
    * `AfterTask`, which runs after each task
 
    `BeforeTask` and `AfterTask` event handlers must have the following parameters:
 
        function Invoke-WhiskeyTaskEvent
        {
            param(
                [Parameter(Mandatory=$true)]
                [object]
                $TaskContext,
 
                [Parameter(Mandatory=$true)]
                [string]
                $TaskName,
 
                [Parameter(Mandatory=$true)]
                [hashtable]
                $TaskParameter
            )
        }
 
    To stop a build while handling an event, call the `Stop-WhiskeyTask` function.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [string]
        # The name of the command to run during the event.
        $CommandName,

        [Parameter(Mandatory=$true)]
        [string]
        [ValidateSet('BeforeTask','AfterTask')]
        # When the command should be run; what events does it respond to?
        $Event,

        [string]
        # Only fire the event for a specific task.
        $TaskName
    )

    Set-StrictMode -Version 'Latest'

    $eventName = $Event
    if( $TaskName )
    {
        $eventType = $Event -replace 'Task$',''
        $eventName = '{0}{1}Task' -f $eventType,$TaskName
    }

    if( -not $events[$eventName] )
    {
        $events[$eventName] = New-Object -TypeName 'Collections.Generic.List[string]'
    }

    $events[$eventName].Add( $CommandName )
}


function Remove-WhiskeyFileSystemItem
{
    <#
    .SYNOPSIS
    Deletes a file or directory.
 
    .DESCRIPTION
    The `Remove-WhiskeyFileSystemItem` deletes files and directories. Directories are deleted recursively. On Windows, this function uses robocopy to delete directories, since it can handle files/directories whose paths are longer than the maximum 260 characters.
 
    If the file or directory doesn't exist, nothing happens.
 
    The path to delete should be absolute or relative to the current working directory.
 
    This function won't fail a build. If you want it to fail a build, pass the `-ErrorAction Stop` parameter.
 
    .EXAMPLE
    Remove-WhiskeyFileSystemItem -Path 'C:\some\file'
 
    Demonstrates how to delete a file.
 
    .EXAMPLE
    Remove-WhiskeyFilesystemItem -Path 'C:\project\node_modules'
 
    Demonstrates how to delete a directory.
 
    .EXAMPLE
    Remove-WhiskeyFileSystemItem -Path 'C:\project\node_modules' -ErrorAction Stop
 
    Demonstrates how to fail a build if the delete fails.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]
        $Path
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    if( (Test-Path -Path $Path -PathType Leaf) )
    {
        Remove-Item -Path $Path -Force
    }
    elseif( (Test-Path -Path $Path -PathType Container) )
    {
        if( $IsWindows )
        {
            $logPath = Join-Path -Path ([IO.Path]::GetTempPath()) -ChildPath ('whiskey.robocopy.{0}.log' -f ([IO.Path]::GetRandomFileName()))
            $emptyDir = Join-Path -Path ([IO.Path]::GetTempPath()) -ChildPath ([IO.Path]::GetRandomFileName())
            $deleteLog = $true
            New-Item -Path $emptyDir -ItemType 'Directory' | Out-Null
            try
            {
                Invoke-WhiskeyRobocopy -Source $emptyDir -Destination $Path -LogPath $logPath | Out-Null
                if( $LASTEXITCODE -ge 8 )
                {
                    $deleteLog = $false
                    Write-Error -Message ('Failed to remove directory "{0}". See "{1}" for more information.' -f $Path,$logPath)
                    return
                }
                Remove-Item -Path $Path -Recurse -Force
            }
            finally
            {
                if( $deleteLog )
                {
                    Remove-Item -Path $logPath -ErrorAction Ignore -Force
                }
                Remove-Item -Path $emptyDir -Recurse -Force
            }
        }
        else
        {
            Remove-Item -Path $Path -Recurse -Force
        }
    }
}


function Resolve-WhiskeyDotNetSdkVersion
{
    <#
    .SYNOPSIS
    Searches for a version of the .NET Core SDK to ensure it exists and returns the resolved version.
 
    .DESCRIPTION
    The `Resolve-WhiskeyDotNetSdkVersion` function ensures a given version is a valid released version of the .NET Core SDK. By default, the function will return the latest LTS version of the SDK. If a `Version` number is given then that version is compared against the list of released SDK versions to ensure the given version is valid. If no valid version is found matching `Version`, then an error is written and nothing is returned.
 
    .EXAMPLE
    Resolve-WhiskeyDotNetSdkVersion -LatestLTS
 
    Demonstrates returning the latest LTS version of the .NET Core SDK.
 
    .EXAMPLE
    Resolve-WhiskeyDotNetSdkVersion -Version '2.1.2'
 
    Demonstrates ensuring that version '2.1.2' is a valid released version of the .NET Core SDK.
 
    .EXAMPLE
    Resolve-WhiskeyDotNetSdkVersion -Version '2.*'
 
    Demonstrates resolving the latest '2.x.x' version of the .NET Core SDK.
    #>

    [CmdletBinding(DefaultParameterSetName='LatestLTS')]
    param(
        [Parameter(ParameterSetName='LatestLTS')]
        # Returns the latest LTS version of the .NET Core SDK.
        [switch]$LatestLTS,

        [Parameter(Mandatory, ParameterSetName='Version')]
        # Version of the .NET Core SDK to search for and resolve. Accepts wildcards.
        [string]$Version
    )

    Set-StrictMode -version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    if ($Version)
    {
        $releasesIndexUri = 'https://dotnetcli.blob.core.windows.net/dotnet/release-metadata/releases-index.json'
        $releasesIndex =
            Invoke-RestMethod -Uri $releasesIndexUri -ErrorAction Stop |
            Select-Object -ExpandProperty 'releases-index' |
            Where-Object { [Version]::TryParse($_.'channel-version', [ref]$null) } |
            ForEach-Object {
                $_.'channel-version' = [Version]$_.'channel-version'
                $_
            } |
            Sort-Object -Property 'channel-version' -Descending

        $Version -match '^\d+\.(?:\d+|\*)|^\*' | Out-Null
        $matcher = $Matches[0]

        $release = $releasesIndex | Where-Object { $_.'channel-version' -like $matcher } | Select-Object -First 1
        if (-not $release)
        {
            Write-Error -Message ('.NET Core release matching "{0}" could not be found in "{1}"' -f $matcher, $releasesIndexUri)
            return
        }

        $releasesJsonUri = $release | Select-Object -ExpandProperty 'releases.json'
        Write-Verbose -Message ('[{0}] Resolving .NET Core SDK version "{1}" against known released versions at: "{2}"' -f $MyInvocation.MyCommand,$Version,$releasesJsonUri)

        $releasesJson = Invoke-RestMethod -Uri $releasesJsonUri -ErrorAction Stop

        $sdkVersions = & {
            $releasesJson.releases |
                Where-Object { $_ | Get-Member -Name 'sdk' } |
                Select-Object -ExpandProperty 'sdk' |
                Select-Object -ExpandProperty 'version'

            $releasesJson.releases |
                Where-Object { $_ | Get-Member -Name 'sdks' } |
                Select-Object -ExpandProperty 'sdks' |
                Select-Object -ExpandProperty 'version'
        }

        $resolvedVersion =
            $sdkVersions |
            ForEach-Object { $_ -as [Version] } |
            Where-Object { $_ -like $Version } |
            Sort-Object -Descending |
            Select-Object -First 1

        if (-not $resolvedVersion)
        {
            Write-Error -Message ('A released version of the .NET Core SDK matching "{0}" could not be found in "{1}"' -f $Version, $releasesJsonUri)
            return
        }

        Write-Verbose -Message ('[{0}] SDK version "{1}" resolved to "{2}"' -f $MyInvocation.MyCommand,$Version,$resolvedVersion)
    }
    else
    {
        $latestLTSVersionUri = 'https://dotnetcli.blob.core.windows.net/dotnet/Sdk/LTS/latest.version'

        Write-Verbose -Message ('[{0}] Resolving latest LTS version of .NET Core SDK from: "{1}"' -f $MyInvocation.MyCommand,$latestLTSVersionUri)
        $latestLTSVersion = Invoke-RestMethod -Uri $latestLTSVersionUri -ErrorAction Stop

        if ($latestLTSVersion -match '(\d+\.\d+\.\d+)')
        {
            $resolvedVersion = $Matches[1]
        }
        else
        {
            Write-Error -Message ('Could not retrieve the latest LTS version of the .NET Core SDK. "{0}" returned:{1}{2}' -f $latestLTSVersionUri,[Environment]::NewLine,$latestLTSVersion)
            return
        }

        Write-Verbose -Message ('[{0}] Latest LTS version resolved as: "{1}"' -f $MyInvocation.MyCommand,$resolvedVersion)
    }

    return $resolvedVersion
}



function Resolve-WhiskeyNodeModulePath
{
    <#
    .SYNOPSIS
    Gets the path to Node module's directory.
 
    .DESCRIPTION
    The `Resolve-WhiskeyNodeModulePath` resolves the path to a Node modules's directory. Pass the name of the module to the `Name` parameter. Pass the path to the build root to the `BuildRootPath` (this is usually where the package.json file is). The function will return the path to the Node module's directory in the local "node_modules" directory. Whiskey installs a private copy of Node for you into a ".node" directory in the build root. If you want to get the path to a global module from this private location, use the `-Global` switch.
     
    To get the Node module's directory from an arbitrary directory where Node is installed, pass the install directory to the `NodeRootPath` directory. This function handles the different locations of the "node_modules" directory across/between operating systems.
 
    If the Node module isn't installed, you'll get an error and nothing will be returned.
 
    .EXAMPLE
    Resolve-WhiskeyNodeModulePath -Name 'npm' -NodeRootPath $pathToNodeInstallRoot
 
    Demonstrates how to get the path to the `npm' module's directory from the "node_modules" directory from a directory where Node is installed, given by the `$pathToInstallRoot` variable.
 
    .EXAMPLE
    Resolve-WhiskeyNodeModulePath -Name 'npm' -BuildRootPath $TaskContext.BuildRoot
 
    Demonstrates how to get the path to a Node module's directory where Node installs a local copy. In this case, `Join-Path -Path $TaskContext.BuildRoot -ChildPath 'node_modules\npm'` would be returned (if it exists).
 
    .EXAMPLE
    Resolve-WhiskeyNodeModulePath -Name 'npm' -BuildRootPath $TaskContext.BuildRoot -Global
 
    Demonstrates how to get the path to a globally installed Node module's directory. Whiskey installs a private copy of Node into a ".node" directory in the build root, so this example would return a path to the module in that directory (if it exists). That path can be different between operating systems.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]
        # The name of the Node module whose path to get.
        $Name,

        [Parameter(Mandatory,ParameterSetName='FromBuildRoot')]
        [string]
        # The path to the build root. This will return the path to Node modules's directory from the "node_modules" directory in the build root. If you want the path to a global node module, installed in the local Node directory Whiskey installs in the repository, use the `-Global` switch.
        $BuildRootPath,

        [Parameter(ParameterSetName='FromBuildRoot')]
        [Switch]
        # Get the path to a Node module in the global "node_modules" directory. The default is to get the path to the copy in the local node_modules directory.
        $Global,

        [Parameter(Mandatory,ParameterSetName='FromNodeRoot')]
        [string]
        # The path to the root of a Node package, as downloaded and expanded from the Node.js project.
        $NodeRootPath
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    if( $PSCmdlet.ParameterSetName -eq 'FromBuildRoot' )
    {
        if( $Global )
        {
            return (Resolve-WhiskeyNodeModulePath -NodeRootPath (Join-Path -Path $BuildRootPath -ChildPath '.node') -Name $Name)
        }

        return (Resolve-WhiskeyNodeModulePath -NodeRootPath $BuildRootPath -Name $Name)
    }

    $nodeModulePath = & {
                            Join-Path -Path $NodeRootPath -ChildPath 'lib/node_modules'
                            Join-Path -Path $NodeRootPath -ChildPath 'node_modules'
                        } |
                        ForEach-Object { Join-Path -Path $_ -ChildPath $Name } |
                        Where-Object { Test-Path -Path $_ -PathType Container } |
                        Select-Object -First 1 |
                        Resolve-Path |
                        Select-Object -ExpandProperty 'ProviderPath'

    if( -not $nodeModulePath )
    {
        Write-Error -Message ('Node module "{0}" directory doesn''t exist in "{1}".' -f $Name,$NodeRootPath) -ErrorAction $ErrorActionPreference
        return
    }

    return $nodeModulePath
}


function Resolve-WhiskeyNodePath
{
    <#
    .SYNOPSIS
    Gets the path to the Node executable.
 
    .DESCRIPTION
    The `Resolve-WhiskeyNodePath` resolves the path to the Node executable in a cross-platform manner. The path/name of the Node executable is different on different operating systems. Pass the path to the root directory where Node is installed to the `NodeRootPath` parameter.
 
    If you want the path to the local version of Node that Whiskey installs for tasks that need it, pass the build root path to the `BuildRootPath` parameter.
 
    Returns the full path to the Node executable. If one isn't found, writes an error and returns nothing.
 
    .EXAMPLE
    Resolve-WhiskeyNodePath -NodeRootPath $pathToNodeInstallRoot
 
    Demonstrates how to get the path to the Node executable when the path to the root Node directory is in the `$pathToInstallRoot` variable.
 
    .EXAMPLE
    Resolve-WhiskeyNodePath -BuildRootPath $TaskContext.BuildRoot
 
    Demonstrates how to get the path to the Node executable in the directory where Whiskey installs it.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory,ParameterSetName='FromBuildRoot')]
        [string]
        # The path to the build root. This will return the path to Node where Whiskey installs a local copy.
        $BuildRootPath,

        [Parameter(Mandatory,ParameterSetName='FromNodeRoot')]
        [string]
        # The path to the root of an Node package, as downloaded and expanded from the Node.js download page.
        $NodeRootPath
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    if( $PSCmdlet.ParameterSetName -eq 'FromBuildRoot' )
    {
        return (Resolve-WhiskeyNodePath -NodeRootPath (Join-Path -Path $BuildRootPath -ChildPath '.node'))
    }

    $nodePath = & {
                        Join-Path -Path $NodeRootPath -ChildPath 'bin/node'
                        Join-Path -Path $NodeRootPath -ChildPath 'node.exe'
                } |
                ForEach-Object {
                    Write-Debug -Message ('Looking for Node executable at "{0}".' -f $_)
                    $_
                } |
                Where-Object { Test-Path -Path $_ -PathType Leaf } |
                Select-Object -First 1 |
                Resolve-Path |
                Select-Object -ExpandProperty 'ProviderPath'

    if( -not $nodePath )
    {
        Write-Error -Message ('Node executable doesn''t exist in "{0}".' -f $NodeRootPath) -ErrorAction $ErrorActionPreference
        return
    }

    Write-Debug -Message ('Found Node executable at "{0}".' -f $nodePath)
    return $nodePath
}

function Resolve-WhiskeyNuGetPackageVersion
{
    [CmdletBinding()]
    param(

        [Parameter(Mandatory=$true)]
        [string]
        # The name of the NuGet package to download.
        $NuGetPackageName,

        [string]
        # The version of the package to download. Must be a three part number, i.e. it must have a MAJOR, MINOR, and BUILD number.
        $Version,

        [string]
        $NugetPath = ($whiskeyNuGetExePath)
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    if( -not $Version )
    {
        Set-Item -Path 'env:EnableNuGetPackageRestore' -Value 'true'
        $NuGetPackage = Invoke-Command -NoNewScope -ScriptBlock {
            & $NugetPath list ('packageid:{0}' -f $NuGetPackageName)
        }
        $Version = $NuGetPackage |
            Where-Object { $_ -match $NuGetPackageName } |
            Where-Object { $_ -match ' (\d+\.\d+\.\d+.*)' } |
            ForEach-Object { $Matches[1] } |
            Select-Object -First 1

        if( -not $Version )
        {
            Write-Error ("Unable to find latest version of package '{0}'." -f $NuGetPackageName)
            return
        }
    }
    elseif( [Management.Automation.WildcardPattern]::ContainsWildcardCharacters($version) )
    {
        Write-Error "Wildcards are not allowed for NuGet packages yet because of a bug in the nuget.org search API (https://github.com/NuGet/NuGetGallery/issues/3274)."
        return
    }
    return $Version
}


function Resolve-WhiskeyPowerShellModule
{
    <#
    .SYNOPSIS
    Searches for a PowerShell module using PowerShellGet to ensure it exists and returns the resulting object from PowerShellGet.
 
    .DESCRIPTION
    The `Resolve-WhiskeyPowerShellModule` function takes a `Name` of a PowerShell module and uses PowerShellGet's `Find-Module` cmdlet to search for the module. If the module is found, the object from `Find-Module` describing the module is returned. If no module is found, an error is written and nothing is returned. If the module is found in multiple PowerShellGet repositories, only the first one from `Find-Module` is returned.
 
    If a `Version` is specified then this function will search for that version of the module from all versions returned from `Find-Module`. If the version cannot be found, an error is written and nothing is returned.
 
    `Version` supports wildcard patterns.
 
    .EXAMPLE
    Resolve-WhiskeyPowerShellModule -Name 'Pester'
 
    Demonstrates getting the module info on the latest version of the Pester module.
 
    .EXAMPLE
    Resolve-WhiskeyPowerShellModule -Name 'Pester' -Version '4.*'
 
    Demonstrates getting the module info on the latest '4.X' version of the Pester module.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        # The name of the PowerShell module.
        $Name,

        [string]
        # The version of the PowerShell module to search for. Must be a three part number, i.e. it must have a MAJOR, MINOR, and BUILD number.
        $Version
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    Import-WhiskeyPowerShellModule -Name 'PackageManagement', 'PowerShellGet'

    Get-PackageProvider -Name 'NuGet' -ForceBootstrap | Out-Null

    if( $Version )
    {
        $atVersionString = ' at version {0}' -f $Version

        if( -not [Management.Automation.WildcardPattern]::ContainsWildcardCharacters($version) )
        {
            $tempVersion = [Version]$Version
            if( $TempVersion -and ($TempVersion.Build -lt 0) )
            {
                $Version = [version]('{0}.{1}.0' -f $TempVersion.Major, $TempVersion.Minor)
            }
        }

        $module = Find-Module -Name $Name -AllVersions |
            Where-Object { $_.Version.ToString() -like $Version } |
            Sort-Object -Property 'Version' -Descending
    }
    else
    {
        $atVersionString = ''
        $module = Find-Module -Name $Name -ErrorAction Ignore
    }

    if( -not $module )
    {
        Write-Error -Message ('Failed to find module {0}{1} module on the PowerShell Gallery. You can browse the PowerShell Gallery at https://www.powershellgallery.com/' -f $Name, $atVersionString)
        return
    }

    return $module | Select-Object -First 1
}



function Resolve-WhiskeyTaskPath
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [object]
        # An object that holds context about the current build and executing task.
        $TaskContext,

        [Parameter(Mandatory=$true,ValueFromPipeline=$true)]
        [string]
        $Path,

        [Parameter(Mandatory=$true)]
        [string]
        $PropertyName,

        [string]
        # The root directory to use when resolving paths. The default is to use the `$TaskContext.BuildRoot` directory. Each path must be relative to this path.
        $ParentPath,

        [Switch]
        # Create the path if it doesn't exist. By default, the path will be created as a directory. To create the path as a file, pass `File` to the `PathType` parameter.
        $Force,

        [string]
        [ValidateSet('Directory','File')]
        # The type of item to create when using the `Force` parameter to create paths that don't exist. The default is to create the path as a directory. Pass `File` to create the path as a file.
        $PathType = 'Directory'
    )

    begin
    {
        Set-StrictMode -Version 'Latest'

        $pathIdx = -1
    }

    process
    {
        Set-StrictMode -Version 'Latest'

        $pathIdx++

        $originalPath = $Path
        if( [IO.Path]::IsPathRooted($Path) )
        {
            Stop-WhiskeyTask -TaskContext $TaskContext -Message ('{0}[{1}] ''{2}'' is absolute but must be relative to the ''{3}'' file.' -f $PropertyName,$pathIdx,$Path,$TaskContext.ConfigurationPath)
            return
        }

        if( -not $ParentPath )
        {
            $ParentPath = $TaskContext.BuildRoot
        }

        $Path = Join-Path -Path $ParentPath -ChildPath $Path
        if( -not (Test-Path -Path $Path) )
        {
            if( $Force )
            {
                New-Item -Path $Path -ItemType $PathType -Force | Out-String | Write-Debug
            }
            else
            {
                if( $ErrorActionPreference -ne [Management.Automation.ActionPreference]::Ignore )
                {
                    Stop-WhiskeyTask -TaskContext $TaskContext -Message ('{0}[{1}] "{2}" does not exist.' -f $PropertyName,$pathIdx,$Path)
                }
                return
            }
        }

        $message = 'Resolve {0} ->' -f $originalPath
        $prefix = ' ' * ($message.Length - 3)
        Write-Debug -Message $message
        Resolve-Path -Path $Path | 
            Select-Object -ExpandProperty 'ProviderPath' |
            ForEach-Object { 
                Write-Debug -Message ('{0} -> {1}' -f $prefix,$_)
                $_
            }
    }

    end
    {
    }
}





function Resolve-WhiskeyVariable
{
    <#
    .SYNOPSIS
    Replaces any variables in a string to their values.
 
    .DESCRIPTION
    The `Resolve-WhiskeyVariable` function replaces any variables in strings, arrays, or hashtables with their values. Variables have the format `$(VARIABLE_NAME)`. Variables are expanded in each item of an array. Variables are expanded in each value of a hashtable. If an array or hashtable contains an array or hashtable, variables are expanded in those objects as well, i.e. `Resolve-WhiskeyVariable` recursivelye expands variables in all arrays and hashtables.
     
    You can add variables to replace via the `Add-WhiskeyVariable` function. If a variable doesn't exist, environment variables are used. If a variable has the same name as an environment variable, the variable value is used instead of the environment variable's value. If no variable or environment variable is found, `Resolve-WhiskeyVariable` will write an error and return the origin string.
 
    See the `about_Whiskey_Variables` help topic for a list of variables.
 
    .EXAMPLE
    '$(COMPUTERNAME)' | Resolve-WhiskeyVariable
 
    Demonstrates that you can use environment variable as variables. In this case, `Resolve-WhiskeyVariable` would return the name of the current computer.
 
    .EXAMPLE
    @( '$(VARIABLE)', 4, @{ 'Key' = '$(VARIABLE') } ) | Resolve-WhiskeyVariable
 
    Demonstrates how to replace all the variables in an array. Any value of the array that isn't a string is ignored. Any hashtable in the array will have any variables in its values replaced. In this example, if the value of `VARIABLE` is 'Whiskey`, `Resolve-WhiskeyVariable` would return:
 
        @(
            'Whiskey',
            4,
            @{
                Key = 'Whiskey'
            }
        )
 
    .EXAMPLE
    @{ 'Key' = '$(Variable)'; 'Array' = @( '$(VARIABLE)', 4 ) 'Integer' = 4; } | Resolve-WhiskeyVariable
 
    Demonstrates that `Resolve-WhiskeyVariable` searches hashtable values and replaces any variables in any strings it finds. If the value of `VARIABLE` is set to `Whiskey`, then the code in this example would return:
 
        @{
            'Key' = 'Whiskey';
            'Array' = @(
                            'Whiskey',
                            4
                      );
            'Integer' = 4;
        }
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true,ValueFromPipeline=$true)]
        [AllowNull()]
        [object]
        # The object on which to perform variable replacement/substitution. If the value is a string, all variables in the string are replaced with their values.
        #
        # If the value is an array, variable expansion is done on each item in the array.
        #
        # If the value is a hashtable, variable replcement is done on each value of the hashtable.
        #
        # Variable expansion is performed on any arrays and hashtables found in other arrays and hashtables, i.e. arrays and hashtables are searched recursively.
        $InputObject,

        [Parameter(Mandatory=$true)]
        [Whiskey.Context]
        # The context of the current build. Necessary to lookup any variables.
        $Context
    )

    begin
    {
        Set-StrictMode -Version 'Latest'
        Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

        $version = $Context.Version
        $buildInfo = $Context.BuildMetadata;
        $sem1Version = ''
        if( $version.SemVer1 )
        {
            $sem1Version = '{0}.{1}.{2}' -f $version.SemVer1.Major,$version.SemVer1.Minor,$version.SemVer1.Patch
        }

        $sem2Version = ''
        if( $version.SemVer2 )
        {
            $sem2Version = '{0}.{1}.{2}' -f $version.SemVer2.Major,$version.SemVer2.Minor,$version.SemVer2.Patch
        }

        $wellKnownVariables = @{
                                    'WHISKEY_BUILD_ID' = $buildInfo.BuildID;
                                    'WHISKEY_BUILD_NUMBER' = $buildInfo.BuildNumber;
                                    'WHISKEY_BUILD_ROOT' = $Context.BuildRoot;
                                    'WHISKEY_BUILD_SERVER_NAME' = $buildInfo.BuildServer;
                                    'WHISKEY_BUILD_STARTED_AT' = $Context.StartedAt;
                                    'WHISKEY_BUILD_URI' = $buildInfo.BuildUri;
                                    'WHISKEY_ENVIRONMENT' = $Context.Environment;
                                    'WHISKEY_JOB_URI' = $buildInfo.JobUri;
                                    'WHISKEY_MSBUILD_CONFIGURATION' = (Get-WhiskeyMSBuildConfiguration -Context $Context);
                                    'WHISKEY_OUTPUT_DIRECTORY' = $Context.OutputDirectory;
                                    'WHISKEY_PIPELINE_NAME' = $Context.PipelineName;
                                    'WHISKEY_SCM_BRANCH' = $buildInfo.ScmBranch;
                                    'WHISKEY_SCM_COMMIT_ID' = $buildInfo.ScmCommitID;
                                    'WHISKEY_SCM_URI' = $buildInfo.ScmUri;
                                    'WHISKEY_SEMVER1' = $version.SemVer1;
                                    'WHISKEY_SEMVER1_VERSION' = $sem1Version;
                                    'WHISKEY_SEMVER2' = $version.SemVer2;
                                    'WHISKEY_SEMVER2_NO_BUILD_METADATA' = $version.SemVer2NoBuildMetadata;
                                    'WHISKEY_SEMVER2_VERSION' = $sem2Version;
                                    'WHISKEY_TASK_NAME' = $Context.TaskName;
                                    'WHISKEY_TEMP_DIRECTORY' = (Get-Item -Path ([IO.Path]::GetTempPath()));
                                    'WHISKEY_TASK_TEMP_DIRECTORY' = $Context.Temp;
                                    'WHISKEY_VERSION' = $version.Version;
                                }
    }

    process
    {
        Set-StrictMode -Version 'Latest'
        Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

        if( $InputObject -eq $null )
        {
            return $InputObject
        }

        if( (Get-Member -Name 'Keys' -InputObject $InputObject) )
        {
            $newValues = @{ }
            $toRemove = New-Object 'Collections.Generic.List[string]'
            # Can't modify a collection while enumerating it.
            foreach( $key in $InputObject.Keys )
            {
                $newKey = $key | Resolve-WhiskeyVariable -Context $Context  
                if( $newKey -ne $key )
                {
                    $toRemove.Add($key)
                }
                $newValues[$newKey] = Resolve-WhiskeyVariable -Context $Context -InputObject $InputObject[$key]
            }
            foreach( $key in $newValues.Keys )
            {
                $InputObject[$key] = $newValues[$key]
            }
            $toRemove | ForEach-Object { $InputObject.Remove($_) } | Out-Null
            return $InputObject
        }

        if( (Get-Member -Name 'Count' -InputObject $InputObject) )
        {
            for( $idx = 0; $idx -lt $InputObject.Count; ++$idx )
            {
                $InputObject[$idx] = Resolve-WhiskeyVariable -Context $Context -InputObject $InputObject[$idx]
            }
            return ,$InputObject
        }

        $startAt = 0
        $haystack = $InputObject.ToString()
        do
        {
            # Parse the variable expression, everything between $( and )
            $needleStart = $haystack.IndexOf('$(',$startAt)
            if( $needleStart -lt 0 )
            {
                break
            }
            elseif( $needleStart -gt 0 )
            {
                if( $haystack[$needleStart - 1] -eq '$' )
                {
                    $haystack = $haystack.Remove($needleStart - 1, 1)
                    $startAt = $needleStart
                    continue
                }
            }

            # Variable expressions can contain method calls, which begin and end with parenthesis, so
            # make sure you don't treat the close parenthesis of a method call as the close parenthesis
            # to the current variable expression.
            $needleEnd = $needleStart + 2
            $depth = 0
            while( $needleEnd -lt $haystack.Length )
            {
                $currentChar = $haystack[$needleEnd]
                if( $currentChar -eq ')' )
                {
                    if( $depth -eq 0 )
                    {
                        break
                    }

                    $depth--
                }
                elseif( $currentChar -eq '(' )
                {
                    $depth++
                }
                ++$needleEnd
            }
            
            $variableName = $haystack.Substring($needleStart + 2, $needleEnd - $needleStart - 2)
            $memberName = $null
            $arguments = $null

            # Does the variable expression contain a method call?
            if( $variableName -match '([^.]+)\.([^.(]+)(\(([^)]+)\))?' )
            {
                $variableName = $Matches[1]
                $memberName = $Matches[2]
                $arguments = $Matches[4]
                $arguments = & {
                                    if( -not $arguments )
                                    {
                                        return
                                    }

                                    $currentArg = New-Object 'Text.StringBuilder'
                                    $currentChar = $null
                                    $inString = $false
                                    # Parse each of the arguments in the method call. Each argument is
                                    # seperated by a comma. Ignore whitespace. Commas and whitespace that
                                    # are part of an argument must be double or single quoted. To include
                                    # a double quote inside a double-quoted string, double it. To include
                                    # a single quote inside a single-quoted string, double it.
                                    for( $idx = 0; $idx -lt $arguments.Length; ++$idx )
                                    {
                                        $nextChar = ''
                                        if( ($idx + 1) -lt $arguments.Length )
                                        {
                                            $nextChar = $arguments[$idx + 1]
                                        }

                                        $currentChar = $arguments[$idx]
                                        if( $currentChar -eq '"' -or $currentChar -eq "'" )
                                        {
                                            if( $inString )
                                            {
                                                if( $nextChar -eq $currentChar )
                                                {
                                                    [void]$currentArg.Append($currentChar)
                                                    $idx++
                                                    continue
                                                }
                                            }
                                            
                                            $inString = -not $inString
                                            continue
                                        }

                                        if( $currentChar -eq ',' -and -not $inString )
                                        {
                                            $currentArg.ToString()
                                            [void]$currentArg.Clear()
                                            continue
                                        }

                                        if( $inString -or -not [string]::IsNullOrWhiteSpace($currentChar) )
                                        {
                                            [void]$currentArg.Append($currentChar)
                                        }
                                    }
                                    if( $currentArg.Length )
                                    {
                                        $currentArg.ToString()
                                    }
                               }

            }

            $envVarPath = 'env:{0}' -f $variableName
            if( $Context.Variables.ContainsKey($variableName) )
            {
                $value = $Context.Variables[$variableName]
            }
            elseif( $wellKnownVariables.ContainsKey($variableName) )
            {
                $value = $wellKnownVariables[$variableName]
            }
            elseif( [Environment] | Get-Member -Static -Name $variableName )
            {
                $value = [Environment]::$variableName
            }
            elseif( (Test-Path -Path $envVarPath) )
            {
                $value = (Get-Item -Path $envVarPath).Value
            }
            else
            {
                Write-Error -Message ('Variable ''{0}'' does not exist. We were trying to replace it in the string ''{1}''. You can:
                 
* Use the `Add-WhiskeyVariable` function to add a variable named ''{0}'', e.g. Add-WhiskeyVariable -Context $context -Name ''{0}'' -Value VALUE.
* Create an environment variable named ''{0}''.
* Prevent variable expansion by escaping the variable with a backtick or backslash, e.g. `$({0}) or \$({0}).
* Remove the variable from the string.
  '
 -f $variableName,$InputObject) -ErrorAction $ErrorActionPreference
                return $InputObject
            }

            if( $value -eq $null )
            {
                $value = ''
            }

            if( $value -ne $null -and $memberName )
            {
                if( -not (Get-Member -Name $memberName -InputObject $value ) )
                {
                    Write-Error -Message ('Variable ''{0}'' does not have a ''{1}'' member. Here are the available members:{2} {2}{3}{2} ' -f $variableName,$memberName,[Environment]::NewLine,($value | Get-Member | Out-String))
                    return $InputObject
                }

                if( $arguments )
                {
                    try
                    {
                        $value = $value.$memberName.Invoke($arguments)
                    }
                    catch
                    {
                        Write-Error -Message ('Failed to call ([{0}]{1}).{2}(''{3}''): {4}.' -f $value.GetType().FullName,$value,$memberName,($arguments -join ''','''),$_)
                        return $InputObject
                    }
                }
                else
                {
                    $value = $value.$memberName
                }
            }

            $variableNumChars = $needleEnd - $needleStart + 1
            if( $needleStart + $variableNumChars -gt $haystack.Length )
            {
                Write-Error -Message ('Unclosed variable expression ''{0}'' in value ''{1}''. Add a '')'' to the end of this value or escape the variable expression with a double dollar sign, e.g. ''${1}''.' -f $haystack.Substring($needleStart),$haystack)
                return $InputObject
            }

            $haystack = $haystack.Remove($needleStart,$variableNumChars)
            $haystack = $haystack.Insert($needleStart,$value)
            # No need to keep searching where we've already looked.
            $startAt = $needleStart
        }
        while( $true )

        return $haystack
    }
}



function Set-WhiskeyBuildStatus
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [object]
        $Context,

        [Parameter(Mandatory=$true)]
        [ValidateSet('Started','Completed','Failed')]
        # The build status. Should be one of `Started`, `Completed`, or `Failed`.
        $Status
    )

    Set-StrictMode -Version 'Latest'

    if( $Context.ByDeveloper )
    {
        return
    }

    $reportingTo = $Context.Configuration['PublishBuildStatusTo']

    if( -not $reportingTo )
    {
        return
    }

    $reporterIdx = -1
    foreach( $reporter in $reportingTo )
    {
        $reporterIdx++
        $reporterName = $reporter.Keys | Select-Object -First 1
        $propertyDescription = 'PublishBuildStatusTo[{0}]: {1}' -f $reporterIdx,$reporterName
        $reporterConfig = $reporter[$reporterName]
        switch( $reporterName )
        {
            'BitbucketServer'
            {
                Install-WhiskeyPowerShellModule -Name 'BitbucketServerAutomation' -Version '0.9.*'
                Import-WhiskeyPowerShellModule -Name 'BitbucketServerAutomation'

                $uri = $reporterConfig['Uri']
                if( -not $uri )
                {
                    Stop-WhiskeyTask -TaskContext $Context -PropertyDescription $propertyDescription -Message (@'
Property 'Uri' does not exist or does not have a value. Set this property to the Bitbucket Server URI where you want build statuses reported to, e.g.,
  
    PublishBuildStatusTo:
    - BitbucketServer:
        Uri: BITBUCKET_SERVER_URI
        CredentialID: CREDENTIAL_ID
         
'@
 -f $uri)
                    return
                }
                $credID = $reporterConfig['CredentialID']
                if( -not $credID )
                {
                    Stop-WhiskeyTask -TaskContext $Context -PropertyDescription $propertyDescription -Message (@'
Property 'CredentialID' does not exist or does not have a value. Set this property to the ID of the credential to use when connecting to the Bitbucket Server at '{0}', e.g.,
  
    PublishBuildStatusTo:
    - BitbucketServer:
        Uri: {0}
        CredentialID: CREDENTIAL_ID
  
Use the `Add-WhiskeyCredential` function to add the credential to the build.`
'@
 -f $uri)
                    return
                }
                $credential = Get-WhiskeyCredential -Context $Context -ID $credID -PropertyName 'CredentialID' -PropertyDescription $propertyDescription
                $conn = New-BBServerConnection -Credential $credential -Uri $uri
                $statusMap = @{
                                    'Started' = 'INPROGRESS';
                                    'Completed' = 'Successful';
                                    'Failed' = 'Failed'
                              }

                $buildInfo = $Context.BuildMetadata
                Set-BBServerCommitBuildStatus -Connection $conn -Status $statusMap[$Status] -CommitID $buildInfo.ScmCommitID -Key $buildInfo.JobUri -BuildUri $buildInfo.BuildUri -Name $buildInfo.JobName
            }

            default
            {
                Stop-WhiskeyTask -TaskContext $Context -PropertyDescription $propertyDescription -Message ('Unknown build status reporter ''{0}''. Supported reporters are ''BitbucketServer''.' -f $reporterName)
                return
            }
        }
    }
}




function Set-WhiskeyDotNetGlobalJson
{
    <#
    .SYNOPSIS
    Sets values within a .NET Core global.json file.
 
    .DESCRIPTION
    The `Set-WhiskeyDotNetGlobalJson` function sets values within a .NET Core `global.json` file. If the `global.json` file does not exist in the given `Directory` then it will be created. If the `global.json` file already exists, then the function will only update the desired values and leave the rest of the content as-is.
 
    .EXAMPLE
    Set-WhiskeyDotNetGlobalJson -Directory 'C:\Build\app' -SdkVersion '2.1.4'
 
    Demonstrates setting the `sdk.version` property in global.json to '2.1.4'.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [string]
        # The directory where the `global.json` will be created/modified.
        $Directory,

        [Parameter(Mandatory=$true)]
        [string]
        # The version of the SDK to set within the `global.json` file.
        $SdkVersion
    )

    Set-StrictMode -version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    Write-Verbose -Message ('[{0}] Setting global.json properties: ''sdk.version'' => ''{1}''' -f $MyInvocation.MyCommand,$SdkVersion)

    if (-not (Test-Path -Path $Directory -PathType Container))
    {
        Write-Error -Message ('The directory ''{0}'' does not exist.' -f $Directory)
        return
    }

    $globalJsonPath = Join-Path -Path $Directory -ChildPath 'global.json'
    Write-Verbose -Message ('[{0}] Looking for global.json at ''{1}''' -f $MyInvocation.MyCommand,$globalJsonPath)

    if (Test-Path -Path $globalJsonPath -PathType Leaf)
    {
        Write-Verbose -Message ('[{0}] Found existing global.json' -f $MyInvocation.MyCommand)
        Write-Verbose -Message ('[{0}] Updating ''{1}''' -f $MyInvocation.MyCommand,$globalJsonPath)
        try
        {
            $globalJson = Get-Content -Path $globalJsonPath -Raw | ConvertFrom-Json
        }
        catch
        {
            Write-Error -Message ('global.json file ''{0}'' contains invalid JSON.' -f $globalJsonPath)
            return
        }

        if (-not ($globalJson | Get-Member -Name 'sdk'))
        {
            $globalJson | Add-Member -MemberType NoteProperty -Name 'sdk' -Value ([pscustomobject]@{ })
        }

        if (-not ($globalJson.sdk | Get-Member -Name 'version'))
        {
            $globalJson.sdk | Add-Member -MemberType NoteProperty -Name 'version' -Value ([pscustomobject]@{ })
        }

        $globalJson.sdk.version = $SdkVersion
    }
    else
    {
        Write-Verbose -Message ('[{0}] global.json does not exist at ''{1}''' -f $MyInvocation.MyCommand,$globalJsonPath)
        Write-Verbose -Message ('[{0}] Creating ''{1}''' -f $MyInvocation.MyCommand,$globalJsonPath)
        $globalJson = @{
                            'sdk' = @{
                                'version' = $SdkVersion
                            }
                       }
    }

    $globalJson | ConvertTo-Json -Depth 100 | Set-Content -Path $globalJsonPath -Force
    Write-Verbose -Message ('[{0}] global.json update finished.' -f $MyInvocation.MyCommand)
}



function Set-WhiskeyMSBuildConfiguration
{
    <#
    .SYNOPSIS
    Changes the configuration to use when running any MSBuild-based task/tool.
 
    .DESCRIPTION
    The `Set-WhiskeyMSBuildConfiguration` function sets the configuration to use when running any MSBuild-based task/tool (e.g. the `MSBuild`, `DotNetBuild`, `DotNetPublish`, etc.). Usually, the value should be set to either `Debug` or `Release`.
 
    Use `Get-WhiskeyMSBuildConfiguration` to get the current configuration.
 
    .EXAMPLE
    Set-WhiskeyMSBuildConfiguration -Context $Context -Value 'Release'
 
    Demonstrates how to set the configuration to use when running MSBuild tasks/tools.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [Whiskey.Context]
        # The context of the build whose MSBuild configuration you want to set. Use `New-WhiskeyContext` to create a context.
        $Context,

        [Parameter(Mandatory=$true)]
        [string]
        # The configuration to use.
        $Value
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    $Context.MSBuildConfiguration = $Value
}


function Stop-Whiskey
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [object]
        # An object
        $Context,
              
        [Parameter(Mandatory=$true)]
        [string]
        $Message
    )
              
    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
              
    throw '{0}: {1}' -f $Context.ConfigurationPath,$Message
}


function Stop-WhiskeyTask
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [object]
        # An object
        $TaskContext,

        [Parameter(Mandatory=$true)]
        [string]
        $Message,

        [string]
        $PropertyName,

        [string]
        $PropertyDescription
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
    
    if( -not ($PropertyDescription) )
    {
        $PropertyDescription = 'Build[{0}]: Task "{1}"' -f $TaskContext.TaskIndex,$TaskContext.TaskName
    }

    if( $PropertyName )
    {
        $PropertyName = ': Property "{0}"' -f $PropertyName
    }

    if( $ErrorActionPreference -ne 'Ignore' )
    {
        throw '{0}: {1}{2}: {3}' -f $TaskContext.ConfigurationPath,$PropertyDescription,$PropertyName,$Message
    }
}



function Test-WhiskeyTaskSkip
{
    <#
    .SYNOPSIS
    Determines if the current Whiskey task should be skipped.
 
    .DESCRIPTION
    The `Test-WhiskeyTaskSkip` function returns `$true` or `$false` indicating whether the current Whiskey task should be skipped. It determines if the task should be skipped by comparing values in the Whiskey context and common task properties.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [Whiskey.Context]
        # The context for the build.
        $Context,

        [Parameter(Mandatory=$true)]
        [hashtable]
        # The common task properties defined for the current task.
        $Properties
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    if( $Properties['OnlyBy'] -and $Properties['ExceptBy'] )
    {
        Stop-WhiskeyTask -TaskContext $Context -Message ('This task defines both "OnlyBy" and "ExceptBy" properties. Only one of these can be used. Please remove one or both of these properties and re-run your build.')
        return
    }
    elseif( $Properties['OnlyBy'] )
    {
        [Whiskey.RunBy]$onlyBy = [Whiskey.RunBy]::Developer
        if( -not ([enum]::TryParse($Properties['OnlyBy'], [ref]$onlyBy)) )
        {
            Stop-WhiskeyTask -TaskContext $Context -PropertyName 'OnlyBy' -Message ('invalid value: ''{0}''. Valid values are ''{1}''.' -f $Properties['OnlyBy'],([enum]::GetValues([Whiskey.RunBy]) -join ''', '''))
            return
        }

        if( $onlyBy -ne $Context.RunBy )
        {
            Write-WhiskeyVerbose -Context $Context -Message ('OnlyBy.{0} -ne Build.RunBy.{1}' -f $onlyBy,$Context.RunBy)
            return $true
        }
    }
    elseif( $Properties['ExceptBy'] )
    {
        [Whiskey.RunBy]$exceptBy = [Whiskey.RunBy]::Developer
        if( -not ([enum]::TryParse($Properties['ExceptBy'], [ref]$exceptBy)) )
        {
            Stop-WhiskeyTask -TaskContext $Context -PropertyName 'ExceptBy' -Message ('invalid value: ''{0}''. Valid values are ''{1}''.' -f $Properties['ExceptBy'],([enum]::GetValues([Whiskey.RunBy]) -join ''', '''))
            return
        }

        if( $exceptBy -eq $Context.RunBy )
        {
            Write-WhiskeyVerbose -Context $Context -Message ('ExceptBy.{0} -eq Build.RunBy.{1}' -f $exceptBy,$Context.RunBy)
            return $true
        }
    }

    $branch = $Context.BuildMetadata.ScmBranch

    if( $Properties['OnlyOnBranch'] -and $Properties['ExceptOnBranch'] )
    {
        Stop-WhiskeyTask -TaskContext $Context -Message ('This task defines both OnlyOnBranch and ExceptOnBranch properties. Only one of these can be used. Please remove one or both of these properties and re-run your build.')
        return
    }

    if( $Properties['OnlyOnBranch'] )
    {
        $runTask = $false
        Write-WhiskeyVerbose -Context $Context -Message ('OnlyOnBranch')
        foreach( $wildcard in $Properties['OnlyOnBranch'] )
        {
            if( $branch -like $wildcard )
            {
                $runTask = $true
                Write-WhiskeyVerbose -Context $Context -Message (' {0} -like {1}' -f $branch, $wildcard)
                break
            }

            Write-WhiskeyVerbose -Context $Context -Message     (' {0} -notlike {1}' -f $branch, $wildcard)
        }
        if( -not $runTask )
        {
            return $true
        }
    }

    if( $Properties['ExceptOnBranch'] )
    {
        $runTask = $true
        Write-WhiskeyVerbose -Context $Context -Message ('ExceptOnBranch')
        foreach( $wildcard in $Properties['ExceptOnBranch'] )
        {
            if( $branch -like $wildcard )
            {
                $runTask = $false
                Write-WhiskeyVerbose -Context $Context -Message (' {0} -like {1}' -f $branch, $wildcard)
                break
            }

            Write-WhiskeyVerbose -Context $Context -Message     (' {0} -notlike {1}' -f $branch, $wildcard)
        }
        if( -not $runTask )
        {
            return $true
        }
    }

    $modes = @( 'Clean', 'Initialize', 'Build' )
    $onlyDuring = $Properties['OnlyDuring']
    $exceptDuring = $Properties['ExceptDuring']

    if ($onlyDuring -and $exceptDuring)
    {
        Stop-WhiskeyTask -TaskContext $Context -Message 'Both ''OnlyDuring'' and ''ExceptDuring'' properties are used. These properties are mutually exclusive, i.e. you may only specify one or the other.'
        return
    }
    elseif ($onlyDuring -and ($onlyDuring -notin $modes))
    {
        Stop-WhiskeyTask -TaskContext $Context -Message ('Property ''OnlyDuring'' has an invalid value: ''{0}''. Valid values are: ''{1}''.' -f $onlyDuring,($modes -join "', '"))
        return
    }
    elseif ($exceptDuring -and ($exceptDuring -notin $modes))
    {
        Stop-WhiskeyTask -TaskContext $Context -Message ('Property ''ExceptDuring'' has an invalid value: ''{0}''. Valid values are: ''{1}''.' -f $exceptDuring,($modes -join "', '"))
        return
    }

    if ($onlyDuring -and ($Context.RunMode -ne $onlyDuring))
    {
        Write-WhiskeyVerbose -Context $Context -Message ('OnlyDuring.{0} -ne Build.RunMode.{1}' -f $onlyDuring,$Context.RunMode)
        return $true
    }
    elseif ($exceptDuring -and ($Context.RunMode -eq $exceptDuring))
    {
        Write-WhiskeyVerbose -Context $Context -Message ('ExceptDuring.{0} -ne Build.RunMode.{1}' -f $exceptDuring,$Context.RunMode)
        return $true
    }

    if( $Properties['IfExists'] )
    {
        $exists = Test-Path -Path $Properties['IfExists']
        if( -not $exists )
        {
            Write-WhiskeyVerbose -Context $Context -Message ('IfExists {0} not exists' -f $Properties['IfExists']) -Verbose
            return $true
        }
        Write-WhiskeyVerbose -Context $Context -Message     ('IfExists {0} exists' -f $Properties['IfExists']) -Verbose
    }

    if( $Properties['UnlessExists'] )
    {
        $exists = Test-Path -Path $Properties['UnlessExists']
        if( $exists )
        {
            Write-WhiskeyVerbose -Context $Context -Message ('UnlessExists {0} exists' -f $Properties['UnlessExists']) -Verbose
            return $true
        }
        Write-WhiskeyVerbose -Context $Context -Message     ('UnlessExists {0} not exists' -f $Properties['UnlessExists']) -Verbose
    }

    if( $Properties['OnlyIfBuild'] )
    {
        [Whiskey.BuildStatus]$buildStatus = [Whiskey.BuildStatus]::Succeeded
        if( -not ([enum]::TryParse($Properties['OnlyIfBuild'], [ref]$buildStatus)) )
        {
            Stop-WhiskeyTask -TaskContext $Context -PropertyName 'OnlyIfBuild' -Message ('invalid value: ''{0}''. Valid values are ''{1}''.' -f $Properties['OnlyIfBuild'],([enum]::GetValues([Whiskey.BuildStatus]) -join ''', '''))
            return
        }

        if( $buildStatus -ne $Context.BuildStatus )
        {
            Write-WhiskeyVerbose -Context $Context -Message ('OnlyIfBuild.{0} -ne Build.BuildStatus.{1}' -f $buildStatus,$Context.BuildStatus)
            return $true
        }
    }

    if( $Properties['OnlyOnPlatform'] )
    {
        $shouldSkip = $true
        [Whiskey.Platform]$platform = [Whiskey.Platform]::Unknown
        foreach( $item in $Properties['OnlyOnPlatform'] )
        {
            if( -not [enum]::TryParse($item,[ref]$platform) )
            {
                $validValues = [Enum]::GetValues([Whiskey.Platform]) | Where-Object { $_ -notin @( 'Unknown', 'All' ) }
                Stop-WhiskeyTask -TaskContext $Context -PropertyName 'OnlyOnPlatform' -Message ('Invalid platform "{0}". Valid values are "{1}".' -f $item,($validValues -join '", "'))
                return
            }
            $platform = [Whiskey.Platform]$item
            if( $CurrentPlatform.HasFlag($platform) )
            {
                Write-WhiskeyVerbose -Context $Context -Message ('OnlyOnPlatform {0} -eq {1}' -f $platform,$CurrentPlatform)
                $shouldSkip = $false
                break
            }
            else
            {
                Write-WhiskeyVerbose -Context $Context -Message ('OnlyOnPlatform ! {0} -ne {1}' -f $platform,$CurrentPlatform)
            }
        }
        return $shouldSkip
    }


    if( $Properties['ExceptOnPlatform'] )
    {
        $shouldSkip = $false
        [Whiskey.Platform]$platform = [Whiskey.Platform]::Unknown
        foreach( $item in $Properties['ExceptOnPlatform'] )
        {
            if( -not [enum]::TryParse($item,[ref]$platform) )
            {
                $validValues = [Enum]::GetValues([Whiskey.Platform]) | Where-Object { $_ -notin @( 'Unknown', 'All' ) }
                Stop-WhiskeyTask -TaskContext $Context -PropertyName 'ExceptOnPlatform' -Message ('Invalid platform "{0}". Valid values are "{1}".' -f $item,($validValues -join '", "'))
                return
            }
            $platform = [Whiskey.Platform]$item
            if( $CurrentPlatform.HasFlag($platform) )
            {
                Write-WhiskeyVerbose -Context $Context -Message ('ExceptOnPlatform ! {0} -eq {1}' -f $platform,$CurrentPlatform)
                $shouldSkip = $true
                break
            }
            else
            {
                Write-WhiskeyVerbose -Context $Context -Message ('ExceptOnPlatform {0} -ne {1}' -f $platform,$CurrentPlatform)
            }
        }
        return $shouldSkip
    }

    return $false
}



function Uninstall-WhiskeyNodeModule
{
    <#
    .SYNOPSIS
    Uninstalls Node.js modules.
     
    .DESCRIPTION
    The `Uninstall-WhiskeyNodeModule` function will uninstall Node.js modules from the `node_modules` directory in the current working directory. It uses the `npm uninstall` command to remove the module.
     
    If the `npm uninstall` command fails to uninstall the module and the `Force` parameter was not used, then the function will write an error and return. If the `Force` parameter is used then the function will attempt to manually remove the module if `npm uninstall` fails.
     
    .EXAMPLE
    Uninstall-WhiskeyNodeModule -Name 'rimraf' -NodePath $TaskParameter['NodePath']
     
    Removes the node module 'rimraf' from the `node_modules` directory in the current directory.
 
    .EXAMPLE
    Uninstall-WhiskeyNodeModule -Name 'rimraf' -NodePath $TaskParameter['NodePath'] -Force
     
    Removes the node module 'rimraf' from `node_modules` directory in the current directory. Because the `Force` switch is used, if `npm uninstall` fails, will attemp to use PowerShell to remove the module.
    #>

    
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [string]
        # The name of the module to uninstall.
        $Name,

        [Parameter(Mandatory=$true)]
        [string]
        # The path to the build root directory.
        $BuildRootPath,

        [switch]
        # Node modules are being uninstalled on a developer computer.
        $ForDeveloper,

        [switch]
        # Remove the module manually if NPM fails to uninstall it
        $Force,

        [Switch]
        # Uninstall the module from the global cache.
        $Global
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    $argumentList = & {
                        $Name
                        if( $Global )
                        {
                            '-g'
                        }
                    }

    Invoke-WhiskeyNpmCommand -Name 'uninstall' `
                             -BuildRootPath $BuildRootPath `
                             -ArgumentList $argumentList `
                             -ForDeveloper:$ForDeveloper
    
    $modulePath = Resolve-WhiskeyNodeModulePath -Name $Name -BuildRootPath $BuildRootPath -Global:$Global -ErrorAction Ignore

    if( $modulePath )
    {
        if( $Force )
        {
            Remove-WhiskeyFileSystemItem -Path $modulePath
        }
        else
        {
            Write-Error -Message ('Failed to remove Node module "{0}" from "{1}". See previous errors for more details.' -f $Name,$modulePath)
            return
        }
    }

    if( $modulePath -and (Test-Path -Path $modulePath -PathType Container) )
    {
        Write-Error -Message ('Failed to remove Node module "{0}" from "{1}" using both "npm prune" and manual removal. See previous errors for more details.' -f $Name,$modulePath)
        return
    }
}



function Uninstall-WhiskeyPowerShellModule
{
    <#
    .SYNOPSIS
    Removes downloaded PowerShell modules.
 
    .DESCRIPTION
    The `Uninstall-WhiskeyPowerShellModule` function deletes downloaded PowerShell modules from Whiskey's local "PSModules" directory.
 
    .EXAMPLE
    Uninstall-WhiskeyPowerShellModule -Name 'Pester'
 
    This example will uninstall the PowerShell module `Pester` from Whiskey's local `PSModules` directory.
 
    .EXAMPLE
    Uninstall-WhiskeyPowerShellModule -Name 'Pester' -ErrorAction Stop
 
    Demonstrates how to fail a build if uninstalling the module fails by setting the `ErrorAction` parameter to `Stop`.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [string]
        # The name of the module to uninstall.
        $Name,

        [string]
        $Version = '*.*.*',

        # Modules are saved into a PSModules directory. The "Path" parameter is the path where this PSModules directory should be, *not* the path to the PSModules directory itself, i.e. this is the path to the "PSModules" directory's parent directory.
        $Path = (Get-Location).ProviderPath
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    Get-Module -Name $Name | Remove-Module -Force

    $modulesRoot = Join-Path -Path $Path -ChildPath $powerShellModulesDirectoryName
    # Remove modules saved by either PowerShell4 or PowerShell5
    $moduleRoots = @( ('{0}\{1}' -f $Name, $Version), ('{0}' -f $Name)  )
    foreach ($item in $moduleRoots)
    {
        $removeModule = (Join-Path -Path $modulesRoot -ChildPath $item )
        if( Test-Path -Path $removeModule -PathType Container )
        {
            Remove-Item -Path $removeModule -Recurse -Force
            break
        }
    }

    if( (Test-Path -Path $modulesRoot -PathType Container) )
    {
        $psmodulesDirEmpty = $null -eq (Get-ChildItem -Path $modulesRoot -File -Recurse)
        if( $psmodulesDirEmpty )
        {
            Remove-Item -Path $modulesRoot -Recurse -Force
        }
    }
}


function Uninstall-WhiskeyTool
{
    <#
    .SYNOPSIS
    Removes a tool installed with `Install-WhiskeyTool`.
 
    .DESCRIPTION
    The `Uninstall-WhiskeyTool` function removes tools that were installed with `Install-WhiskeyTool`. It removes PowerShell modules, NuGet packages, Node, Node modules, and .NET Core SDKs that Whiskey installs into your build root. PowerShell modules are removed from the `Modules` direcory. NuGet packages are removed from the `packages` directory. Node and node modules are removed from the `.node` directory. The .NET Core SDK is removed from the `.dotnet` directory.
 
    When uninstalling a Node module, its name should be prefixed with `NodeModule::`, e.g. `NodeModule::rimraf`.
     
    Users of the `Whiskey` API typcially won't need to use this function. It is called by other `Whiskey` function so they have the tools they need.
 
    .EXAMPLE
    Uninstall-WhiskeyTool -ModuleName 'Pester'
 
    Demonstrates how to remove the `Pester` module from the default location.
         
    .EXAMPLE
    Uninstall-WhiskeyTool -NugetPackageName 'NUnit.Runners' -Version '2.6.4'
 
    Demonstrates how to uninstall a specific NuGet Package. In this case, NUnit Runners version 2.6.4 would be removed from the default location.
 
    .EXAMPLE
    Uninstall-WhiskeyTool -ModuleName 'Pester' -Path $forPath
 
    Demonstrates how to remove a Pester module from a specified path location other than the default location. In this case, Pester would be removed from the directory pointed to by the $forPath variable.
     
    .EXAMPLE
    Uninstall-WhiskeyTool -ModuleName 'Pester' -DownloadRoot $Root
 
    Demonstrates how to remove a Pester module from a DownloadRoot. In this case, Pester would be removed from `$Root\Modules`.
 
    .EXAMPLE
    Uninstall-WhiskeyTool -Name 'Node' -InstallRoot $TaskContext.BuildRoot
 
    Demonstrates how to uninstall Node from the `.node` directory in your build root.
 
    .EXAMPLE
    Uninstall-WhiskeyTool -Name 'NodeModule::rimraf' -InstallRoot $TaskContext.BuildRoot
 
    Demonstrates how to uninstall the `rimraf` Node module from the `node_modules` directory in the Node directory in your build root.
 
    .EXAMPLE
    Uninstall-WhiskeyTool -Name 'DotNet' -InstallRoot $TaskContext.BuildRoot
 
    Demonstrates how to uninstall the .NET Core SDK from the `.dotnet` directory in your build root.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true,ParameterSetName='Tool')]
        [string]
        # The name of the tool to uninstall. Currently only Node is supported.
        $Name,

        [Parameter(Mandatory=$true,ParameterSetName='Tool')]
        [string]
        # The directory where the tool should be uninstalled from.
        $InstallRoot,

        [Parameter(Mandatory=$true,ParameterSetName='NuGet')]
        [string]
        # The name of the NuGet package to uninstall.
        $NuGetPackageName,

        [String]
        # The version of the package to uninstall. Must be a three part number, i.e. it must have a MAJOR, MINOR, and BUILD number.
        $Version,

        [Parameter(Mandatory=$true,ParameterSetName='NuGet')]
        [string]
        # The build root where the build is currently running. Tools are installed here.
        $BuildRoot
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
    
    if( $PSCmdlet.ParameterSetName -eq 'NuGet' )
    {
        $Version = Resolve-WhiskeyNuGetPackageVersion -NuGetPackageName $NuGetPackageName -Version $Version -NugetPath $whiskeyNuGetExePath
        if( -not $Version )
        {
            return
        }
        $packagesRoot = Join-Path -Path $BuildRoot -ChildPath 'packages'
        $nuGetRootName = '{0}.{1}' -f $NuGetPackageName,$Version
        $nuGetRoot = Join-Path -Path $packagesRoot -ChildPath $nuGetRootName
        
        if( (Test-Path -Path $nuGetRoot -PathType Container) )
        {
            Remove-Item -Path $nuGetRoot -Recurse -Force
        }
    }
    elseif( $PSCmdlet.ParameterSetName -eq 'Tool' )
    {
        $provider,$Name = $Name -split '::'
        if( -not $Name )
        {
            $Name = $provider
            $provider = ''
        }

        switch( $provider )
        {
            'NodeModule'
            {
                # Don't do anything. All node modules require the Node tool to also be defined so they'll get deleted by the Node deletion.
            }
            'PowerShellModule'
            {
                Uninstall-WhiskeyPowerShellModule -Name $Name
            }
            default
            {
                switch( $Name )
                {
                    'Node'
                    {
                        $dirToRemove = Join-Path -Path $InstallRoot -ChildPath '.node'
                        Remove-WhiskeyFileSystemItem -Path $dirToRemove
                    }
                    'DotNet'
                    {
                        $dotnetToolRoot = Join-Path -Path $InstallRoot -ChildPath '.dotnet'
                        Remove-WhiskeyFileSystemItem -Path $dotnetToolRoot
                    }
                    default
                    {
                        throw ('Unknown tool ''{0}''. The only supported tools are ''Node'' and ''DotNet''.' -f $Name)
                    }
                }
            }
        }
    }
}



function Unregister-WhiskeyEvent
{
    <#
    .SYNOPSIS
    Unregisters a command to call when specific events happen during a build.
 
    .DESCRIPTION
    The `Unregister-WhiskeyEvent` function unregisters a command to run when a specific event happens during a build. This function is paired with `Register-WhiskeyEvent'.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [string]
        # The name of the command to run during the event.
        $CommandName,

        [Parameter(Mandatory=$true)]
        [string]
        [ValidateSet('BeforeTask','AfterTask')]
        # When the command should be run; what events does it respond to?
        $Event,

        [string]
        # The specific task whose events to unregister.
        $TaskName
    )

    Set-StrictMode -Version 'Latest'

    $eventName = $Event
    if( $TaskName )
    {
        $eventType = $Event -replace 'Task$',''
        $eventName = '{0}{1}Task' -f $eventType,$TaskName
    }

    if( -not $events[$eventName] )
    {
        return
    }

    if( -not $Events[$eventName].Contains( $CommandName ) )
    {
        return
    }

    $events[$eventName].Remove( $CommandName )
}

# Copyright 2012 Aaron Jensen
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

function Use-CallerPreference
{
    <#
    .SYNOPSIS
    Sets the PowerShell preference variables in a module's function based on the callers preferences.
 
    .DESCRIPTION
    Script module functions do not automatically inherit their caller's variables, including preferences set by common parameters. This means if you call a script with switches like `-Verbose` or `-WhatIf`, those that parameter don't get passed into any function that belongs to a module.
 
    When used in a module function, `Use-CallerPreference` will grab the value of these common parameters used by the function's caller:
 
     * ErrorAction
     * Debug
     * Confirm
     * InformationAction
     * Verbose
     * WarningAction
     * WhatIf
     
    This function should be used in a module's function to grab the caller's preference variables so the caller doesn't have to explicitly pass common parameters to the module function.
 
    This function is adapted from the [`Get-CallerPreference` function written by David Wyatt](https://gallery.technet.microsoft.com/scriptcenter/Inherit-Preference-82343b9d).
 
    There is currently a [bug in PowerShell](https://connect.microsoft.com/PowerShell/Feedback/Details/763621) that causes an error when `ErrorAction` is implicitly set to `Ignore`. If you use this function, you'll need to add explicit `-ErrorAction $ErrorActionPreference` to every function/cmdlet call in your function. Please vote up this issue so it can get fixed.
 
    .LINK
    about_Preference_Variables
 
    .LINK
    about_CommonParameters
 
    .LINK
    https://gallery.technet.microsoft.com/scriptcenter/Inherit-Preference-82343b9d
 
    .LINK
    http://powershell.org/wp/2014/01/13/getting-your-script-module-functions-to-inherit-preference-variables-from-the-caller/
 
    .EXAMPLE
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
 
    Demonstrates how to set the caller's common parameter preference variables in a module function.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        #[Management.Automation.PSScriptCmdlet]
        # The module function's `$PSCmdlet` object. Requires the function be decorated with the `[CmdletBinding()]` attribute.
        $Cmdlet,

        [Parameter(Mandatory = $true)]
        [Management.Automation.SessionState]
        # The module function's `$ExecutionContext.SessionState` object. Requires the function be decorated with the `[CmdletBinding()]` attribute.
        #
        # Used to set variables in its callers' scope, even if that caller is in a different script module.
        $SessionState
    )

    Set-StrictMode -Version 'Latest'

    # List of preference variables taken from the about_Preference_Variables and their common parameter name (taken from about_CommonParameters).
    $commonPreferences = @{
                              'ErrorActionPreference' = 'ErrorAction';
                              'DebugPreference' = 'Debug';
                              'ConfirmPreference' = 'Confirm';
                              'InformationPreference' = 'InformationAction';
                              'VerbosePreference' = 'Verbose';
                              'WarningPreference' = 'WarningAction';
                              'WhatIfPreference' = 'WhatIf';
                          }

    foreach( $prefName in $commonPreferences.Keys )
    {
        $parameterName = $commonPreferences[$prefName]

        # Don't do anything if the parameter was passed in.
        if( $Cmdlet.MyInvocation.BoundParameters.ContainsKey($parameterName) )
        {
            continue
        }

        $variable = $Cmdlet.SessionState.PSVariable.Get($prefName)
        # Don't do anything if caller didn't use a common parameter.
        if( -not $variable )
        {
            continue
        }

        if( $SessionState -eq $ExecutionContext.SessionState )
        {
            Set-Variable -Scope 1 -Name $variable.Name -Value $variable.Value -Force -Confirm:$false -WhatIf:$false
        }
        else
        {
            $SessionState.PSVariable.Set($variable.Name, $variable.Value)
        }
    }

}

    Set-StrictMode -version 'latest'
    
    function Write-CommandOutput
    {
        param(
            [Parameter(ValueFromPipeline=$true)]
            [string]
            $InputObject,

            [Parameter(Mandatory=$true)]
            [string]
            $Description
        )

        process
        {
            Set-StrictMode -Version 'Latest'
            Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

            if( $InputObject -match '^WARNING\b' )
            {
                $InputObject | Write-Warning 
            }
            elseif( $InputObject -match '^ERROR\b' )
            {
                $InputObject | Write-Error
            }
            else
            {
                $InputObject | ForEach-Object { Write-Verbose -Message ('[{0}] {1}' -f $Description,$InputObject) }
            }
        }
    }


function Write-WhiskeyCommand
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [Whiskey.Context]
        $Context,

        [string]
        $Path,

        [string[]]
        $ArgumentList
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
    
    $logArgumentList = Invoke-Command {
                                          if( $Path -match '\ ' )
                                          {
                                              '&'
                                          }
                                          $Path
                                          $ArgumentList
                                      } |
                                      ForEach-Object { 
                                          if( $_ -match '\ ' )
                                          {
                                              '"{0}"' -f $_.Trim('"',"'")
                                          }
                                          else
                                          {
                                              $_
                                          }
                                      }

    Write-WhiskeyInfo -Context $TaskContext -Message ($logArgumentList -join ' ')
    Write-WhiskeyVerbose -Context $TaskContext -Message $path
    $argumentPrefix = ' ' * ($path.Length + 2)
    foreach( $argument in $ArgumentList )
    {
        Write-WhiskeyVerbose -Context $TaskContext -Message ('{0}{1}' -f $argumentPrefix,$argument)
    }
}



function Write-WhiskeyInfo
{
    <#
    .SYNOPSIS
    Writes `Info` level messages.
 
    .DESCRIPTION
    The `Write-WhiskeyInfo` function writes `Info` level messages during a build with a prefix that identifies the current pipeline and task being executed.
 
    Pass the `Whiskey.Context` object to the `Context` parameter and the message to write to the `Message` parameter. Optionally, you may specify a custom indentation level for the message with the `Indent` parameter. The default message indentation is 1 space.
 
    This function uses PowerShell's [Write-Information](https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/write-information) cmdlet to write the message. If `Write-Information` is not supported, e.g. below PowerShell 5.0, the function falls back to using [Write-Output](https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/write-output).
 
    .EXAMPLE
    Write-WhiskeyInfo -Context $context -Message 'An info message'
 
    Demonstrates how write an `Info` message.
 
    .EXAMPLE
    Write-WhiskeyInfo -Context $context -Message 'An info message' -Indent 2
 
    Demonstrates how write an `Info` message with a custom indent.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        # The current context.
        [Whiskey.Context]$Context,

        [Parameter(Mandatory=$true,ValueFromPipeline=$true)]
        [AllowEmptyString()]
        [AllowNull()]
        # The message to write.
        [string]$Message,

        [int]$Indent = 0
    )

    process
    {
        Set-StrictMode -Version 'Latest'
        Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

        $Message = Format-WhiskeyMessage @PSBoundParameters
        if( $supportsWriteInformation )
        {
            Write-Information -MessageData $Message -InformationAction Continue
        }
        else
        {
            Write-Output -InputObject $Message
        }
    }
}



function Write-WhiskeyTiming
{
    [CmdletBinding()]
    param(
        $Message
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    $now = Get-Date
    Write-Debug -Message ('[{0}] [{1}] {2}' -f $now,($now - $buildStartedAt),$Message)
}


function Write-WhiskeyVerbose
{
    <#
    .SYNOPSIS
    Writes `Verbose` level messages.
 
    .DESCRIPTION
    The `Write-WhiskeyVerbose` function writes `Verbose` level messages during a build with a prefix that identifies the current pipeline and task being executed.
 
    Pass the `Whiskey.Context` object to the `Context` parameter and the message to write to the `Message` parameter. Optionally, you may specify a custom indentation level for the message with the `Indent` parameter. The default message indentation is 1 space.
 
    This function uses PowerShell's [Write-Verbose](https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/write-verbose) cmdlet to write the message.
 
    .EXAMPLE
    Write-WhiskeyVerbose -Context $context -Message 'A verbose message'
 
    Demonstrates how write a `Verbose` message.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        # The current context.
        [Whiskey.Context]$Context,

        [Parameter(Mandatory=$true,ValueFromPipeline=$true)]
        [AllowEmptyString()]
        [AllowNull()]
        # The message to write.
        [string]$Message,

        [int]$Indent = 0
    )

    process
    {
        Set-StrictMode -Version 'Latest'
        Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
    
        Write-Verbose -Message (Format-WhiskeyMessage @PSBoundParameters)
    }
}



function Write-WhiskeyWarning
{
    <#
    .SYNOPSIS
    Writes `Warning` level messages.
 
    .DESCRIPTION
    The `Write-WhiskeyWarning` function writes `Warning` level messages during a build with a prefix that identifies the current build configuration and task being executed.
 
    Pass the `Whiskey.Context` object to the `Context` parameter and the message to write to the `Message` parameter. Optionally, you may specify a custom indentation level for the message with the `Indent` parameter. The default message indentation is 1 space.
 
    This function uses PowerShell's [Write-Warning](https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/write-warning) cmdlet to write the message.
 
    .EXAMPLE
    Write-WhiskeyWarning -Context $context -Message 'A warning message'
 
    Demonstrates how write a `Warning` message.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        # The current context.
        [Whiskey.Context]$Context,

        [Parameter(Mandatory=$true,ValueFromPipeline=$true)]
        # The message to write.
        [string]$Message,

        [int]$Indent = 0
    )

    Set-StrictMode -Version 'Latest'

    Write-Warning -Message (Format-WhiskeyMessage @PSBoundParameters)
}



function Copy-WhiskeyFile
{
    [Whiskey.Task("CopyFile")]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [Whiskey.Context]
        $TaskContext,

        [Parameter(Mandatory=$true)]
        [hashtable]
        $TaskParameter
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    $pathErrorMessage = @'
'Path' property is missing. Please set it to the list of files to copy, e.g.
 
Build:
- CopyFile:
    Path: myfile.txt
    Destination: \\computer\share
'@

    $destDirErrorMessage = @'
'DestinationDirectory' property is missing. Please set it to the list of target locations to copy to, e.g.
 
Build:
- CopyFile:
    Path: myfile.txt
    DestinationDirectory: \\computer\share
'@


    if(!$TaskParameter.ContainsKey('Path'))
    {
        Stop-WhiskeyTask -TaskContext $TaskContext -Message ($pathErrorMessage)
        return
    }

    $sourceFiles = $TaskParameter['Path'] | Resolve-WhiskeyTaskPath -TaskContext $TaskContext -PropertyName 'Path'
    if(!$sourceFiles)
    {
        Stop-WhiskeyTask -TaskContext $TaskContext -Message ($pathErrorMessage)
        return
    }

    if(!$TaskParameter.ContainsKey('DestinationDirectory'))
    {
        Stop-WhiskeyTask -TaskContext $TaskContext -Message ($destDirErrorMessage)
        return
    }

    if(!$TaskParameter['DestinationDirectory'])
    {
        Stop-WhiskeyTask -TaskContext $TaskContext -Message ($destDirErrorMessage)
        return
    }

    foreach($sourceFile in $sourceFiles)
    {
        if((Test-Path -Path $sourceFile -PathType Container))
        {
            Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Path "{0}" is directory. The CopyFile task only copies files. Please remove this path from your "Path" property.' -f $sourceFile)
            return
        }
    }

    $idx = 0
    $destinations = $TaskParameter['DestinationDirectory'] |
                        ForEach-Object {

                            $path = $_

                            if( -not [IO.Path]::IsPathRooted($path) )
                            {
                                $path = Join-Path -Path $TaskContext.BuildRoot -ChildPath $path
                            }

                            if( [Management.Automation.WildcardPattern]::ContainsWildcardCharacters($path) )
                            {
                                $path = Resolve-Path -Path $path -ErrorAction Ignore | Select-Object -ExpandProperty 'ProviderPath'
                                if( -not $path )
                                {
                                    Stop-WhiskeyTask -TaskContext $TaskContext -Message ('DestinationDirectory[{0}]: Wildcard pattern "{1}" doesn''t point to an existing directory.' -f $idx, $_)
                                    return
                                }
                                $path
                            }
                            else
                            {
                                $path
                            }

                            $idx++
                        }

    foreach ($destDir in $destinations)
    {

        if(!(Test-Path -Path $destDir -PathType Container))
        {
            $null = New-Item -Path $destDir -ItemType 'Directory' -Force
        }

        if(!(Test-Path -Path $destDir -PathType Container))
        {
            Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Failed to create destination directory "{0}". Make sure the current user, "{1}\{2}" has access to create directories in "{0}". If it is a file share, check that the share exists and the share"s permissions.' -f $destDir, [Environment]::UserDomainName, [Environment]::UserName)
            return
        }
    }

    foreach( $destDir in $destinations )
    {
        foreach($sourceFile in $sourceFiles)
        {
            Write-WhiskeyVerbose -Context $TaskContext ('{0} -> {1}' -f $sourceFile,$destDir)
            Copy-Item -Path $sourceFile -Destination $destDir
        }
    }
}



function Remove-WhiskeyItem
{
    [Whiskey.TaskAttribute('Delete', SupportsClean=$true)]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [Whiskey.Context]
        $TaskContext,

        [Parameter(Mandatory=$true)]
        [hashtable]
        $TaskParameter
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
    
    foreach( $path in $TaskParameter['Path'] )
    {
        $path = $path | Resolve-WhiskeyTaskPath -TaskContext $TaskContext -PropertyName 'Path' -ErrorAction Ignore
        if( -not $path )
        {
            continue
        }

        foreach( $pathItem in $path )
        {
            Remove-WhiskeyFileSystemItem -Path $pathitem -ErrorAction Stop
        }
    }
}


function Invoke-WhiskeyDotNet
{
    [CmdletBinding()]
    [Whiskey.Task("DotNet")]
    [Whiskey.RequiresTool('DotNet','DotNetPath',VersionParameterName='SdkVersion')]
    param(
        [Parameter(Mandatory=$true)]
        [Whiskey.Context]
        $TaskContext,

        [Parameter(Mandatory=$true)]
        [hashtable]
        $TaskParameter
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    $command = $TaskParameter['Command']
    if( -not $command )
    {
        Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Property "Command" is required. It should be the name of the dotnet.exe command to run, e.g. "build", "test", etc.')
        return
    }

    $dotnetExe = $TaskParameter['DotNetPath']

    $invokeParameters = @{
        TaskContext = $TaskContext
        DotNetPath = $dotnetExe
        Name = $command
        ArgumentList = $TaskParameter['Argument']
    }

    Write-WhiskeyVerbose -Context $TaskContext -Message ('.NET Core SDK {0}' -f (& $dotnetExe --version))

    if( $TaskParameter.ContainsKey('Path') )
    {
        $projectPaths = $TaskParameter['Path'] | Resolve-WhiskeyTaskPath -TaskContext $TaskContext -PropertyName 'Path'
        foreach( $projectPath in $projectPaths )
        {
            Invoke-WhiskeyDotNetCommand @invokeParameters -ProjectPath $projectPath
        }
    }
    else
    {
        Invoke-WhiskeyDotNetCommand @invokeParameters
    }
}



function Invoke-WhiskeyDotNetBuild
{
    [CmdletBinding()]
    [Whiskey.Task("DotNetBuild",Obsolete,ObsoleteMessage='The "DotNetTest" task is obsolete and will be removed in a future version of Whiskey. Please use the "DotNet" task instead.')]
    [Whiskey.RequiresTool('DotNet','DotNetPath',VersionParameterName='SdkVersion')]
    param(
        [Parameter(Mandatory=$true)]
        [Whiskey.Context]
        $TaskContext,

        [Parameter(Mandatory=$true)]
        [hashtable]
        $TaskParameter
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    $dotnetExe = $TaskParameter['DotNetPath']

    $projectPaths = ''
    if ($TaskParameter['Path'])
    {
        $projectPaths = $TaskParameter['Path'] | Resolve-WhiskeyTaskPath -TaskContext $TaskContext -PropertyName 'Path'
    }

    $verbosity = $TaskParameter['Verbosity']
    if( -not $verbosity )
    {
        $verbosity = 'minimal'
    }

    $dotnetArgs = & {
        '--configuration={0}' -f (Get-WhiskeyMSBuildConfiguration -Context $TaskContext)
        '-p:Version={0}'      -f $TaskContext.Version.SemVer1.ToString()

        if ($verbosity)
        {
            '--verbosity={0}' -f $verbosity
        }

        if ($TaskParameter['OutputDirectory'])
        {
            '--output={0}' -f $TaskParameter['OutputDirectory']
        }

        if ($TaskParameter['Argument'])
        {
            $TaskParameter['Argument']
        }
    }

    Write-WhiskeyVerbose -Context $TaskContext -Message ('.NET Core SDK {0}' -f (& $dotnetExe --version))

    foreach($project in $projectPaths)
    {
        Invoke-WhiskeyDotNetCommand -TaskContext $TaskContext -DotNetPath $dotnetExe -Name 'build' -ArgumentList $dotnetArgs -ProjectPath $project
    }
}



function Invoke-WhiskeyDotNetPack
{
    [CmdletBinding()]
    [Whiskey.Task("DotNetPack",Obsolete,ObsoleteMessage='The "DotNetTest" task is obsolete and will be removed in a future version of Whiskey. Please use the "DotNet" task instead.')]
    [Whiskey.RequiresTool('DotNet','DotNetPath',VersionParameterName='SdkVersion')]
    param(
        [Parameter(Mandatory=$true)]
        [Whiskey.Context]
        $TaskContext,

        [Parameter(Mandatory=$true)]
        [hashtable]
        $TaskParameter
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    $dotnetExe = $TaskParameter['DotNetPath']

    $projectPaths = ''
    if ($TaskParameter['Path'])
    {
        $projectPaths = $TaskParameter['Path'] | Resolve-WhiskeyTaskPath -TaskContext $TaskContext -PropertyName 'Path'
    }

    $symbols = $TaskParameter['Symbols'] | ConvertFrom-WhiskeyYamlScalar

    $verbosity = $TaskParameter['Verbosity']
    if (-not $verbosity)
    {
        $verbosity = 'minimal'
    }

    $dotnetArgs = & {
        '-p:PackageVersion={0}' -f $TaskContext.Version.SemVer1.ToString()
        '--configuration={0}' -f (Get-WhiskeyMSBuildConfiguration -Context $TaskContext)
        '--output={0}' -f $TaskContext.OutputDirectory
        '--no-build'
        '--no-dependencies'
        '--no-restore'

        if ($symbols)
        {
            '--include-symbols'
        }

        if ($verbosity)
        {
            '--verbosity={0}' -f $verbosity
        }

        if ($TaskParameter['Argument'])
        {
            $TaskParameter['Argument']
        }
    }

    Write-WhiskeyVerbose -Context $TaskContext -Message ('.NET Core SDK {0}' -f (& $dotnetExe --version))

    foreach($project in $projectPaths)
    {
        Invoke-WhiskeyDotNetCommand -TaskContext $TaskContext -DotNetPath $dotnetExe -Name 'pack' -ArgumentList $dotnetArgs -ProjectPath $project
    }
}



function Invoke-WhiskeyDotNetPublish
{
    [CmdletBinding()]
    [Whiskey.Task("DotNetPublish",Obsolete,ObsoleteMessage='The "DotNetPublish" task is obsolete and will be removed in a future version of Whiskey. Please use the "DotNet" task instead.')]
    [Whiskey.RequiresTool('DotNet','DotNetPath',VersionParameterName='SdkVersion')]
    param(
        [Parameter(Mandatory=$true)]
        [Whiskey.Context]
        $TaskContext,

        [Parameter(Mandatory=$true)]
        [hashtable]
        $TaskParameter
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    $dotnetExe = $TaskParameter['DotNetPath']

    $projectPaths = ''
    if ($TaskParameter['Path'])
    {
        $projectPaths = $TaskParameter['Path'] | Resolve-WhiskeyTaskPath -TaskContext $TaskContext -PropertyName 'Path'
    }

    $verbosity = $TaskParameter['Verbosity']
    if (-not $verbosity)
    {
        $verbosity = 'minimal'
    }

    $dotnetArgs = & {
        '--configuration={0}' -f (Get-WhiskeyMSBuildConfiguration -Context $TaskContext)
        '-p:Version={0}'      -f $TaskContext.Version.SemVer1.ToString()

        if ($verbosity)
        {
            '--verbosity={0}' -f $verbosity
        }

        if ($TaskParameter['OutputDirectory'])
        {
            '--output={0}' -f $TaskParameter['OutputDirectory']
        }

        if ($TaskParameter['Argument'])
        {
            $TaskParameter['Argument']
        }
    }

    Write-WhiskeyVerbose -Context $TaskContext -Message ('.NET Core SDK {0}' -f (& $dotnetExe --version))

    foreach($project in $projectPaths)
    {
        Invoke-WhiskeyDotNetCommand -TaskContext $TaskContext -DotNetPath $dotnetExe -Name 'publish' -ArgumentList $dotnetArgs -ProjectPath $project
    }
}



function Invoke-WhiskeyDotNetTest
{
    [CmdletBinding()]
    [Whiskey.Task("DotNetTest",Obsolete,ObsoleteMessage='The "DotNetTest" task is obsolete and will be removed in a future version of Whiskey. Please use the "DotNet" task instead.')]
    [Whiskey.RequiresTool('DotNet','DotNetPath',VersionParameterName='SdkVersion')]
    param(
        [Parameter(Mandatory=$true)]
        [Whiskey.Context]
        $TaskContext,

        [Parameter(Mandatory=$true)]
        [hashtable]
        $TaskParameter
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    $dotnetExe = $TaskParameter['DotNetPath']

    $projectPaths = ''
    if ($TaskParameter['Path'])
    {
        $projectPaths = $TaskParameter['Path'] | Resolve-WhiskeyTaskPath -TaskContext $TaskContext -PropertyName 'Path'
    }

    $verbosity = $TaskParameter['Verbosity']
    if (-not $verbosity)
    {
        $verbosity = 'minimal'
    }

    $dotnetArgs = & {
        '--configuration={0}' -f (Get-WhiskeyMSBuildConfiguration -Context $TaskContext)
        '--no-build'
        '--results-directory={0}' -f ($TaskContext.OutputDirectory.FullName)

        if ($Taskparameter['Filter'])
        {
            '--filter={0}' -f $TaskParameter['Filter']
        }

        if ($TaskParameter['Logger'])
        {
            '--logger={0}' -f $TaskParameter['Logger']
        }

        if ($verbosity)
        {
            '--verbosity={0}' -f $verbosity
        }

        if ($TaskParameter['Argument'])
        {
            $TaskParameter['Argument']
        }
    }

    Write-WhiskeyVerbose -Context $TaskContext -Message ('.NET Core SDK {0}' -f (& $dotnetExe --version))

    foreach($project in $projectPaths)
    {
        Invoke-WhiskeyDotNetCommand -TaskContext $TaskContext -DotNetPath $dotnetExe -Name 'test' -ArgumentList $dotnetArgs -ProjectPath $project
    }
}


function Invoke-WhiskeyExec
{
    [CmdletBinding()]
    [Whiskey.Task("Exec",SupportsClean=$true,SupportsInitialize=$true)]
    param(
        [Parameter(Mandatory=$true)]
        [Whiskey.Context]
        $TaskContext,

        [Parameter(Mandatory=$true)]
        [hashtable]
        $TaskParameter
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    if( $TaskParameter.ContainsKey('') )
    {
        $regExMatches = Select-String -InputObject $TaskParameter[''] -Pattern '([^\s"'']+)|("[^"]*")|(''[^'']*'')' -AllMatches
        $defaultProperty = @($regExMatches.Matches.Groups | Where-Object { $_.Name -ne '0' -and $_.Success -eq $true } | Select-Object -ExpandProperty 'Value')

        $TaskParameter['Path'] = $defaultProperty[0]
        if( $defaultProperty.Count -gt 1 )
        {
            $TaskParameter['Argument'] = $defaultProperty[1..($defaultProperty.Count - 1)] | ForEach-Object { $_.Trim("'",'"') }
        }
    }

    $path = $TaskParameter['Path']
    if ( -not $path )
    {
        Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Property "Path" is mandatory. It should be the Path to the executable you want the Exec task to run, e.g.
 
            Build:
            - Exec:
                Path: cmd.exe
 
        '
)
        return
    }

    $path = & {
                    if( [IO.Path]::IsPathRooted($path) )
                    {
                        $path
                    }
                    else 
                    {
                        Join-Path -Path (Get-Location).Path -ChildPath $path
                        Join-Path -Path $TaskContext.BuildRoot -ChildPath $path
                    }
            } |
            Where-Object { Test-Path -path $_ -PathType Leaf } |
            Select-Object -First 1 |
            Resolve-Path |
            Select-Object -ExpandProperty 'ProviderPath'

    if( -not $path )
    {
        $path = $TaskParameter['Path']
        if( -not (Get-Command -Name $path -CommandType Application -ErrorAction Ignore) )
        {
            Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Executable "{0}" does not exist. We checked if the executable is at that path on the file system and if it is in your PATH environment variable.' -f $path)
            return
        }
    }

    if( ($path | Measure-Object).Count -gt 1 )
    {
        Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Unable to run executable "{0}": it contains wildcards and resolves to the following files: "{1}".' -f $TaskParameter['Path'],($path -join '","'))
        return
    }

    Write-WhiskeyCommand -Context $TaskContext -Path $path -ArgumentList $TaskParameter['Argument']

    # Don't use Start-Process. If/when a build runs in a background job, when Start-Process finishes, it immediately terminates the build. Full stop.
    & $path $TaskParameter['Argument']
    $exitCode = $LASTEXITCODE

    $successExitCodes = $TaskParameter['SuccessExitCode']
    if( -not $successExitCodes )
    {
        $successExitCodes = '0'
    }

    foreach( $successExitCode in $successExitCodes )
    {
        if( $successExitCode -match '^(\d+)$' )
        {
            if( $exitCode -eq [int]$Matches[0] )
            {
                Write-WhiskeyVerbose -Context $TaskContext -Message ('Exit Code {0} = {1}' -f $exitCode,$Matches[0])
                return
            }
        }

        if( $successExitCode -match '^(<|<=|>=|>)\s*(\d+)$' )
        {
            $operator = $Matches[1]
            $successExitCode = [int]$Matches[2]
            switch( $operator )
            {
                '<'
                {
                    if( $exitCode -lt $successExitCode )
                    {
                        Write-WhiskeyVerbose -Context $TaskContext -Message ('Exit Code {0} < {1}' -f $exitCode,$successExitCode)
                        return
                    }
                }
                '<='
                {
                    if( $exitCode -le $successExitCode )
                    {
                        Write-WhiskeyVerbose -Context $TaskContext -Message ('Exit Code {0} <= {1}' -f $exitCode,$successExitCode)
                        return
                    }
                }
                '>'
                {
                    if( $exitCode -gt $successExitCode )
                    {
                        Write-WhiskeyVerbose -Context $TaskContext -Message ('Exit Code {0} > {1}' -f $exitCode,$successExitCode)
                        return
                    }
                }
                '>='
                {
                    if( $exitCode -ge $successExitCode )
                    {
                        Write-WhiskeyVerbose -Context $TaskContext -Message ('Exit Code {0} >= {1}' -f $exitCode,$successExitCode)
                        return
                    }
                }
            }
        }

        if( $successExitCode -match '^(\d+)\.\.(\d+)$' )
        {
            if( $exitCode -ge [int]$Matches[1] -and $exitCode -le [int]$Matches[2] )
            {
                Write-WhiskeyVerbose -Context $TaskContext -Message ('Exit Code {0} <= {1} <= {2}' -f $Matches[1],$exitCode,$Matches[2])
                return
            }
        }
    }

    Stop-WhiskeyTask -TaskContext $TaskContext -Message ('"{0}" returned with an exit code of "{1}". View the build output to see why the executable''s process failed.' -F $TaskParameter['Path'],$exitCode)
}


function Get-WhiskeyPowerShellModule
{
    <#
    .SYNOPSIS
    Downloads a PowerShell module.
 
    .DESCRIPTION
    The `GetPowerShellModule` task downloads a PowerShell module and saves it into a `Modules` directory in the build root. Use the `Name` property to specify the name of the module to download. By default, it downloads the most recent version of the module. Use the `Version` property to download a specific version.
 
    The module is downloaded from any of the repositories that are configured on the current machine. Those repositories must be trusted and initialized, otherwise the module will fail to download.
 
    ## Property
 
    * `Name` (mandatory): a list of module names you want installed.
    * `Version`: the version number to download. The default behavior is to download the latest version. Wildcards allowed, e.g. use `2.*` to pin to major version `2`.
 
    .EXAMPLE
 
    ## Example 1
 
        Build:
        - GetPowerShellModule:
            Name: Whiskey
 
    This example demonstrates how to download the latest version of a PowerShell module. In this case, the latest version of Whiskey is downloaded and saved into the `.\Modules` directory in your build root.
 
    ## Example 2
 
        Build:
        - GetPowerShellModule:
            Name: Whiskey
            Version: "0.14.*"
 
    This example demonstrates how to pin to a specific version of a module. In this case, the latest `0.14.x` version will be downloaded. When version 0.15.0 comes out, you'll still download the latest `0.14.x` version.
 
    #>

    [CmdletBinding()]
    [Whiskey.Task("GetPowerShellModule",SupportsClean=$true, SupportsInitialize=$true)]
    param(
        [Parameter(Mandatory=$true)]
        [Whiskey.Context]
        $TaskContext,

        [Parameter(Mandatory=$true)]
        [hashtable]
        $TaskParameter
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    if( -not $TaskParameter['Name'] )
    {
        Stop-WhiskeyTask -TaskContext $TaskContext -Message 'Property "Name" is mandatory. It should be set to the name of the PowerShell module you want installed.'
        return
    }

    if( $TaskContext.ShouldClean )
    {
        Uninstall-WhiskeyPowerShellModule -Name $TaskParameter['Name']
        return
    }

    $module = Resolve-WhiskeyPowerShellModule -Name $TaskParameter['Name'] -Version $TaskParameter['Version'] -ErrorAction Stop
    if( -not $module )
    {
        return
    }

    Write-WhiskeyInfo -Context $TaskContext -Message ('Installing PowerShell module {0} {1}.' -f $TaskParameter['Name'],$module.Version)
    $moduleRoot = Install-WhiskeyPowerShellModule -Name $TaskParameter['Name'] -Version $module.Version -ErrorAction Stop
    Write-WhiskeyVerbose -Context $TaskContext -Message (' {0}' -f $moduleRoot)
}


function New-WhiskeyGitHubRelease
{
    [CmdletBinding()]
    [Whiskey.Task('GitHubRelease')]
    param(
        [Parameter(Mandatory=$true)]
        [Whiskey.Context]
        $TaskContext,

        [Parameter(Mandatory=$true)]
        [hashtable]
        $TaskParameter
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    $apiKeyID = $TaskParameter['ApiKeyID']
    if( -not $apiKeyID )
    {
        Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Property "ApiKeyID" is mandatory. It should be set to the ID of the API key to use when talking to the GitHub API. API keys are added to your build with the "Add-WhiskeyApiKey" function.')
        return
    }

    $apiKey = Get-WhiskeyApiKey -Context $TaskContext -ID $apiKeyID -PropertyName 'ApiKeyID'
    $headers = @{
                    Authorization = 'Basic ' + [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($apiKey + ":x-oauth-basic"))
                }
    $repositoryName = $TaskParameter['RepositoryName']
    if( -not $repositoryName )
    {
        Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Property "RepositoryName" is mandatory. It should be the owner and repository name of the repository you want to access as a URI path, e.g. OWNER/REPO.')
        return
    }

    if( $repositoryName -notmatch '^[^/]+/[^/]+$' )
    {
        Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Property "RepositoryName" is invalid. It should be the owner and repository name of the repository you want to access as a URI path, e.g. OWNER/REPO.')
        return
    }

    $baseUri = [uri]'https://api.github.com/repos/{0}' -f $repositoryName

    [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [System.Net.SecurityProtocolType]::Tls12

    function Invoke-GitHubApi
    {
        [CmdletBinding(DefaultParameterSetName='NoBody')]
        param(
            [Parameter(Mandatory=$true)]
            [uri]
            $Uri,

            [Parameter(Mandatory=$true,ParameterSetName='FileUpload')]
            [string]
            $ContentType,

            [Parameter(Mandatory=$true,ParameterSetName='FileUpload')]
            [string]
            $InFile,

            [Parameter(Mandatory=$true,ParameterSetName='JsonRequest')]
            $Parameter,

            [Microsoft.PowerShell.Commands.WebRequestMethod]
            $Method = [Microsoft.PowerShell.Commands.WebRequestMethod]::Post
        )

        $optionalParams = @{ }
        if( $PSCmdlet.ParameterSetName -eq 'JsonRequest' )
        {
            if( $Parameter )
            {
                $optionalParams['Body'] = $Parameter | ConvertTo-Json
                Write-WhiskeyVerbose -Context $TaskContext -Message $optionalParams['Body']
            }
            $ContentType = 'application/json'
        }
        elseif( $PSCmdlet.ParameterSetName -eq 'FileUpload' )
        {
            $optionalParams['InFile'] = $InFile
        }

        try
        {
            Invoke-RestMethod -Uri $Uri -Method $Method -Headers $headers -ContentType $ContentType @optionalParams
        }
        catch
        {
            if( $ErrorActionPreference -eq 'Ignore' )
            {
                $Global:Error.RemoveAt(0)
            }
            Stop-WhiskeyTask -TaskContext $TaskContext -Message ('GitHub API call to "{0}" failed: {1}' -f $uri,$_)
            return
        }
    }

    $tag = $TaskParameter['Tag']
    if( -not $tag )
    {
        Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Property "Tag" is mandatory. It should be the tag to create in your repository for this release. This is usually a version number. We recommend using the `$(WHISKEY_SEMVER2_NO_BUILD_METADATA)` variable to use the version number of the current build.')
        return
    }
    $release = Invoke-GitHubApi -Uri ('{0}/releases/tags/{1}' -f $baseUri,[uri]::EscapeUriString($tag)) -Method Get -ErrorAction Ignore

    $createOrEditMethod = [Microsoft.PowerShell.Commands.WebRequestMethod]::Post
    $actionDescription = 'Creating'
    $createOrEditUri = '{0}/releases' -f $baseUri
    if( $release )
    {
        $createOrEditMethod = [Microsoft.PowerShell.Commands.WebRequestMethod]::Patch
        $actionDescription = 'Updating'
        $createOrEditUri = $release.url
    }

    $releaseData = @{
                        tag_name = $tag
                    }

    if( $TaskParameter['Commitish'] )
    {
        $releaseData['target_commitish'] = $TaskParameter['Commitish']
    }

    if( $TaskParameter['Name'] )
    {
        $releaseData['name'] = $TaskParameter['Name']
    }

    if( $TaskParameter['Description'] )
    {
        $releaseData['body'] = $TaskParameter['Description']
    }

    Write-WhiskeyInfo -Context $TaskContext -Message ('{0} release "{1}" "{2}" at commit "{3}".' -f $actionDescription,$TaskParameter['Name'],$tag,$TaskContext.BuildMetadata.ScmCommitID)
    $release = Invoke-GitHubApi -Uri $createOrEditUri -Parameter $releaseData -Method $createOrEditMethod
    $release

    if( $TaskParameter['Assets'] )
    {
        $existingAssets = Invoke-GitHubApi -Uri $release.assets_url -Method Get

        $assetIdx = 0
        foreach( $asset in $TaskParameter['Assets'] )
        {
            $basePropertyName = 'Assets[{0}]' -f $assetIdx++
            $assetPath = $asset['Path'] | Resolve-WhiskeyTaskPath -TaskContext $TaskContext -PropertyName ('{0}.Path:' -f $basePropertyName) -PathType File
            if( -not $assetPath )
            {
                continue
            }

            $assetName = $assetPath | Split-Path -Leaf
            $assetLabel = $asset['Name']
            if( $assetLabel -eq $null )
            {
                $assetLabel = ""
            }

            $existingAsset = $existingAssets | Where-Object { $_ -and $_.name -eq $assetName }
            if( $existingAsset )
            {
                Write-WhiskeyInfo -Context $TaskContext -Message ('Updating file "{0}".' -f $assetName)
                Invoke-GitHubApi -Method Patch -Uri $existingAsset.url -Parameter @{ name = $assetName; label = $assetLabel }
            }
            else
            {
                $uri = $release.upload_url -replace '{[^}]+}$'
                $uri = '{0}?name={1}' -f $uri,[uri]::EscapeDataString($assetName)
                if( $assetLabel )
                {
                    $uri = '{0}&label={1}' -f $uri,[uri]::EscapeDataString($assetLabel)
                }
                Write-WhiskeyInfo -Context $TaskContext -Message ('Uploading file "{0}".' -f $assetPath)
                $contentType = $asset['ContentType']
                if( -not $contentType )
                {
                    Stop-WhiskeyTask -TaskContext $TaskContext -PropertyName $basePropertyName -Message ('Property "ContentType" is mandatory. It must be the "{0}" file''s media type. For a list of acceptable types, see https://www.iana.org/assignments/media-types/media-types.xhtml.' -f $assetPath)
                    continue
                }
                Invoke-GitHubApi -Method Post -Uri $uri -ContentType $asset['ContentType'] -InFile $assetPath
            }
        }
    }
}


function Import-WhiskeyTask
{
    [Whiskey.Task("LoadTask")]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [Whiskey.Context]
        $TaskContext,

        [Parameter(Mandatory=$true)]
        [hashtable]
        $TaskParameter
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    $module = Get-Module -Name 'Whiskey'
    $paths = Resolve-WhiskeyTaskPath -TaskContext $TaskContext -Path $TaskParameter['Path'] -PropertyName 'Path'
    foreach( $path in $paths )
    {
        if( $TaskContext.TaskPaths | Where-Object { $_.FullName -eq $path } )
        {
            Write-WhiskeyVerbose -Context $TaskContext -Message ('Already loaded tasks from file "{0}".' -f $path) -Verbose
            continue
        }

        $knownTasks = @{}
        Get-WhiskeyTask | ForEach-Object { $knownTasks[$_.Name] = $_ }
        # We do this in a background script block to ensure the function is scoped correctly. If it isn't, it
        # won't be available outside the script block. If it is, it will be visible after the script block completes.
        & {
            . $path
        }
        $newTasks = Get-WhiskeyTask | Where-Object { -not $knownTasks.ContainsKey($_.Name) }
        if( -not $newTasks )
        {
            Stop-WhiskeyTask -TaskContext $TaskContext -Message ('File "{0}" contains no Whiskey tasks. Make sure:
 
* the file contains a function
* the function is scoped correctly (e.g. `function script:MyTask`)
* the function has a `[Whiskey.Task("MyTask")]` attribute that declares the task''s name
* a task with the same name hasn''t already been loaded
 
See about_Whiskey_Writing_Tasks for more information.'
 -f $path)
            return
        }

        Write-WhiskeyInfo -Context $TaskContext -Message ($path)
        foreach( $task in $newTasks )
        {
            Write-WhiskeyInfo -Context $TaskContext -Message $task.Name -Indent 1
        }
        $TaskContext.TaskPaths.Add((Get-Item -Path $path))
    }
}



function Merge-WhiskeyFile
{
    [CmdletBinding()]
    [Whiskey.Task('MergeFile')]
    param(
        [Parameter(Mandatory)]
        [Whiskey.Context]$TaskContext,

        [Whiskey.Tasks.ValidatePath(Mandatory,PathType='File')]
        [string[]]$Path,

        [string]$DestinationPath,

        [switch]$DeleteSourceFiles,

        [string]$TextSeparator,

        [byte[]]$BinarySeparator,

        [switch]$Clear,

        [string[]]$Exclude
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    $DestinationPath = Resolve-WhiskeyTaskPath -TaskContext $TaskContext `
                                               -Path $DestinationPath `
                                               -PropertyName 'DestinationPath' `
                                               -PathType 'File' `
                                               -Force
    
    $normalizedBuildRoot = $TaskContext.BuildRoot.FullName.TrimEnd([IO.Path]::DirectorySeparatorChar,[IO.Path]::AltDirectorySeparatorChar)
    $normalizedBuildRoot = Join-Path -Path $normalizedBuildRoot -ChildPath ([IO.Path]::DirectorySeparatorChar)
    if( -not $DestinationPath.StartsWith($normalizedBuildRoot) )
    {
        Stop-WhiskeyTask -TaskContext $TaskContext -PropertyName 'DestinationPath' -Message ('"{0}" resolves to "{1}", which is outside the build root "{2}".' -f $PSBoundParameters['DestinationPath'],$DestinationPath,$TaskContext.BuildRoot.FullName)
        return
    }

    if( $Clear )
    {
        Clear-Content -Path $DestinationPath
    }

    if( $TextSeparator -and $BinarySeparator )
    {
        Stop-WhiskeyTask -TaskContext $TaskContext `
                         -Message ('You can''t use both a text separator and binary separator when merging files. Please use only the TextSeparator or BinarySeparator property, not both.')
        return
    }

    [byte[]]$separatorBytes = $BinarySeparator
    if( $TextSeparator )
    {
        $separatorBytes = [Text.Encoding]::UTF8.GetBytes($TextSeparator)
    }

    $relativePath = Resolve-Path -Path $DestinationPath -Relative
    $writer = [IO.File]::OpenWrite($relativePath)
    try 
    {
        Write-WhiskeyInfo -Context $TaskContext -Message $relativePath 

        # Move to the end of the file.
        $writer.Position = $writer.Length

        # Only add the separator first if we didn't clear the file's original contents.
        $addSeparator = (-not $Clear) -and ($writer.Length -gt 0)

        # Normalize the exclusion pattern so it works across platforms.
        $Exclude = 
            $Exclude | 
            ForEach-Object { $_ -replace '\\|/',[IO.Path]::DirectorySeparatorChar }
        foreach( $filePath in $Path )
        {
            $excluded = $false
            foreach( $pattern in $Exclude )
            {
                if( $filePath -like $pattern )
                {
                    Write-WhiskeyVerbose -Context $TaskContext -Message ('Skipping file "{0}": it matches exclusion pattern "{1}".' -f $filePath,$pattern)
                    $excluded = $true
                    break
                }
                else 
                {
                    Write-Debug -Message ('"{0}" -notlike "{1}"' -f $filePath,$pattern)
                }
            }

            if( $excluded )
            {
                continue
            }

            $relativePath = Resolve-Path -Path $filePath -Relative
            Write-WhiskeyInfo -Context $TaskContext -Message (' + {0}' -f $relativePath) -Verbose

            if( $addSeparator -and $separatorBytes )
            {
                $writer.Write($separatorBytes,0,$separatorBytes.Length)
            }
            $addSeparator = $true

            $reader = [IO.File]::OpenRead($filePath)
            try
            {
                $bufferSize = 4kb
                [byte[]]$buffer = New-Object 'byte[]' ($bufferSize)
                while( $bytesRead = $reader.Read($buffer,0,$bufferSize) )
                {
                    $writer.Write($buffer,0,$bytesRead)
                }
            }
            finally
            {
                $reader.Close()
            }

            if( $DeleteSourceFiles )
            {
                Remove-Item -Path $filePath -Force
            }
        }
    }
    finally
    {
        $writer.Close()
    }
}



function Invoke-WhiskeyMSBuild
{
    [Whiskey.Task('MSBuild',SupportsClean,Platform='Windows')]
    [Whiskey.RequiresTool('PowerShellModule::VSSetup','VSSetupPath',Version='2.*',VersionParameterName='VSSetupVersion')]
    [CmdletBinding()]
    param(
        [Whiskey.Context]
        # The context this task is operating in. Use `New-WhiskeyContext` to create context objects.
        $TaskContext,

        [hashtable]
        # The parameters/configuration to use to run the task. Should be a hashtable that contains the following item(s):
        #
        # * `Path` (Mandatory): the relative paths to the files/directories to include in the build. Paths should be relative to the whiskey.yml file they were taken from.
        $TaskParameter
    )

    Set-StrictMode -version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    Import-WhiskeyPowerShellModule -Name 'VSSetup'

    #setup
    $nuGetPath = Install-WhiskeyNuGet -DownloadRoot $TaskContext.BuildRoot -Version $TaskParameter['NuGetVersion']

    # Make sure the Taskpath contains a Path parameter.
    if( -not ($TaskParameter.ContainsKey('Path')) -or -not $TaskParameter['Path'] )
    {
        Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Element ''Path'' is mandatory. It should be one or more paths, relative to your whiskey.yml file, to build with MSBuild.exe, e.g.
 
        Build:
        - MSBuild:
            Path:
            - MySolution.sln
            - MyCsproj.csproj'
)
        return
    }

    $path = $TaskParameter['Path'] | Resolve-WhiskeyTaskPath -TaskContext $TaskContext -PropertyName 'Path'

    $msbuildInfos = Get-MSBuild | Sort-Object -Descending 'Version'
    $version = $TaskParameter['Version']
    if( $version )
    {
        $msbuildInfo = $msbuildInfos | Where-Object { $_.Name -eq $version } | Select-Object -First 1
    }
    else
    {
        $msbuildInfo = $msbuildInfos | Select-Object -First 1
    }

    if( -not $msbuildInfo )
    {
        $msbuildVersionNumbers = $msbuildInfos | Select-Object -ExpandProperty 'Name'
        Stop-WhiskeyTask -TaskContext $TaskContext -Message ('MSBuild {0} is not installed. Installed versions are: {1}' -f $version,($msbuildVersionNumbers -join ', '))
        return
    }

    $msbuildExePath = $msbuildInfo.Path
    if( $TaskParameter.ContainsKey('Use32Bit') -and ($TaskParameter['Use32Bit'] | ConvertFrom-WhiskeyYamlScalar) )
    {
        $msbuildExePath = $msbuildInfo.Path32
        if( -not $msbuildExePath )
        {
            Stop-WhiskeyTask -TaskContext $TaskContext -Message ('A 32-bit version of MSBuild {0} does not exist.' -f $version)
            return
        }
    }
    Write-WhiskeyVerbose -Context $TaskContext -Message ('{0}' -f $msbuildExePath)

    $target = @( 'build' )
    if( $TaskContext.ShouldClean )
    {
        $target = 'clean'
    }
    else
    {
        if( $TaskParameter.ContainsKey('Target') )
        {
            $target = $TaskParameter['Target']
        }
    }

    foreach( $projectPath in $path )
    {
        Write-WhiskeyVerbose -Context $TaskContext -Message (' {0}' -f $projectPath)
        $errors = $null
        if( $projectPath -like '*.sln' )
        {
            if( $TaskContext.ShouldClean )
            {
                $packageDirectoryPath = Join-Path -path ( Split-Path -Path $projectPath -Parent ) -ChildPath 'packages'
                if( Test-Path -Path $packageDirectoryPath -PathType Container )
                {
                    Write-WhiskeyVerbose -Context $TaskContext -Message (' Removing NuGet packages at {0}.' -f $packageDirectoryPath)
                    Remove-Item $packageDirectoryPath -Recurse -Force
                }
            }
            else
            {
                Write-WhiskeyVerbose -Context $TaskContext -Message (' Restoring NuGet packages.')
                & $nugetPath restore $projectPath
            }
        }

        if( $TaskContext.ByBuildServer )
        {
            $projectPath |
                Split-Path |
                Get-ChildItem -Filter 'AssemblyInfo.cs' -Recurse |
                ForEach-Object {
                    $assemblyInfo = $_
                    $assemblyInfoPath = $assemblyInfo.FullName
                    $newContent = Get-Content -Path $assemblyInfoPath | Where-Object { $_ -notmatch '\bAssembly(File|Informational)?Version\b' }
                    $newContent | Set-Content -Path $assemblyInfoPath
                    Write-WhiskeyVerbose -Context $TaskContext -Message (' Updating version in {0}.' -f $assemblyInfoPath)
    @"
[assembly: System.Reflection.AssemblyVersion("{0}")]
[assembly: System.Reflection.AssemblyFileVersion("{0}")]
[assembly: System.Reflection.AssemblyInformationalVersion("{1}")]
"@
 -f $TaskContext.Version.Version,$TaskContext.Version.SemVer2 | Add-Content -Path $assemblyInfoPath
                }
        }

        $verbosity = 'm'
        if( $TaskParameter['Verbosity'] )
        {
            $verbosity = $TaskParameter['Verbosity']
        }

        $configuration = Get-WhiskeyMSBuildConfiguration -Context $TaskContext

        $property = Invoke-Command {
                                        ('Configuration={0}' -f $configuration)

                                        if( $TaskParameter.ContainsKey('Property') )
                                        {
                                            $TaskParameter['Property']
                                        }

                                        if( $TaskParameter.ContainsKey('OutputDirectory') )
                                        {
                                            ('OutDir={0}' -f ($TaskParameter['OutputDirectory'] | Resolve-WhiskeyTaskPath -TaskContext $TaskContext -PropertyName 'OutputDirectory' -Force))
                                        }
                                  }

        $cpuArg = '/maxcpucount'
        $cpuCount = $TaskParameter['CpuCount'] | ConvertFrom-WhiskeyYamlScalar
        if( $cpuCount )
        {
            $cpuArg = '/maxcpucount:{0}' -f $TaskParameter['CpuCount']
        }

        if( ($TaskParameter['NoMaxCpuCountArgument'] | ConvertFrom-WhiskeyYamlScalar) )
        {
            $cpuArg = ''
        }

        $noFileLogger = $TaskParameter['NoFileLogger'] | ConvertFrom-WhiskeyYamlScalar

        $projectFileName = $projectPath | Split-Path -Leaf
        $logFilePath = Join-Path -Path $TaskContext.OutputDirectory -ChildPath ('msbuild.{0}.log' -f $projectFileName)
        $msbuildArgs = Invoke-Command {
                                            ('/verbosity:{0}' -f $verbosity)
                                            $cpuArg
                                            $TaskParameter['Argument']
                                            if( -not $noFileLogger )
                                            {
                                                '/filelogger9'
                                                ('/flp9:LogFile={0};Verbosity=d' -f $logFilePath)
                                            }
                                      } | Where-Object { $_ }
        $separator = '{0}VERBOSE: ' -f [Environment]::NewLine
        Write-WhiskeyVerbose -Context $TaskContext -Message (' Target {0}' -f ($target -join $separator))
        Write-WhiskeyVerbose -Context $TaskContext -Message (' Property {0}' -f ($property -join $separator))
        Write-WhiskeyVerbose -Context $TaskContext -Message (' Argument {0}' -f ($msbuildArgs -join $separator))

        $propertyArgs = $property | ForEach-Object {
            $item = $_
            $name,$value = $item -split '=',2
            $value = $value.Trim('"')
            $value = $value.Trim("'")
            if( $value.EndsWith( '\' ) )
            {
                $value = '{0}\' -f $value
            }
            '/p:{0}="{1}"' -f $name,($value -replace ' ','%20')
        }

        $targetArg = '/t:{0}' -f ($target -join ';')

        & $msbuildExePath $projectPath $targetArg $propertyArgs $msbuildArgs /nologo
        if( $LASTEXITCODE -ne 0 )
        {
            Stop-WhiskeyTask -TaskContext $TaskContext -Message ('MSBuild exited with code {0}.' -f $LASTEXITCODE)
            return
        }
    }
}



function Invoke-WhiskeyNodeTask
{
    [Whiskey.Task('Node',SupportsClean,SupportsInitialize,Obsolete,ObsoleteMessage='The "Node" task is obsolete and will be removed in a future version of Whiskey. It''s functionality has been broken up into the "Npm" and "NodeLicenseChecker" tasks.')]
    [Whiskey.RequiresTool('Node','NodePath')]
    [Whiskey.RequiresTool('NodeModule::license-checker','LicenseCheckerPath',VersionParameterName='LicenseCheckerVersion')]
    [Whiskey.RequiresTool('NodeModule::nsp','NspPath',VersionParameterName='PINNED_TO_NSP_2_7_0',Version='2.7.0')]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [Whiskey.Context]
        # The context the task is running under.
        $TaskContext,

        [Parameter(Mandatory=$true)]
        [hashtable]
        # The task parameters, which are:
        #
        # * `NpmScript`: a list of one or more NPM scripts to run, e.g. `npm run $SCRIPT_NAME`. Each script is run indepently.
        # * `WorkingDirectory`: the directory where all the build commands should be run. Defaults to the directory where the build's `whiskey.yml` file was found. Must be relative to the `whiskey.yml` file.
        $TaskParameter
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    if( $TaskContext.ShouldClean )
    {
        Write-WhiskeyTiming -Message 'Cleaning'
        $nodeModulesPath = Join-Path -Path $TaskContext.BuildRoot -ChildPath 'node_modules'
        Remove-WhiskeyFileSystemItem -Path $nodeModulesPath
        Write-WhiskeyTiming -Message 'COMPLETE'
        return
    }

    $npmScripts = $TaskParameter['NpmScript']
    $npmScriptCount = $npmScripts | Measure-Object | Select-Object -ExpandProperty 'Count'
    $numSteps = 4 + $npmScriptCount
    $stepNum = 0

    $activity = 'Running Node Task'

    function Update-Progress
    {
        param(
            [Parameter(Mandatory=$true)]
            [string]
            $Status,

            [int]
            $Step
        )

        Write-Progress -Activity $activity -Status $Status.TrimEnd('.') -PercentComplete ($Step/$numSteps*100)
    }

    $workingDirectory = (Get-Location).ProviderPath
    $originalPath = $env:PATH

    try
    {
    $nodePath = Resolve-WhiskeyNodePath -BuildRoot $TaskContext.BuildRoot
    
        Set-Item -Path 'env:PATH' -Value ('{0}{1}{2}' -f ($nodePath | Split-Path),[IO.Path]::PathSeparator,$env:PATH)

        Update-Progress -Status ('Installing NPM packages') -Step ($stepNum++)
        Write-WhiskeyTiming -Message ('npm install')
        Invoke-WhiskeyNpmCommand -Name 'install' -ArgumentList '--production=false' -BuildRootPath $TaskContext.BuildRoot -ForDeveloper:$TaskContext.ByDeveloper -ErrorAction Stop
        Write-WhiskeyTiming -Message ('COMPLETE')

        if( $TaskContext.ShouldInitialize )
        {
            Write-WhiskeyTiming -Message 'Initialization complete.'
            return
        }

        if( -not $npmScripts )
        {
            Write-WhiskeyWarning -Context $TaskContext -Message (@'
Property 'NpmScript' is missing or empty. Your build isn''t *doing* anything. The 'NpmScript' property should be a list of one or more npm scripts to run during your build, e.g.
 
Build:
- Node:
  NpmScript:
  - build
  - test
'@
)
        }

        foreach( $script in $npmScripts )
        {
            Update-Progress -Status ('npm run {0}' -f $script) -Step ($stepNum++)
            Write-WhiskeyTiming -Message ('Running script ''{0}''.' -f $script)
            Invoke-WhiskeyNpmCommand -Name 'run-script' -ArgumentList $script -BuildRootPath $TaskContext.BuildRoot -ForDeveloper:$TaskContext.ByDeveloper -ErrorAction Stop
            Write-WhiskeyTiming -Message ('COMPLETE')
        }

        $nodePath = Resolve-WhiskeyNodePath -BuildRootPath $TaskContext.BuildRoot

        Update-Progress -Status ('nsp check') -Step ($stepNum++)
        Write-WhiskeyTiming -Message ('Running NSP security check.')
        $nspPath = Assert-WhiskeyNodeModulePath -Path $TaskParameter['NspPath'] -CommandPath 'bin\nsp' -ErrorAction Stop
        $output = & $nodePath $nspPath 'check' '--output' 'json' 2>&1 |
                        ForEach-Object { if( $_ -is [Management.Automation.ErrorRecord]) { $_.Exception.Message } else { $_ } }
        Write-WhiskeyTiming -Message ('COMPLETE')
        $results = ($output -join [Environment]::NewLine) | ConvertFrom-Json
        if( $LASTEXITCODE )
        {
            $summary = $results | Format-List | Out-String
            Stop-WhiskeyTask -TaskContext $TaskContext -Message ('NSP, the Node Security Platform, found the following security vulnerabilities in your dependencies (exit code: {0}):{1}{2}' -f $LASTEXITCODE,[Environment]::NewLine,$summary)
            return
        }

        Update-Progress -Status ('license-checker') -Step ($stepNum++)
        Write-WhiskeyTiming -Message ('Generating license report.')

        $licenseCheckerPath = Assert-WhiskeyNodeModulePath -Path $TaskParameter['LicenseCheckerPath'] -CommandPath 'bin\license-checker' -ErrorAction Stop

        $reportJson = & $nodePath $licenseCheckerPath '--json'
        Write-WhiskeyTiming -Message ('COMPLETE')
        $report = ($reportJson -join [Environment]::NewLine) | ConvertFrom-Json
        if( -not $report )
        {
            Stop-WhiskeyTask -TaskContext $TaskContext -Message ('License Checker failed to output a valid JSON report.')
            return
        }

        Write-WhiskeyTiming -Message ('Converting license report.')
        # The default license checker report has a crazy format. It is an object with properties for each module.
        # Let's transform it to a more sane format: an array of objects.
        [object[]]$newReport = $report |
                                    Get-Member -MemberType NoteProperty |
                                    Select-Object -ExpandProperty 'Name' |
                                    ForEach-Object { $report.$_ | Add-Member -MemberType NoteProperty -Name 'name' -Value $_ -PassThru }

        # show the report
        $newReport | Sort-Object -Property 'licenses','name' | Format-Table -Property 'licenses','name' -AutoSize | Out-String | Write-WhiskeyVerbose -Context $TaskContext

        $licensePath = 'node-license-checker-report.json'
        $licensePath = Join-Path -Path $TaskContext.OutputDirectory -ChildPath $licensePath
        ConvertTo-Json -InputObject $newReport -Depth 100 | Set-Content -Path $licensePath
        Write-WhiskeyTiming -Message ('COMPLETE')
    }
    finally
    {
        Set-Item -Path 'env:PATH' -Value $originalPath
        Write-Progress -Activity $activity -Completed -PercentComplete 100
    }
}



function Invoke-WhiskeyNodeLicenseChecker
{
    [CmdletBinding()]
    [Whiskey.Task('NodeLicenseChecker')]
    [Whiskey.RequiresTool('Node', 'NodePath',VersionParameterName='NodeVersion')]
    [Whiskey.RequiresTool('NodeModule::license-checker', 'LicenseCheckerPath', VersionParameterName='Version')]
    param(
        [Parameter(Mandatory=$true)]
        [Whiskey.Context]
        $TaskContext,

        [Parameter(Mandatory=$true)]
        [hashtable]
        $TaskParameter
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    $licenseCheckerPath = Assert-WhiskeyNodeModulePath -Path $TaskParameter['LicenseCheckerPath'] -CommandPath 'bin\license-checker' -ErrorAction Stop

    $nodePath = Assert-WhiskeyNodePath -Path $TaskParameter['NodePath'] -ErrorAction Stop

    Write-WhiskeyTiming -Message ('Generating license report')
    $reportJson = Invoke-Command -NoNewScope -ScriptBlock {
        & $nodePath $licenseCheckerPath '--json'
    }
    Write-WhiskeyTiming -Message ('COMPLETE')

    $report = Invoke-Command -NoNewScope -ScriptBlock {
        ($reportJson -join [Environment]::NewLine) | ConvertFrom-Json
    }
    if (-not $report)
    {
        Stop-WhiskeyTask -TaskContext $TaskContext -Message 'License Checker failed to output a valid JSON report.'
        return
    }

    Write-WhiskeyTiming -Message 'Converting license report.'
    # The default license checker report has a crazy format. It is an object with properties for each module.
    # Let's transform it to a more sane format: an array of objects.
    [object[]]$newReport = $report |
                                Get-Member -MemberType NoteProperty |
                                Select-Object -ExpandProperty 'Name' |
                                ForEach-Object { $report.$_ | Add-Member -MemberType NoteProperty -Name 'name' -Value $_ -PassThru }

    # show the report
    $newReport | Sort-Object -Property 'licenses','name' | Format-Table -Property 'licenses','name' -AutoSize | Out-String | Write-WhiskeyVerbose -Context $TaskContext

    $licensePath = 'node-license-checker-report.json'
    $licensePath = Join-Path -Path $TaskContext.OutputDirectory -ChildPath $licensePath
    ConvertTo-Json -InputObject $newReport -Depth 100 | Set-Content -Path $licensePath
    Write-WhiskeyTiming -Message ('COMPLETE')
}



function Invoke-WhiskeyNodeNspCheck
{
    [Whiskey.Task('NodeNspCheck',Obsolete,ObsoleteMessage='The "NodeNspCheck" task is obsolete and will be removed in a future version of Whiskey. Please use the "Npm" task instead. The NSP project shut down in September 2018 and was replaced with the `npm audit` command.')]
    [Whiskey.RequiresTool('Node', 'NodePath', VersionParameterName='NodeVersion')]
    [Whiskey.RequiresTool('NodeModule::nsp', 'NspPath', VersionParameterName='Version')]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [Whiskey.Context]
        $TaskContext,

        [Parameter(Mandatory=$true)]
        [hashtable]
        $TaskParameter
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    $nspPath = Assert-WhiskeyNodeModulePath -Path $TaskParameter['NspPath'] -CommandPath 'bin\nsp' -ErrorAction Stop

    $nodePath = Assert-WhiskeyNodePath -Path $TaskParameter['NodePath'] -ErrorAction Stop

    $formattingArg = '--reporter'
    $isPreNsp3 = $TaskParameter.ContainsKey('Version') -and $TaskParameter['Version'] -match '^(0|1|2)\.'
    if( $isPreNsp3 )
    {
        $formattingArg = '--output'
    }

    Write-WhiskeyTiming -Message 'Running NSP security check'
    $output = Invoke-Command -NoNewScope -ScriptBlock {
        param(
            $JsonOutputFormat
        )

        & $nodePath $nspPath 'check' $JsonOutputFormat 'json' 2>&1 |
            ForEach-Object { if( $_ -is [Management.Automation.ErrorRecord]) { $_.Exception.Message } else { $_ } }
    } -ArgumentList $formattingArg

    Write-WhiskeyTiming -Message 'COMPLETE'

    try
    {
        $results = ($output -join [Environment]::NewLine) | ConvertFrom-Json
    }
    catch
    {
        Stop-WhiskeyTask -TaskContext $TaskContext -Message ('NSP, the Node Security Platform, did not run successfully as it did not return valid JSON (exit code: {0}):{1}{2}' -f $LASTEXITCODE,[Environment]::NewLine,$output)
        return
    }

    if ($Global:LASTEXITCODE -ne 0)
    {
        $summary = $results | Format-List | Out-String
        Stop-WhiskeyTask -TaskContext $TaskContext -Message ('NSP, the Node Security Platform, found the following security vulnerabilities in your dependencies (exit code: {0}):{1}{2}' -f $LASTEXITCODE,[Environment]::NewLine,$summary)
        return
    }
}



function Invoke-WhiskeyNpm
{
    [Whiskey.Task('Npm')]
    [Whiskey.RequiresTool('Node','NodePath',VersionParameterName='NodeVersion')]
    [Whiskey.RequiresTool('NodeModule::npm','NpmPath',VersionParameterName='NpmVersion')]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [object]
        $TaskContext,

        [Parameter(Mandatory=$true)]
        [hashtable]
        $TaskParameter
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    $commandName = $TaskParameter['Command']
    if( -not $commandName )
    {
        Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Property "Command" is required. It should be the name of the NPM command to run. See https://docs.npmjs.com/cli#cli for a list.')
        return
    }

    Invoke-WhiskeyNpmCommand -Name $commandName -BuildRootPath $TaskContext.BuildRoot -ArgumentList $TaskParameter['Argument'] -ErrorAction Stop

}



function Invoke-WhiskeyNpmConfig
{
    [Whiskey.Task('NpmConfig',Obsolete,ObsoleteMessage='The "NpmConfig" task is obsolete. It will be removed in a future version of Whiskey. Please use the "Npm" task instead.')]
    [Whiskey.RequiresTool('Node','NodePath',VersionParameterName='NodeVersion')]
    param(
        [Parameter(Mandatory=$true)]
        [Whiskey.Context]
        $TaskContext,

        [Parameter(Mandatory=$true)]
        [hashtable]
        $TaskParameter
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    $configuration = $TaskParameter['Configuration']
    if( -not $configuration )
    {
        Write-Warning -Message ('Your NpmConfig task isn''t doing anything. Its Configuration property is missing. Please update the NpmConfig task in your whiskey.yml file so that it is actually setting configuration, e.g.
 
    Build:
    - NpmConfig:
        Configuration:
            key1: value1
            key2: value2
            '
)
        return
    }

    if( -not ($configuration | Get-Member -Name 'Keys') )
    {
        Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Configuration property is invalid. It must have only key/value pairs, e.g.
 
    Build:
    - NpmConfig:
        Configuration:
            key1: value1
            key2: value2
     '
)
        return
    }

    $scope = $TaskParameter['Scope']
    if( $scope )
    {
        if( @('Project', 'User', 'Global') -notcontains $scope )
        {
            Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Scope property ''{0}'' is invalid. Allowed values are `Project`, `User`, `Global` to set configuration at the project, user, or global level. You may also remove the `Scope` property to set configuration at the project level (i.e. in the current directory).' -f $scope)
            return
        }
    }

    foreach( $key in $TaskParameter['Configuration'].Keys )
    {
        $argumentList = & {
                                'set'
                                $key
                                $configuration[$key]
                                if( $scope -eq 'User' )
                                {
                                }
                                elseif( $scope -eq 'Global' )
                                {
                                    '-g'
                                }
                                else
                                {
                                    '-userconfig'
                                    '.npmrc'
                                }
                        }

        Invoke-WhiskeyNpmCommand -Name 'config' -ArgumentList $argumentList -BuildRootPath $TaskContext.BuildRoot -ForDeveloper:$TaskContext.ByDeveloper
    }

}


function Invoke-WhiskeyNpmInstall
{
    [Whiskey.Task('NpmInstall',SupportsClean,Obsolete,ObsoleteMessage='The "NpmInstall" task is obsolete. It will be removed in a future version of Whiskey. Please use the "Npm" task instead.')]
    [Whiskey.RequiresTool('Node', 'NodePath',VersionParameterName='NodeVersion')]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [Whiskey.Context]
        $TaskContext,

        [Parameter(Mandatory=$true)]
        [hashtable]
        $TaskParameter
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    $workingDirectory = (Get-Location).ProviderPath

    if( -not $TaskParameter['Package'] )
    {
        if( $TaskContext.ShouldClean )
        {
            Write-WhiskeyTiming -Message 'Removing project node_modules'
            Remove-WhiskeyFileSystemItem -Path 'node_modules' -ErrorAction Stop
        }
        else
        {
            Write-WhiskeyTiming -Message 'Installing Node modules'
            Invoke-WhiskeyNpmCommand -Name 'install' -ArgumentList '--production=false' -BuildRootPath $TaskContext.BuildRoot -ForDeveloper:$TaskContext.ByDeveloper -ErrorAction Stop
        }
        Write-WhiskeyTiming -Message 'COMPLETE'
    }
    else
    {
        $installGlobally = $false
        if( $TaskParameter.ContainsKey('Global') )
        {
            $installGlobally = $TaskParameter['Global'] | ConvertFrom-WhiskeyYamlScalar
        }

        foreach( $package in $TaskParameter['Package'] )
        {
            $packageVersion = ''
            if ($package | Get-Member -Name 'Keys')
            {
                $packageName = $package.Keys | Select-Object -First 1
                $packageVersion = $package[$packageName]
            }
            else
            {
                $packageName = $package
            }

            if( $TaskContext.ShouldClean )
            {
                if( $TaskParameter.ContainsKey('NodePath') -and (Test-Path -Path $TaskParameter['NodePath'] -PathType Leaf) )
                {
                    Write-WhiskeyTiming -Message ('Uninstalling {0}' -f $packageName)
                    Uninstall-WhiskeyNodeModule -BuildRootPath $TaskContext.BuildRoot `
                                                -Name $packageName `
                                                -ForDeveloper:$TaskContext.ByDeveloper `
                                                -Global:$installGlobally `
                                                -ErrorAction Stop
                }
            }
            else
            {
                Write-WhiskeyTiming -Message ('Installing {0}' -f $packageName)
                Install-WhiskeyNodeModule -BuildRootPath $TaskContext.BuildRoot `
                                          -Name $packageName `
                                          -Version $packageVersion `
                                          -ForDeveloper:$TaskContext.ByDeveloper `
                                          -Global:$installGlobally `
                                          -ErrorAction Stop
            }
            Write-WhiskeyTiming -Message 'COMPLETE'
        }
    }
}



function Invoke-WhiskeyNpmPrune
{
    [Whiskey.Task('NpmPrune',Obsolete,ObsoleteMessage='The "NpmPrune" task is obsolete. It will be removed in a future version of Whiskey. Please use the "Npm" task instead.')]
    [Whiskey.RequiresTool('Node','NodePath',VersionParameterName='NodeVersion')]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [Whiskey.Context]
        # The context the task is running under.
        $TaskContext,

        [Parameter(Mandatory=$true)]
        [hashtable]
        # The parameters/configuration to use to run the task.
        $TaskParameter
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    Invoke-WhiskeyNpmCommand -Name 'prune' -ArgumentList '--production' -BuildRootPath $TaskContext.BuildRoot -ForDeveloper:$TaskContext.ByDeveloper -ErrorAction Stop
}



function Invoke-WhiskeyNpmRunScript
{
    [Whiskey.Task('NpmRunScript',Obsolete,ObsoleteMessage='The "NpmRunScriptTask" is obsolete. It will be removed in a future version of Whiskey. Please use the "Npm" task instead.')]
    [Whiskey.RequiresTool('Node','NodePath',VersionParameterName='NodeVersion')]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [Whiskey.Context]
        $TaskContext,

        [Parameter(Mandatory=$true)]
        [hashtable]
        $TaskParameter
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    $npmScripts = $TaskParameter['Script']
    if (-not $npmScripts)
    {
        Stop-WhiskeyTask -TaskContext $TaskContext -Message 'Property ''Script'' is mandatory. It should be a list of one or more npm scripts to run during your build, e.g.,
 
        Build:
        - NpmRunScript:
            Script:
            - build
            - test
 
        '

        return
    }

    foreach ($script in $npmScripts)
    {
        Write-WhiskeyTiming -Message ('Running script ''{0}''.' -f $script)
        Invoke-WhiskeyNpmCommand -Name 'run-script' -ArgumentList $script -BuildRootPath $TaskContext.BuildRoot -ForDeveloper:$TaskContext.ByDeveloper -ErrorAction Stop
        Write-WhiskeyTiming -Message ('COMPLETE')
    }
}



function New-WhiskeyNuGetPackage
{
    [Whiskey.Task("NuGetPack",Platform='Windows')]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [Whiskey.Context]
        $TaskContext,

        [Parameter(Mandatory=$true)]
        [hashtable]
        $TaskParameter
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    if( -not ($TaskParameter.ContainsKey('Path')))
    {
        Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Property ''Path'' is mandatory. It should be one or more paths to .csproj or .nuspec files to pack, e.g.
 
    Build:
    - PublishNuGetPackage:
        Path:
        - MyProject.csproj
        - MyNuspec.nuspec
    '
)
        return
    }

    $paths = $TaskParameter['Path'] | Resolve-WhiskeyTaskPath -TaskContext $TaskContext -PropertyName 'Path'

    $symbols = $TaskParameter['Symbols'] | ConvertFrom-WhiskeyYamlScalar
    $symbolsArg = ''
    $symbolsFileNameSuffix = ''
    if( $symbols )
    {
        $symbolsArg = '-Symbols'
        $symbolsFileNameSuffix = '.symbols'
    }

    $nuGetPath = Install-WhiskeyNuGet -DownloadRoot $TaskContext.BuildRoot -Version $TaskParameter['Version']
    if( -not $nugetPath )
    {
        return
    }

    $properties = $TaskParameter['Properties']
    $propertiesArgs = @()
    if( $properties )
    {
        if( -not (Get-Member -InputObject $properties -Name 'Keys') )
        {
            Stop-WhiskeyTask -TaskContext $TaskContext -PropertyName 'Properties' -Message ('Property is invalid. This property must be a name/value mapping of properties to pass to nuget.exe pack command''s "-Properties" parameter.')
            return
        }

        $propertiesArgs = $properties.Keys |
                                ForEach-Object {
                                    '-Properties'
                                    '{0}={1}' -f $_,$properties[$_]
                                }
    }

    foreach ($path in $paths)
    {
        $projectName = $TaskParameter['PackageID']
        if( -not $projectName )
        {
            $projectName = [IO.Path]::GetFileNameWithoutExtension(($path | Split-Path -Leaf))
        }
        $packageVersion = $TaskParameter['PackageVersion']
        if( -not $packageVersion )
        {
            $packageVersion = $TaskContext.Version.SemVer1
        }

        # Create NuGet package
        $configuration = Get-WhiskeyMSBuildConfiguration -Context $TaskContext

        & $nugetPath pack -Version $packageVersion -OutputDirectory $TaskContext.OutputDirectory $symbolsArg -Properties ('Configuration={0}' -f $configuration) $propertiesArgs $path

        # Make sure package was created.
        $filename = '{0}.{1}{2}.nupkg' -f $projectName,$packageVersion,$symbolsFileNameSuffix

        $packagePath = Join-Path -Path $TaskContext.OutputDirectory -childPath $filename
        if( -not (Test-Path -Path $packagePath -PathType Leaf) )
        {
            Stop-WhiskeyTask -TaskContext $TaskContext -Message ('We ran nuget pack against ''{0}'' but the expected NuGet package ''{1}'' does not exist.' -f $path,$packagePath)
            return
        }
    }
}



function Publish-WhiskeyNuGetPackage
{
    [Whiskey.Task("NuGetPush",Platform='Windows',Aliases=('PublishNuGetLibrary','PublishNuGetPackage'),WarnWhenUsingAlias=$true)]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [Whiskey.Context]
        $TaskContext,

        [Parameter(Mandatory)]
        [hashtable]
        $TaskParameter,

        [Whiskey.Tasks.ValidatePath(PathType='File')]
        [string[]]
        $Path
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    if( -not $Path )
    {
        $Path = 
            Join-Path -Path $TaskContext.OutputDirectory.FullName -ChildPath '*.nupkg' |
            Resolve-Path |
            Select-Object -ExpandProperty 'ProviderPath'
    }

    $publishSymbols = $TaskParameter['Symbols'] | ConvertFrom-WhiskeyYamlScalar

    $paths = $Path |
                Where-Object {
                    $wildcard = '*.symbols.nupkg'
                    if( $publishSymbols )
                    {
                        $_ -like $wildcard
                    }
                    else
                    {
                        $_ -notlike $wildcard
                    }
                }

    $source = $TaskParameter['Uri']
    if( -not $source )
    {
        Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Property ''Uri'' is mandatory. It should be the URI where NuGet packages should be published, e.g.
 
    Build:
    - PublishNuGetPackage:
        Uri: https://nuget.org
    '
)
        return
    }

    $apiKeyID = $TaskParameter['ApiKeyID']
    if( -not $apiKeyID )
    {
        Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Property ''ApiKeyID'' is mandatory. It should be the ID/name of the API key to use when publishing NuGet packages to {0}, e.g.:
 
    Build:
    - PublishNuGetPackage:
        Uri: {0}
        ApiKeyID: API_KEY_ID
 
Use the `Add-WhiskeyApiKey` function to add the API key to the build.
 
            '
 -f $source)
        return
    }
    $apiKey = Get-WhiskeyApiKey -Context $TaskContext -ID $apiKeyID -PropertyName 'ApiKeyID'

    $nuGetPath = Install-WhiskeyNuGet -DownloadRoot $TaskContext.BuildRoot -Version $TaskParameter['Version']
    if( -not $nugetPath )
    {
        return
    }

    foreach ($packagePath in $paths)
    {
        $packageFilename = [IO.Path]::GetFileNameWithoutExtension(($packagePath | Split-Path -Leaf))
        $packageName = $packageFilename -replace '\.\d+\.\d+\.\d+(-.*)?(\.symbols)?',''

        $packageFilename -match '(\d+\.\d+\.\d+(?:-[0-9a-z]+)?)'
        $packageVersion = $Matches[1]

        $packageUri = '{0}/package/{1}/{2}' -f $source,$packageName,$packageVersion

        # Make sure this version doesn't exist.
        $packageExists = $false
        $numErrorsAtStart = $Global:Error.Count
        try
        {
            Invoke-WebRequest -Uri $packageUri -UseBasicParsing | Out-Null
            $packageExists = $true
        }
        catch
        {
            # Invoke-WebRequest throws differnt types of errors in Windows PowerShell and PowerShell Core. Handle the case where a non-HTTP exception occurs.
            if( -not ($_.Exception | Get-Member 'Response') )
            {
                Write-Error -ErrorRecord $_
                Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Unknown failure checking if {0} {1} package already exists at {2}. {3}' -f  $packageName,$packageVersion,$packageUri,$_)
                return
            }

            $response = $_.Exception.Response
            if( $response.StatusCode -ne [Net.HttpStatusCode]::NotFound )
            {
                $error = ''
                if( $response | Get-Member 'GetResponseStream' )
                {
                    $content = $response.GetResponseStream()
                    $content.Position = 0
                    $reader = New-Object 'IO.StreamReader' $content
                    $error = $reader.ReadToEnd() -replace '<[^>]+?>',''
                    $reader.Close()
                    $response.Close()
                }
                Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Failure checking if {0} {1} package already exists at {2}. The web request returned a {3} ({4}) status code:{5} {5}{6}' -f $packageName,$packageVersion,$packageUri,$response.StatusCode,[int]$response.StatusCode,[Environment]::NewLine,$error)
                return
            }

            for( $idx = 0; $idx -lt ($Global:Error.Count - $numErrorsAtStart); ++$idx )
            {
                $Global:Error.RemoveAt(0)
            }
        }

        if( $packageExists )
        {
            Stop-WhiskeyTask -TaskContext $TaskContext -Message ('{0} {1} already exists. Please increment your library''s version number in ''{2}''.' -f $packageName,$packageVersion,$TaskContext.ConfigurationPath)
            return
        }

        # Publish package and symbols to NuGet
        Invoke-WhiskeyNuGetPush -Path $packagePath -Uri $source -ApiKey $apiKey -NuGetPath $nuGetPath

        if( -not ($TaskParameter['SkipUploadedCheck'] | ConvertFrom-WhiskeyYamlScalar) )
        {
            try
            {
                Invoke-WebRequest -Uri $packageUri -UseBasicParsing | Out-Null
            }
            catch
            {
                # Invoke-WebRequest throws differnt types of errors in Windows PowerShell and PowerShell Core. Handle the case where a non-HTTP exception occurs.
                if( -not ($_.Exception | Get-Member 'Response') )
                {
                    Write-Error -ErrorRecord $_
                    Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Unknown failure checking if {0} {1} package was published to {2}. {3}' -f  $packageName,$packageVersion,$packageUri,$_)
                    return
                }

                Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Failed to publish NuGet package {0} {1} to {2}. When we checked if that package existed, we got a {3} HTTP status code. Please see build output for more information.' -f $packageName,$packageVersion,$packageUri,$_.Exception.Response.StatusCode)
                return
            }
        }
    }
}



function Restore-WhiskeyNuGetPackage
{
    [CmdletBinding()]
    [Whiskey.TaskAttribute("NuGetRestore",Platform='Windows')]
    param(
        [Parameter(Mandatory)]
        [Whiskey.Tasks.ValidatePath(Mandatory)]
        [string[]]
        $Path,

        [string[]]
        $Argument,

        [string]
        $Version,

        [Whiskey.Tasks.ParameterValueFromVariable('WHISKEY_BUILD_ROOT')]
        [IO.DirectoryInfo]
        $BuildRoot
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    $nuGetPath = Install-WhiskeyNuGet -DownloadRoot $BuildRoot -Version $Version

    foreach( $item in $Path )
    {
        & $nuGetPath 'restore' $item $Argument
    }
}


function Invoke-WhiskeyNUnit2Task
{
    [Whiskey.Task("NUnit2",SupportsClean, SupportsInitialize,Platform='Windows')]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [Whiskey.Context]
        $TaskContext,

        [Parameter(Mandatory=$true)]
        [hashtable]
        $TaskParameter
    )

    Set-StrictMode -version 'latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    $package = 'NUnit.Runners'
    $version = '2.6.4'
    if( $TaskParameter['Version'] )
    {
        $version = $TaskParameter['Version']
        if( $version -notlike '2.*' )
        {
            Stop-WhiskeyTask -TaskContext $TaskContext -PropertyName 'Version' -Message ('The version ''{0}'' isn''t a valid 2.x version of NUnit.' -f $TaskParameter['Version'])
            return
        }
    }

    $openCoverVersionArg  = @{}
    $reportGeneratorVersionArg = @{}
    if( $TaskParameter['OpenCoverVersion'] )
    {
        $openCoverVersionArg['Version'] = $TaskParameter['OpenCoverVersion']
    }
    if( $TaskParameter['ReportGeneratorVersion'] )
    {
        $reportGeneratorVersionArg['Version'] = $TaskParameter['ReportGeneratorVersion']
    }

    $openCoverArgs = @()
    if( $TaskParameter['OpenCoverArgument'] )
    {
        $openCoverArgs += $TaskParameter['OpenCoverArgument']
    }

    $reportGeneratorArgs = @()
    if( $TaskParameter['ReportGeneratorArgument'] )
    {
        $reportGeneratorArgs += $TaskParameter['ReportGeneratorArgument']
    }

    if( $TaskContext.ShouldClean )
    {
        Write-WhiskeyTiming -Message ('Uninstalling ReportGenerator.')
        Uninstall-WhiskeyTool -NuGetPackageName 'ReportGenerator' -BuildRoot $TaskContext.BuildRoot @reportGeneratorVersionArg
        Write-WhiskeyTiming -Message ('COMPLETE')
        Write-WhiskeyTiming -Message ('Uninstalling OpenCover.')
        Uninstall-WhiskeyTool -NuGetPackageName 'OpenCover' -BuildRoot $TaskContext.BuildRoot @openCoverVersionArg
        Write-WhiskeyTiming -Message ('COMPLETE')
        Write-WhiskeyTiming -Message ('Uninstalling NUnit.')
        Uninstall-WhiskeyTool -NuGetPackageName $package -BuildRoot $TaskContext.BuildRoot -Version $version
        Write-WhiskeyTiming -Message ('COMPLETE')
        return
    }

    $includeParam = $null
    if( $TaskParameter.ContainsKey('Include') )
    {
        $includeParam = '/include={0}' -f $TaskParameter['Include']
    }

    $excludeParam = $null
    if( $TaskParameter.ContainsKey('Exclude') )
    {
        $excludeParam = '/exclude={0}' -f $TaskParameter['Exclude']
    }

    $frameworkParam = '4.0'
    if( $TaskParameter.ContainsKey('Framework') )
    {
        $frameworkParam = $TaskParameter['Framework']
    }
    $frameworkParam = '/framework={0}' -f $frameworkParam

    Write-WhiskeyTiming -Message ('Installing NUnit.')
    $nunitRoot = Install-WhiskeyTool -NuGetPackageName $package -Version $version -DownloadRoot $TaskContext.BuildRoot
    Write-WhiskeyTiming -Message ('COMPLETE')
    if( -not (Test-Path -Path $nunitRoot -PathType Container) )
    {
        Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Package {0} {1} failed to install!' -f $package,$version)
        return
    }
    $nunitRoot = Get-Item -Path $nunitRoot | Select-Object -First 1
    $nunitRoot = Join-Path -Path $nunitRoot -ChildPath 'tools'
    $nunitConsolePath = Join-Path -Path $nunitRoot -ChildPath 'nunit-console.exe' -Resolve
    if( -not ($nunitConsolePath))
    {
        Stop-WhiskeyTask -TaskContext $TaskContext -Message ('{0} {1} was installed, but couldn''t find nunit-console.exe at ''{2}''.' -f $package,$version,$nunitConsolePath)
        return
    }

    Write-WhiskeyTiming -Message ('Installing OpenCover.')
    $openCoverRoot = Install-WhiskeyTool -NuGetPackageName 'OpenCover' -DownloadRoot $TaskContext.BuildRoot @openCoverVersionArg
    Write-WhiskeyTiming -Message ('COMPLETE')
    if( -not (Test-Path -Path $openCoverRoot -PathType Container))
    {
        Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Failed to install NuGet package OpenCover {0}.' -f $version)
        return
    }
    $openCoverPath = Get-ChildItem -Path $openCoverRoot -Filter 'OpenCover.Console.exe' -Recurse |
                        Select-Object -First 1 |
                        Select-Object -ExpandProperty 'FullName'
    if( -not (Test-Path -Path $openCoverPath -PathType Leaf) )
    {
        Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Unable to find OpenCover.Console.exe in OpenCover NuGet package at ''{0}''.' -f $openCoverRoot)
        return
    }

    Write-WhiskeyTiming -Message ('Installing ReportGenerator.')
    $reportGeneratorRoot = Install-WhiskeyTool -NuGetPackageName 'ReportGenerator' -DownloadRoot $TaskContext.BuildRoot @reportGeneratorVersionArg
    Write-WhiskeyTiming -Message ('COMPLETE')
    if( -not (Test-Path -Path $reportGeneratorRoot -PathType Container))
    {
        Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Failed to install NuGet package ReportGenerator.' -f $version)
        return
    }
    $reportGeneratorPath = Get-ChildItem -Path $reportGeneratorRoot -Filter 'ReportGenerator.exe' -Recurse |
                                Select-Object -First 1 |
                                Select-Object -ExpandProperty 'FullName'
    if( -not (Test-Path -Path $reportGeneratorPath -PathType Leaf) )
    {
        Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Unable to find ReportGenerator.exe in ReportGenerator NuGet package at ''{0}''.' -f $reportGeneratorRoot)
        return
    }

    if( $TaskContext.ShouldInitialize )
    {
        return
    }

    # Be sure that the Taskparameter contains a 'Path'.
    if( -not ($TaskParameter.ContainsKey('Path')))
    {
        Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Element ''Path'' is mandatory. It should be one or more paths, which should be a list of assemblies whose tests to run, e.g.
 
        Build:
        - NUnit2:
            Path:
            - Assembly.dll
            - OtherAssembly.dll'
)
        return
    }

    $path = $TaskParameter['Path'] | Resolve-WhiskeyTaskPath -TaskContext $TaskContext -PropertyName 'Path'
    $reportPath = Join-Path -Path $TaskContext.OutputDirectory -ChildPath ('nunit2+{0}.xml' -f [IO.Path]::GetRandomFileName())

    $coverageReportDir = Join-Path -Path $TaskContext.outputDirectory -ChildPath "opencover"
    New-Item -Path $coverageReportDir -ItemType 'Directory' -Force | Out-Null
    $openCoverReport = Join-Path -Path $coverageReportDir -ChildPath 'openCover.xml'

    $extraArgs = $TaskParameter['Argument'] | Where-Object { $_ }
    
    Write-WhiskeyVerbose -Context $TaskContext -Message (' Path {0}' -f ($Path | Select-Object -First 1))
    $Path | Select-Object -Skip 1 | ForEach-Object { Write-WhiskeyVerbose -Context $TaskContext -Message (' {0}' -f $_) }
    Write-WhiskeyVerbose -Context $TaskContext -Message (' Framework {0}' -f $frameworkParam)
    Write-WhiskeyVerbose -Context $TaskContext -Message (' Include {0}' -f $includeParam)
    Write-WhiskeyVerbose -Context $TaskContext -Message (' Exclude {0}' -f $excludeParam)
    Write-WhiskeyVerbose -Context $TaskContext -Message (' Argument /xml={0}' -f $reportPath)
    $extraArgs | ForEach-Object { Write-WhiskeyVerbose -Context $TaskContext -Message (' {0}' -f $_) }
    Write-WhiskeyVerbose -Context $TaskContext -Message (' CoverageFilter {0}' -f ($TaskParameter['CoverageFilter'] | Select-Object -First 1))
    $TaskParameter['CoverageFilter'] | Select-Object -Skip 1 | ForEach-Object { Write-WhiskeyVerbose -Context $TaskContext -Message (' {0}' -f $_) }
    Write-WhiskeyVerbose -Context $TaskContext -Message (' Output {0}' -f $openCoverReport)
    $disableCodeCoverage = $TaskParameter['DisableCodeCoverage'] | ConvertFrom-WhiskeyYamlScalar
    Write-WhiskeyVerbose -Context $TaskContext -Message (' DisableCodeCoverage {0}' -f $disableCodeCoverage)
    Write-WhiskeyVerbose -Context $TaskContext -Message (' OpenCoverArgs {0}' -f ($openCoverArgs | Select-Object -First 1))
    $openCoverArgs | Select-Object -Skip 1 | ForEach-Object { Write-WhiskeyVerbose -Context $TaskContext -Message (' {0}' -f $_) }
    Write-WhiskeyVerbose -Context $TaskContext -Message (' ReportGeneratorArgs {0}' -f ($reportGeneratorArgs | Select-Object -First 1))
    $reportGeneratorArgs | Select-Object -Skip 1 | ForEach-Object { Write-WhiskeyVerbose -Context $TaskContext -Message (' {0}' -f $_) }

    if( -not $disableCodeCoverage )
    {
        $coverageFilterString = ($TaskParameter['CoverageFilter'] -join " ")
        $extraArgString = ($extraArgs -join " ")
        $pathsArg = ($path -join '" "')
        $nunitArgs = '"{0}" /noshadow {1} /xml="{2}" {3} {4} {5}' -f $pathsArg,$frameworkParam,$reportPath,$includeParam,$excludeParam,$extraArgString
        $nunitArgs = $nunitArgs -replace '"', '\"'
        Write-WhiskeyTiming -Message ('Running OpenCover')
        & $openCoverPath "-target:${nunitConsolePath}" "-targetargs:${nunitArgs}" "-filter:${coverageFilterString}" '-register:user' "-output:${openCoverReport}" '-returntargetcode' $openCoverArgs
        Write-WhiskeyTiming -Message ('COMPLETE')
        $testsFailed = $LastExitCode;
        Write-WhiskeyTiming -Message ('Running ReportGenerator')
        & $reportGeneratorPath "-reports:${openCoverReport}" "-targetdir:$coverageReportDir" $reportGeneratorArgs
        Write-WhiskeyTiming -Message ('COMPLETE')
        if( $LastExitCode -or $testsFailed )
        {
            Stop-WhiskeyTask -TaskContext $TaskContext -Message ('NUnit2 tests failed. {0} returned exit code {1}.' -f $openCoverPath,$LastExitCode)
            return
        }
    }
    else
    {
        Write-WhiskeyTiming -Message ('Running NUnit')
        & $nunitConsolePath $path $frameworkParam $includeParam $excludeParam $extraArgs ('/xml={0}' -f $reportPath)
        Write-WhiskeyTiming -Message ('COMPLETE')
        if( $LastExitCode )
        {
            Stop-WhiskeyTask -TaskContext $TaskContext -Message ('NUnit2 tests failed. {0} returned exit code {1}.' -f $nunitConsolePath,$LastExitCode)
            return
        }
    }
}



function Invoke-WhiskeyNUnit3Task
{
    [CmdletBinding()]
    [Whiskey.Task("NUnit3",SupportsClean,SupportsInitialize,Platform='Windows')]
    param(
        [Parameter(Mandatory=$true)]
        [Whiskey.Context]
        $TaskContext,

        [Parameter(Mandatory=$true)]
        [hashtable]
        $TaskParameter
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    # NUnit.Console pulls in ConsoleRunner (of the same version) as a dependency and several NUnit2 compatibility/extension packages.
    # The ConsoleRunner packages is installed explicitly to resolve the tool/bin path from installed package location.
    $nunitSupportPackage = 'NUnit.Console'
    $nunitPackage = 'NUnit.ConsoleRunner'

    # Due to a bug in NuGet we can't search for and install packages with wildcards (e.g. 3.*), so we're hardcoding a version for now. See Resolve-WhiskeyNuGetPackageVersion for more details.
    # (This is the vesrion of NUnit.Console/NUnit.ConsoleRunner which may differ from the core NUnit library version.)
    $nunitVersion = '3.10.0'
    if( $TaskParameter['Version'] )
    {
        $nunitVersion = $TaskParameter['Version']
        if( $nunitVersion -notlike '3.*' )
        {
            Stop-WhiskeyTask -TaskContext $TaskContext -PropertyName 'Version' -Message ('The version ''{0}'' isn''t a valid 3.x version of NUnit.' -f $TaskParameter['Version'])
            return
        }
    }

    $reportFormat = 'nunit3';
    if ($TaskParameter['ResultFormat'])
    {
        $reportFormat = $TaskParameter['ResultFormat']
    }

    # NUnit3 currently allows 'nunit2' and 'nunit3' which aligns with output filename usage
    $nunitReport = Join-Path -Path $TaskContext.OutputDirectory -ChildPath ('{0}+{1}.xml' -f  $reportFormat, [IO.Path]::GetRandomFileName())
    $nunitReportParam = '--result={0};format={1}' -f $nunitReport, $reportFormat

    $openCoverVersionParam = @{}
    if ($TaskParameter['OpenCoverVersion'])
    {
        $openCoverVersionParam['Version'] = $TaskParameter['OpenCoverVersion']
    }

    $reportGeneratorVersionParam = @{}
    if ($TaskParameter['ReportGeneratorVersion'])
    {
        $reportGeneratorVersionParam['Version'] = $TaskParameter['ReportGeneratorVersion']
    }

    if( $TaskContext.ShouldClean )
    {
        Uninstall-WhiskeyTool -NuGetPackageName $nunitSupportPackage -BuildRoot $TaskContext.BuildRoot -Version $nunitVersion
        Uninstall-WhiskeyTool -NuGetPackageName $nunitPackage -BuildRoot $TaskContext.BuildRoot -Version $nunitVersion
        Uninstall-WhiskeyTool -NuGetPackageName 'OpenCover' -BuildRoot $TaskContext.BuildRoot @openCoverVersionParam
        Uninstall-WhiskeyTool -NuGetPackageName 'ReportGenerator' -BuildRoot $TaskContext.BuildRoot @reportGeneratorVersionParam
        return
    }

    $nunitSupportPath = Install-WhiskeyTool -NuGetPackageName $nunitSupportPackage -Version $nunitVersion -DownloadRoot $TaskContext.BuildRoot
    if (-not $nunitSupportPath)
    {
        Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Package "{0}" failed to install.' -f $nunitSupportPackage)
        return
    }

    $nunitPath = Install-WhiskeyTool -NuGetPackageName $nunitPackage -Version $nunitVersion -DownloadRoot $TaskContext.BuildRoot
    if (-not $nunitPath)
    {
        Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Package "{0}" failed to install.' -f $nunitPackage)
        return
    }

    $openCoverPath = Install-WhiskeyTool -NuGetPackageName 'OpenCover' -DownloadRoot $TaskContext.BuildRoot @openCoverVersionParam
    if (-not $openCoverPath)
    {
        Stop-WhiskeyTask -TaskContext $TaskContext -Message 'Package "OpenCover" failed to install.'
        return
    }

    $reportGeneratorPath = Install-WhiskeyTool -NuGetPackageName 'ReportGenerator' -DownloadRoot $TaskContext.BuildRoot @reportGeneratorVersionParam
    if (-not $reportGeneratorPath)
    {
        Stop-WhiskeyTask -TaskContext $TaskContext -Message 'Package "ReportGenerator" failed to install.'
        return
    }

    if( $TaskContext.ShouldInitialize )
    {
        return
    }

    $openCoverArgument = @()
    if ($TaskParameter['OpenCoverArgument'])
    {
        $openCoverArgument = $TaskParameter['OpenCoverArgument']
    }

    $reportGeneratorArgument = @()
    if ($TaskParameter['ReportGeneratorArgument'])
    {
        $reportGeneratorArgument = $TaskParameter['ReportGeneratorArgument']
    }

    $framework = '4.0'
    if ($TaskParameter['Framework'])
    {
        $framework = $TaskParameter['Framework']
    }
    $frameworkParam = '--framework={0}' -f $framework

    $testFilter = ''
    $testFilterParam = ''
    if ($TaskParameter['TestFilter'])
    {
        $testFilter = $TaskParameter['TestFilter'] | ForEach-Object { '({0})' -f $_ }
        $testFilter = $testFilter -join ' or '
        $testFilterParam = '--where={0}' -f $testFilter
    }

    $nunitExtraArgument = ''
    if ($TaskParameter['Argument'])
    {
        $nunitExtraArgument = $TaskParameter['Argument']
    }

    $disableCodeCoverage = $TaskParameter['DisableCodeCoverage'] | ConvertFrom-WhiskeyYamlScalar

    $coverageFilter = ''
    if ($TaskParameter['CoverageFilter'])
    {
        $coverageFilter = $TaskParameter['CoverageFilter'] -join ' '
    }

    $nunitConsolePath = Get-ChildItem -Path $nunitPath -Filter 'nunit3-console.exe' -Recurse |
                            Select-Object -First 1 |
                            Select-Object -ExpandProperty 'FullName'

    if( -not $nunitConsolePath )
    {
        Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Unable to find "nunit3-console.exe" in NUnit3 NuGet package at "{0}".' -f $nunitPath)
        return
    }


    $openCoverConsolePath = Get-ChildItem -Path $openCoverPath -Filter 'OpenCover.Console.exe' -Recurse |
                                Select-Object -First 1 |
                                Select-Object -ExpandProperty 'FullName'

    if( -not $openCoverConsolePath )
    {
        Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Unable to find "OpenCover.Console.exe" in OpenCover NuGet package at "{0}".' -f $openCoverPath)
        return
    }


    $reportGeneratorConsolePath = Get-ChildItem -Path $reportGeneratorPath -Filter 'ReportGenerator.exe' -Recurse |
                                      Select-Object -First 1 |
                                      Select-Object -ExpandProperty 'FullName'

    if( -not $reportGeneratorConsolePath )
    {
        Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Unable to find "ReportGenerator.exe" in ReportGenerator NuGet package at "{0}".' -f $reportGeneratorPath)
        return
    }

    if (-not $TaskParameter['Path'])
    {
        Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Property ''Path'' is mandatory. It should be one or more paths to the assemblies whose tests should be run, e.g.
 
            Build:
            - NUnit3:
                Path:
                - Assembly.dll
                - OtherAssembly.dll
 
        '
)
        return
    }

    $path = $TaskParameter['Path'] | Resolve-WhiskeyTaskPath -TaskContext $TaskContext -PropertyName 'Path'
    $path | Foreach-Object {
        if (-not (Test-Path -Path $_ -PathType Leaf))
        {
            Stop-WhiskeyTask -TaskContext $TaskContext -Message ('''Path'' item ''{0}'' does not exist.' -f $_)
            return
        }
    }

    $coverageReportDir = Join-Path -Path $TaskContext.outputDirectory -ChildPath "opencover"
    New-Item -Path $coverageReportDir -ItemType 'Directory' -Force | Out-Null
    $openCoverReport = Join-Path -Path $coverageReportDir -ChildPath 'openCover.xml'

    $separator = '{0}VERBOSE: ' -f [Environment]::NewLine
    Write-WhiskeyVerbose -Context $TaskContext -Message (' Path {0}' -f ($Path -join $separator))
    Write-WhiskeyVerbose -Context $TaskContext -Message (' Framework {0}' -f $framework)
    Write-WhiskeyVerbose -Context $TaskContext -Message (' TestFilter {0}' -f $testFilter)
    Write-WhiskeyVerbose -Context $TaskContext -Message (' Argument {0}' -f ($nunitExtraArgument -join $separator))
    Write-WhiskeyVerbose -Context $TaskContext -Message (' NUnit Report {0}' -f $nunitReport)
    Write-WhiskeyVerbose -Context $TaskContext -Message (' CoverageFilter {0}' -f $coverageFilter)
    Write-WhiskeyVerbose -Context $TaskContext -Message (' OpenCover Report {0}' -f $openCoverReport)
    Write-WhiskeyVerbose -Context $TaskContext -Message (' DisableCodeCoverage {0}' -f $disableCodeCoverage)
    Write-WhiskeyVerbose -Context $TaskContext -Message (' OpenCoverArgs {0}' -f ($openCoverArgument -join ' '))
    Write-WhiskeyVerbose -Context $TaskContext -Message (' ReportGeneratorArgs {0}' -f ($reportGeneratorArgument -join ' '))

    $nunitExitCode = 0
    $reportGeneratorExitCode = 0
    $openCoverExitCode = 0
    $openCoverExitCodeOffset = 1000

    if (-not $disableCodeCoverage)
    {

        $path = $path | ForEach-Object { '\"{0}\"' -f $_ }
        $path = $path -join ' '

        $nunitReportParam = '\"{0}\"' -f $nunitReportParam

        if ($frameworkParam)
        {
            $frameworkParam = '\"{0}\"' -f $frameworkParam
        }

        if ($testFilterParam)
        {
            $testFilterParam = '\"{0}\"' -f $testFilterParam
        }

        if ($nunitExtraArgument)
        {
            $nunitExtraArgument = $nunitExtraArgument | ForEach-Object { '\"{0}\"' -f $_ }
            $nunitExtraArgument = $nunitExtraArgument -join ' '
        }

        $openCoverNunitArguments = '{0} {1} {2} {3} {4}' -f $path,$frameworkParam,$testFilterParam,$nunitReportParam,$nunitExtraArgument
        & $openCoverConsolePath "-target:$nunitConsolePath" "-targetargs:$openCoverNunitArguments" "-filter:$coverageFilter" "-output:$openCoverReport" -register:user -returntargetcode:$openCoverExitCodeOffset $openCoverArgument

        if ($LASTEXITCODE -ge 745)
        {
            $openCoverExitCode = $LASTEXITCODE - $openCoverExitCodeOffset
        }
        else
        {
            $nunitExitCode = $LASTEXITCODE
        }

        & $reportGeneratorConsolePath "-reports:$openCoverReport" "-targetdir:$coverageReportDir" $reportGeneratorArgument
        $reportGeneratorExitCode = $LASTEXITCODE
    }
    else
    {
        & $nunitConsolePath $path $frameworkParam $testFilterParam $nunitReportParam $nunitExtraArgument
        $nunitExitCode = $LASTEXITCODE

    }

    if ($reportGeneratorExitCode -ne 0)
    {
        Stop-WhiskeyTask -TaskContext $TaskContext -Message ('ReportGenerator didn''t run successfully. ''{0}'' returned exit code ''{1}''.' -f $reportGeneratorConsolePath,$reportGeneratorExitCode)
        return
    }
    elseif ($openCoverExitCode -ne 0)
    {
        Stop-WhiskeyTask -TaskContext $TaskContext -Message ('OpenCover didn''t run successfully. ''{0}'' returned exit code ''{1}''.' -f $openCoverConsolePath, $openCoverExitCode)
        return
    }
    elseif ($nunitExitCode -ne 0)
    {
        if (-not (Test-Path -Path $nunitReport -PathType Leaf))
        {
            Stop-WhiskeyTask -TaskContext $TaskContext -Message ('NUnit3 didn''t run successfully. ''{0}'' returned exit code ''{1}''.' -f $nunitConsolePath,$nunitExitCode)
            return
        }
        else
        {
            Stop-WhiskeyTask -TaskContext $TaskContext -Message ('NUnit3 tests failed. ''{0}'' returned exit code ''{1}''.' -f $nunitConsolePath,$nunitExitCode)
            return
        }
    }
}



function Invoke-WhiskeyParallelTask
{
    [CmdletBinding()]
    [Whiskey.Task('Parallel')]
    param(
        [Parameter(Mandatory=$true)]
        [Whiskey.Context]
        $TaskContext,

        [Parameter(Mandatory=$true)]
        [hashtable]
        $TaskParameter
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    $queues = $TaskParameter['Queues']
    if( -not $queues )
    {
        Stop-WhiskeyTask -TaskContext $TaskContext -Message 'Property "Queues" is mandatory. It should be an array of queues to run. Each queue should contain a "Tasks" property that is an array of task to run, e.g.
 
    Build:
    - Parallel:
        Queues:
        - Tasks:
            - TaskOne
            - TaskTwo
        - Tasks:
            - TaskOne
 
'

        return
    }

    try
    {

        $jobs = New-Object 'Collections.ArrayList'
        $queueIdx = -1

        foreach( $queue in $queues )
        {
            $queueIdx++
            $whiskeyModulePath = Join-Path -Path $whiskeyScriptRoot -ChildPath 'Whiskey.psd1' -Resolve

            if( -not $queue.ContainsKey('Tasks') )
            {
                Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Queue[{0}]: Property "Tasks" is mandatory. Each queue should have a "Tasks" property that is an array of Whiskey task to run, e.g.
 
    Build:
    - Parallel:
        Queues:
        - Tasks:
            - TaskOne
            - TaskTwo
        - Tasks:
            - TaskOne
 
    '
 -f $queueIdx);
                return
            }

            Write-WhiskeyVerbose -Context $TaskContext -Message ('[{0}] Starting background queue.' -f $queueIdx)

            $serializableContext = $TaskContext | ConvertFrom-WhiskeyContext

            $tasks = 
                $queue['Tasks'] |
                ForEach-Object { 
                    $taskName,$taskParameter = ConvertTo-WhiskeyTask -InputObject $_ -ErrorAction Stop
                    [pscustomobject]@{
                        Name = $taskName;
                        Parameter = $taskParameter
                    }
                }
            $job = Start-Job -Name $queueIdx -ScriptBlock {

                    Set-StrictMode -Version 'Latest'

                    $VerbosePreference = $using:VerbosePreference
                    $DebugPreference = $using:DebugPreference
                    $ProgressPreference = $using:ProgressPreference
                    $WarningPreference = $using:WarningPreference
                    $ErrorActionPreference = $using:ErrorActionPreference

                    $whiskeyModulePath = $using:whiskeyModulePath
                    $serializedContext = $using:serializableContext

                    & {
                        Import-Module -Name $whiskeyModulePath
                    } 4> $null

                    [Whiskey.Context]$context = $serializedContext | ConvertTo-WhiskeyContext

                    # Load third-party tasks.
                    foreach( $info in $context.TaskPaths )
                    {
                        Write-Verbose ('Loading tasks from "{0}".' -f $info.FullName)
                        . $info.FullName
                    }

                    foreach( $task in $using:tasks )
                    {
                        Write-Debug -Message ($task.Name)
                        $task.Parameter | ConvertTo-Json -Depth 50 | Write-Debug
                        Invoke-WhiskeyTask -TaskContext $context -Name $task.Name -Parameter $task.Parameter
                    }
                }

                $job |
                    Add-Member -MemberType NoteProperty -Name 'QueueIndex' -Value $queueIdx -PassThru |
                    Add-Member -MemberType NoteProperty -Name 'Completed' -Value $false
                [void]$jobs.Add($job)
        }

        $lastNotice = (Get-Date).AddSeconds(-61)
        while( $jobs | Where-Object { -not $_.Completed } )
        {
            foreach( $job in $jobs )
            {
                if( $job.Completed )
                {
                    continue
                }

                if( $lastNotice -lt (Get-Date).AddSeconds(-60) )
                {
                    Write-WhiskeyVerbose -Context $TaskContext -Message ('[{0}][{1}] Waiting for queue.' -f $job.QueueIndex,$job.Name)
                    $notified = $true
                }

                $completedJob = $job | Wait-Job -Timeout 1
                if( $completedJob )
                {
                    $job.Completed = $true
                    $completedJob | Receive-Job
                    $duration = $job.PSEndTime - $job.PSBeginTime
                    Write-WhiskeyVerbose -Context $TaskContext -Message ('[{0}][{1}] {2} in {3}' -f $job.QueueIndex,$job.Name,$job.State.ToString().ToUpperInvariant(),$duration)
                    if( $job.JobStateInfo.State -eq [Management.Automation.JobState]::Failed )
                    {
                        Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Queue[{0}] failed. See previous output for error information.' -f $job.Name)
                        return
                    }
                }
            }

            if( $notified )
            {
                $notified = $false
                $lastNotice = Get-Date
            }
        }
    }
    finally
    {
        $jobs | Stop-Job
        $jobs | Remove-Job
    }
}


function Invoke-WhiskeyPester3Task
{
    [Whiskey.Task('Pester3',Platform='Windows')]
    [Whiskey.RequiresTool('PowerShellModule::Pester','PesterPath',Version='3.*',VersionParameterName='Version')]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [Whiskey.Context]
        $TaskContext,

        [Parameter(Mandatory=$true)]
        [hashtable]
        $TaskParameter
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    if( -not ($TaskParameter.ContainsKey('Path')))
    {
        Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Element ''Path'' is mandatory. It should be one or more paths, which should be a list of Pester Tests to run with Pester3, e.g.
 
        Build:
        - Pester3:
            Path:
            - My.Tests.ps1
            - Tests'
)
        return
    }

    $path = $TaskParameter['Path'] | Resolve-WhiskeyTaskPath -TaskContext $TaskContext -PropertyName 'Path'

    $outputFile = Join-Path -Path $TaskContext.OutputDirectory -ChildPath ('pester+{0}.xml' -f [IO.Path]::GetRandomFileName())
    $outputFile = [IO.Path]::GetFullPath($outputFile)

    # We do this in the background so we can test this with Pester.
    $job = Start-Job -ScriptBlock {
        $VerbosePreference = $using:VerbosePreference
        $DebugPreference = $using:DebugPreference
        $ProgressPreference = $using:ProgressPreference
        $WarningPreference = $using:WarningPreference
        $ErrorActionPreference = $using:ErrorActionPreference

        $script = $using:Path
        $pesterModulePath = $using:TaskParameter['PesterPath']
        $outputFile = $using:outputFile

        Invoke-Command -ScriptBlock {
                                        $VerbosePreference = 'SilentlyContinue'
                                        Import-Module -Name $pesterModulePath
                                    }

        Invoke-Pester -Script $script -OutputFile $outputFile -OutputFormat NUnitXml -PassThru
    }

    # There's a bug where Write-Host output gets duplicated by Receive-Job if $InformationPreference is set to "Continue".
    # Since Pester uses Write-Host, this is a workaround to avoid seeing duplicate Pester output.
    $informationActionParameter = @{ }
    if( (Get-Command -Name 'Receive-Job' -ParameterName 'InformationAction') )
    {
        $informationActionParameter['InformationAction'] = 'SilentlyContinue'
    }

    do
    {
        $job | Receive-Job @informationActionParameter
    }
    while( -not ($job | Wait-Job -Timeout 1) )

    $job | Receive-Job @informationActionParameter

    Publish-WhiskeyPesterTestResult -Path $outputFile

    $result = [xml](Get-Content -Path $outputFile -Raw)

    if( -not $result )
    {
        throw ('Unable to parse Pester output XML report ''{0}''.' -f $outputFile)
    }

    if( $result.'test-results'.errors -ne '0' -or $result.'test-results'.failures -ne '0' )
    {
        throw ('Pester tests failed.')
    }
}




function Invoke-WhiskeyPester4Task
{
    [Whiskey.Task("Pester4")]
    [Whiskey.RequiresTool('PowerShellModule::Pester','PesterPath',Version='4.*',VersionParameterName='Version')]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [Whiskey.Context]
        $TaskContext,

        [Parameter(Mandatory=$true)]
        [hashtable]
        $TaskParameter
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    if( -not ($TaskParameter.ContainsKey('Path')))
    {
        Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Property "Path" is mandatory. It should be one or more paths, which should be a list of Pester test scripts (e.g. Invoke-WhiskeyPester4Task.Tests.ps1) or directories that contain Pester test scripts, e.g.
 
        Build:
        - Pester4:
            Path:
            - My.Tests.ps1
            - Tests'
)
        return
    }

    $path = $TaskParameter['Path'] | Resolve-WhiskeyTaskPath -TaskContext $TaskContext -PropertyName 'Path'

    if( $TaskParameter['Exclude'] )
    {
        $path = $path |
                    Where-Object {
                        foreach( $exclusion in $TaskParameter['Exclude'] )
                        {
                            if( $_ -like $exclusion )
                            {
                                Write-WhiskeyVerbose -Context $TaskContext -Message ('EXCLUDE {0} -like {1}' -f $_,$exclusion)
                                return $false
                            }
                            else
                            {
                                Write-WhiskeyVerbose -Context $TaskContext -Message (' {0} -notlike {1}' -f $_,$exclusion)
                            }
                        }
                        return $true
                    }
        if( -not $path )
        {
            Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Found no tests to run. Property "Exclude" matched all paths in the "Path" property. Please update your exclusion rules to include at least one test. View verbose output to see what exclusion filters excluded what test files.')
            return
        }
    }

    [int]$describeDurationCount = 0
    $describeDurationCount = $TaskParameter['DescribeDurationReportCount']
    [int]$itDurationCount = 0
    $itDurationCount = $TaskParameter['ItDurationReportCount']

    $outputFile = Join-Path -Path $TaskContext.OutputDirectory -ChildPath ('pester+{0}.xml' -f [IO.Path]::GetRandomFileName())

    Write-WhiskeyVerbose -Context $TaskContext -Message $TaskParameter['PesterPath']
    Write-WhiskeyVerbose -Context $TaskContext -Message (' Script {0}' -f ($Path | Select-Object -First 1))
    $Path | Select-Object -Skip 1 | ForEach-Object { Write-WhiskeyVerbose -Context $TaskContext -Message (' {0}' -f $_) }
    Write-Verbose -Message (' OutputFile {0}' -f $outputFile)
    # We do this in the background so we can test this with Pester.
    $job = Start-Job -ScriptBlock {
        $VerbosePreference = $using:VerbosePreference
        $DebugPreference = $using:DebugPreference
        $ProgressPreference = $using:ProgressPreference
        $WarningPreference = $using:WarningPreference
        $ErrorActionPreference = $using:ErrorActionPreference

        $script = $using:Path
        $pesterModulePath = $using:TaskParameter['PesterPath']
        $outputFile = $using:outputFile
        [int]$describeCount = $using:describeDurationCount
        [int]$itCount = $using:itDurationCount

        Invoke-Command -ScriptBlock {
                                        $VerbosePreference = 'SilentlyContinue'
                                        Import-Module -Name $pesterModulePath
                                    }

        $result = Invoke-Pester -Script $script -OutputFile $outputFile -OutputFormat NUnitXml -PassThru

        $result.TestResult |
            Group-Object 'Describe' |
            ForEach-Object {
                $totalTime = [TimeSpan]::Zero
                $_.Group | ForEach-Object { $totalTime += $_.Time }
                [pscustomobject]@{
                                    Describe = $_.Name;
                                    Duration = $totalTime
                                }
            } | Sort-Object -Property 'Duration' -Descending |
            Select-Object -First $describeCount |
            Format-Table -AutoSize

        $result.TestResult |
            Sort-Object -Property 'Time' -Descending |
            Select-Object -First $itCount |
            Format-Table -AutoSize -Property 'Describe','Name','Time'
    }

    # There's a bug where Write-Host output gets duplicated by Receive-Job if $InformationPreference is set to "Continue".
    # Since Pester uses Write-Host, this is a workaround to avoid seeing duplicate Pester output.
    $informationActionParameter = @{ }
    if( (Get-Command -Name 'Receive-Job' -ParameterName 'InformationAction') )
    {
        $informationActionParameter['InformationAction'] = 'SilentlyContinue'
    }

    do
    {
        $job | Receive-Job @informationActionParameter
    }
    while( -not ($job | Wait-Job -Timeout 1) )

    $job | Receive-Job @informationActionParameter

    Publish-WhiskeyPesterTestResult -Path $outputFile

    $result = [xml](Get-Content -Path $outputFile -Raw)

    if( -not $result )
    {
        throw ('Unable to parse Pester output XML report ''{0}''.' -f $outputFile)
    }

    if( $result.'test-results'.errors -ne '0' -or $result.'test-results'.failures -ne '0' )
    {
        throw ('Pester tests failed.')
    }
}


 function Invoke-WhiskeyPipelineTask
{
    [CmdletBinding()]
    [Whiskey.Task("Pipeline", SupportsClean=$true, SupportsInitialize=$true)]
    param(
        [Parameter(Mandatory=$true)]
        [Whiskey.Context]
        $TaskContext,

        [Parameter(Mandatory=$true)]
        [hashtable]
        $TaskParameter
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    if( -not $TaskParameter['Name'] )
    {
        Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Name is a mandatory property, but is missing or doesn''t have a value. It should be set to a list of pipeline names you want to run as part of another pipeline, e.g.
 
    Build:
    - Pipeline:
        Name:
        - One
        - Two
 
    One:
    - TASK
 
    Two:
    - TASK
 
'
)
    return
    }

    $currentPipeline = $TaskContext.PipelineName
    try
    {
        foreach( $name in $TaskParameter['Name'] )
        {
            Invoke-WhiskeyPipeline -Context $TaskContext -Name $name
        }
    }
    finally
    {
        $TaskContext.PipelineName = $currentPipeline
    }
}


function Invoke-WhiskeyPowerShell
{
    [Whiskey.Task("PowerShell",SupportsClean=$true,SupportsInitialize=$true)]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [Whiskey.Context]
        $TaskContext,

        [Parameter(Mandatory=$true)]
        [hashtable]
        $TaskParameter
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    if( -not ($TaskParameter.ContainsKey('Path')) )
    {
        Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Property ''Path'' is mandatory. It should be one or more paths, which should be a list of PowerShell Scripts to run, e.g.
 
        Build:
        - PowerShell:
            Path:
            - myscript.ps1
            - myotherscript.ps1
            WorkingDirectory: bin'
)
        return
    }

    $path = $TaskParameter['Path'] | Resolve-WhiskeyTaskPath -TaskContext $TaskContext -PropertyName 'Path'

    $workingDirectory = (Get-Location).ProviderPath

    $argument = $TaskParameter['Argument']
    if( -not $argument )
    {
        $argument = @{ }
    }

    foreach( $scriptPath in $path )
    {

        if( -not (Test-Path -Path $WorkingDirectory -PathType Container) )
        {
            Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Can''t run PowerShell script ''{0}'': working directory ''{1}'' doesn''t exist.' -f $scriptPath,$WorkingDirectory)
            continue
        }

        $scriptCommand = Get-Command -Name $scriptPath -ErrorAction Ignore
        if( -not $scriptCommand )
        {
            Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Can''t run PowerShell script ''{0}'': it has a syntax error.' -f $scriptPath)
            continue
        }

        $passTaskContext = $scriptCommand.Parameters.ContainsKey('TaskContext')

        if( (Get-Member -InputObject $argument -Name 'Keys') )
        {
            $scriptCommand.Parameters.Values |
                Where-Object { $_.ParameterType -eq [switch] } |
                Where-Object { $argument.ContainsKey($_.Name) } |
                ForEach-Object { $argument[$_.Name] = $argument[$_.Name] | ConvertFrom-WhiskeyYamlScalar }
        }

        $resultPath = Join-Path -Path $TaskContext.OutputDirectory -ChildPath ('PowerShell-{0}-RunResult-{1}' -f ($scriptPath | Split-Path -Leaf),([IO.Path]::GetRandomFileName()))
        $serializableContext = $TaskContext | ConvertFrom-WhiskeyContext
        $job = Start-Job -ScriptBlock {
            $VerbosePreference = $using:VerbosePreference
            $DebugPreference = $using:DebugPreference
            $ProgressPreference = $using:ProgressPreference
            $WarningPreference = $using:WarningPreference
            $ErrorActionPreference = $using:ErrorActionPreference

            $workingDirectory = $using:WorkingDirectory
            $scriptPath = $using:ScriptPath
            $argument = $using:argument
            $serializedContext = $using:serializableContext
            $whiskeyScriptRoot = $using:whiskeyScriptRoot
            $resultPath = $using:resultPath
            $passTaskContext = $using:passTaskContext

            Invoke-Command -ScriptBlock {
                                            $VerbosePreference = 'SilentlyContinue';
                                            & (Join-Path -Path $whiskeyScriptRoot -ChildPath 'Import-Whiskey.ps1' -Resolve -ErrorAction Stop)
                                        }
            [Whiskey.Context]$context = $serializedContext | ConvertTo-WhiskeyContext

            $contextArgument = @{ }
            if( $passTaskContext )
            {
                $contextArgument['TaskContext'] = $context
            }

            Set-Location $workingDirectory
            $Global:LASTEXITCODE = 0

            & $scriptPath @contextArgument @argument

            $result = @{
                'ExitCode'   = $Global:LASTEXITCODE
                'Successful' = $?
            }

            $result | ConvertTo-Json | Set-Content -Path $resultPath
        }

        do
        {
            $job | Receive-Job
        }
        while( -not ($job | Wait-Job -Timeout 1) )

        $job | Receive-Job

        if( (Test-Path -Path $resultPath -PathType Leaf) )
        {
            $runResult = Get-Content -Path $resultPath -Raw | ConvertFrom-Json
        }
        else
        {
            Stop-WhiskeyTask -TaskContext $TaskContext -Message ('PowerShell script ''{0}'' threw a terminating exception.' -F $scriptPath)
            return
        }

        if( $runResult.ExitCode -ne 0 )
        {
            Stop-WhiskeyTask -TaskContext $TaskContext -Message ('PowerShell script ''{0}'' failed, exited with code {1}.' -F $scriptPath,$runResult.ExitCode)
            return
        }
        elseif( $runResult.Successful -eq $false )
        {
            Stop-WhiskeyTask -TaskContext $TaskContext -Message ('PowerShell script ''{0}'' threw a terminating exception.' -F $scriptPath)
            return
        }

    }
}



function New-WhiskeyProGetUniversalPackage
{
    [CmdletBinding()]
    [Whiskey.Task("ProGetUniversalPackage")]
    [Whiskey.RequiresTool('PowerShellModule::ProGetAutomation','ProGetAutomationPath',Version='0.9.*',VersionParameterName='ProGetAutomationVersion')]
    param(
        [Parameter(Mandatory=$true)]
        [Whiskey.Context]
        $TaskContext,

        [Parameter(Mandatory=$true)]
        [hashtable]
        $TaskParameter
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    Import-WhiskeyPowerShellModule -Name 'ProGetAutomation'

    $manifestProperties = @{}
    if( $TaskParameter.ContainsKey('ManifestProperties') )
    {
        $manifestProperties = $TaskParameter['ManifestProperties']
        foreach( $taskProperty in @( 'Name', 'Description', 'Version' ))
        {
            if( $manifestProperties.Keys -contains $taskProperty )
            {
                Stop-WhiskeyTask -TaskContext $TaskContext -Message ('"ManifestProperties" contains key "{0}". This property cannot be manually defined in "ManifestProperties" as it is set automatically from the corresponding task property "{0}".' -f $taskProperty)
                return
            }
        }
    }

    foreach( $mandatoryProperty in @( 'Name', 'Description' ) )
    {
        if( -not $TaskParameter.ContainsKey($mandatoryProperty) )
        {
            Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Property "{0}" is mandatory.' -f $mandatoryProperty)
            return
        }
    }

    $name = $TaskParameter['Name']
    $validNameRegex = '^[0-9A-z\-\._]{1,50}$'
    if ($name -notmatch $validNameRegex)
    {
        Stop-WhiskeyTask -TaskContext $TaskContext -Message '"Name" property is invalid. It should be a string of one to fifty characters: numbers (0-9), upper and lower-case letters (A-z), dashes (-), periods (.), and underscores (_).'
        return
    }

    $version = $TaskParameter['Version']

    # ProGet uses build metadata to distinguish different versions, so we can't use a full semantic version.
    if( $version )
    {
        if( ($version -notmatch '^\d+\.\d+\.\d+$') )
        {
            Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Property "Version" is invalid. It must be a three part version number, i.e. MAJOR.MINOR.PATCH.')
            return
        }
        [SemVersion.SemanticVersion]$semVer = $null
        if( -not ([SemVersion.SemanticVersion]::TryParse($version, [ref]$semVer)) )
        {
            Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Property "Version" is not a valid semantic version.')
            return
        }
        $semVer = New-Object 'SemVersion.SemanticVersion' $semVer.Major,$semVer.Minor,$semVer.Patch,$TaskContext.Version.SemVer2.Prerelease,$TaskContext.Version.SemVer2.Build
        $version = New-WhiskeyVersionObject -SemVer $semVer
    }
    else
    {
        $version = $TaskContext.Version
    }

    $compressionLevel = [IO.Compression.CompressionLevel]::Optimal
    if( $TaskParameter['CompressionLevel'] )
    {
        $expectedValues = [enum]::GetValues([IO.Compression.CompressionLevel])
        $compressionLevel = $TaskParameter['CompressionLevel']
        if( $compressionLevel -notin $expectedValues )
        {
            [int]$intCompressionLevel = 0
            if( -not [int]::TryParse($TaskParameter['CompressionLevel'],[ref]$intCompressionLevel) )
            {
                Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Property "CompressionLevel": "{0}" is not a valid compression level. It must be one of: {1}' -f $TaskParameter['CompressionLevel'],($expectedValues -join ', '));
                return
            }
            $compressionLevel = $intCompressionLevel
            if( $compressionLevel -ge 5 )
            {
                $compressionLevel = [IO.Compression.CompressionLevel]::Optimal
            }
            else
            {
                $compressionLevel = [IO.Compression.CompressionLevel]::Fastest
            }
            Write-Warning -Message ('The ProGetUniversalPackage task no longer supports integer-style compression levels. Please update your task in your whiskey.yml file to use one of the new values: {0}. We''re converting the number you provided, "{1}", to "{2}".' -f ($expectedValues -join ', '),$TaskParameter['CompressionLevel'],$compressionLevel)
        }
    }

    $parentPathParam = @{ }
    $sourceRoot = $TaskContext.BuildRoot
    if( $TaskParameter.ContainsKey('SourceRoot') )
    {
        $sourceRoot = $TaskParameter['SourceRoot'] | Resolve-WhiskeyTaskPath -TaskContext $TaskContext -PropertyName 'SourceRoot'
        $parentPathParam['ParentPath'] = $sourceRoot
    }

    $tempRoot = Join-Path -Path $TaskContext.Temp -ChildPath 'upack'
    New-Item -Path $tempRoot -ItemType 'Directory' | Out-Null

    $tempPackageRoot = Join-Path -Path $tempRoot -ChildPath 'package'
    New-Item -Path $tempPackageRoot -ItemType 'Directory' | Out-Null

    $upackJsonPath = Join-Path -Path $tempRoot -ChildPath 'upack.json'
    $manifestProperties | ConvertTo-Json | Set-Content -Path $upackJsonPath

    # Add the version.json file
    $versionJsonPath = Join-Path -Path $tempPackageRoot -ChildPath 'version.json'
    @{
        Version = $version.Version.ToString();
        SemVer2 = $version.SemVer2.ToString();
        SemVer2NoBuildMetadata = $version.SemVer2NoBuildMetadata.ToString();
        PrereleaseMetadata = $version.SemVer2.Prerelease;
        BuildMetadata = $version.SemVer2.Build;
        SemVer1 = $version.SemVer1.ToString();
    } | ConvertTo-Json -Depth 1 | Set-Content -Path $versionJsonPath

    function Copy-ToPackage
    {
        param(
            [Parameter(Mandatory=$true)]
            [object[]]
            $Path,

            [Switch]
            $AsThirdPartyItem
        )

        foreach( $item in $Path )
        {
            $override = $False
            if( (Get-Member -InputObject $item -Name 'Keys') )
            {
                $sourcePath = $null
                $override = $True
                foreach( $key in $item.Keys )
                {
                    $destinationItemName = $item[$key]
                    $sourcePath = $key
                }
            }
            else
            {
                $sourcePath = $item
            }
            $pathparam = 'path'
            if( $AsThirdPartyItem )
            {
                $pathparam = 'ThirdPartyPath'
            }

            $sourcePaths = $sourcePath | Resolve-WhiskeyTaskPath -TaskContext $TaskContext -PropertyName $pathparam @parentPathParam
            if( -not $sourcePaths )
            {
                return
            }

            foreach( $sourcePath in $sourcePaths )
            {
                $relativePath = $sourcePath -replace ('^{0}' -f ([regex]::Escape($sourceRoot))),''
                $relativePath = $relativePath.Trim([IO.Path]::DirectorySeparatorChar)

                $addParams = @{ BasePath = $sourceRoot }
                $overrideInfo = ''
                if( $override )
                {
                    $addParams = @{ PackageItemName = $destinationItemName }
                    $overrideInfo = ' -> {0}' -f $destinationItemName
                }
                $addParams['CompressionLevel'] = $compressionLevel

                if( $AsThirdPartyItem )
                {
                    Write-WhiskeyInfo -Context $TaskContext -Message (' packaging unfiltered item {0}{1}' -f $relativePath,$overrideInfo)
                    Get-Item -Path $sourcePath |
                        Add-ProGetUniversalPackageFile -PackagePath $outFile @addParams -ErrorAction Stop
                    continue
                }

                if( (Test-Path -Path $sourcePath -PathType Leaf) )
                {
                    Write-WhiskeyInfo -Context $TaskContext -Message (' packaging file {0}{1}' -f $relativePath,$overrideInfo)
                    Add-ProGetUniversalPackageFile -PackagePath $outFile -InputObject $sourcePath @addParams -ErrorAction Stop
                    continue
                }

                if( -not $TaskParameter['Include'] )
                {
                    Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Property "Include" is mandatory because "{0}" is in your "Path" property and it is a directory. The "Include" property is a whitelist of files (wildcards supported) to include in your package. Only files in directories that match an item in the "Include" list will be added to your package.' -f $sourcePath)
                    return
                }

                function Find-Item
                {
                    param(
                        [Parameter(Mandatory)]
                        $Path
                    )

                    if( (Test-Path -Path $Path -PathType Leaf) )
                    {
                        return Get-Item -Path $Path
                    }

                    $Path = Join-Path -Path $Path -ChildPath '*'
                    & {
                            Get-ChildItem -Path $Path -Include $TaskParameter['Include'] -Exclude $TaskParameter['Exclude'] -File
                            Get-Item -Path $Path -Exclude $TaskParameter['Exclude'] |
                                Where-Object { $_.PSIsContainer }
                        }  |
                        ForEach-Object {
                            if( $_.PSIsContainer )
                            {
                                Find-Item -Path $_.FullName
                            }
                            else
                            {
                                $_
                            }
                        }
                }

                if( $override )
                {
                    $addParams['BasePath'] = $sourcePath
                    $addParams.Remove('PackageItemName')
                    $overrideInfo = ' -> {0}' -f $destinationItemName

                    if ($destinationItemName -ne '.')
                    {
                        $addParams['PackageParentPath'] = $destinationItemName
                    }
                }

                Write-WhiskeyInfo -Context $TaskContext -Message (' packaging filtered directory {0}{1}' -f $relativePath,$overrideInfo)
                Find-Item -Path $sourcePath |
                    Add-ProGetUniversalPackageFile -PackagePath $outFile @addParams -ErrorAction Stop
            }
        }
    }

    $badChars = [IO.Path]::GetInvalidFileNameChars() | ForEach-Object { [regex]::Escape($_) }
    $fixRegex = '[{0}]' -f ($badChars -join '')
    $fileName = '{0}.{1}.upack' -f $name,($version.SemVer2NoBuildMetadata -replace $fixRegex,'-')

    $outFile = Join-Path -Path $TaskContext.OutputDirectory -ChildPath $fileName

    if( (Test-Path -Path $outFile -PathType Leaf) )
    {
        Remove-Item -Path $outFile -Force
    }

    if( -not $manifestProperties.ContainsKey('title') )
    {
        $manifestProperties['title'] = $TaskParameter['Name']
    }

    $outFileDisplay = $outFile -replace ('^{0}' -f [regex]::Escape($TaskContext.BuildRoot)),''
    $outFileDisplay = $outFileDisplay.Trim([IO.Path]::DirectorySeparatorChar)
    Write-WhiskeyInfo -Context $TaskContext -Message ('Creating universal package "{0}".' -f $outFileDisplay)
    New-ProGetUniversalPackage -OutFile $outFile `
                               -Version $version.SemVer2NoBuildMetadata.ToString() `
                               -Name $TaskParameter['Name'] `
                               -Description $TaskParameter['Description'] `
                               -AdditionalMetadata $manifestProperties

    Add-ProGetUniversalPackageFile -PackagePath $outFile -InputObject $versionJsonPath -ErrorAction Stop

    if( $TaskParameter['Path'] )
    {
        Copy-ToPackage -Path $TaskParameter['Path']
    }

    if( $TaskParameter.ContainsKey('ThirdPartyPath') -and $TaskParameter['ThirdPartyPath'] )
    {
        Copy-ToPackage -Path $TaskParameter['ThirdPartyPath'] -AsThirdPartyItem
    }
}



function Publish-WhiskeyBBServerTag
{
    [CmdletBinding()]
    [Whiskey.Task('PublishBitbucketServerTag')]
    [Whiskey.RequiresTool('PowerShellModule::BitbucketServerAutomation','BitbucketServerAutomationPath',Version='0.9.*',VersionParameterName='BitbucketServerAutomationVersion')]
    param(
        [Parameter(Mandatory=$true)]
        [Whiskey.Context]
        $TaskContext,

        [Parameter(Mandatory=$true)]
        [hashtable]
        $TaskParameter
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    Import-WhiskeyPowerShellModule -Name 'BitbucketServerAutomation'

    $exampleTask = 'Publish:
        - PublishBitbucketServerTag:
            CredentialID: BitbucketServerCredential
            Uri: https://bitbucketserver.example.com'


    if( -not $TaskParameter['CredentialID'] )
    {
        Stop-WhiskeyTask -TaskContext $TaskContext -Message "Property 'CredentialID' is mandatory. It should be the ID of the credential to use when connecting to Bitbucket Server:
 
        $exampleTask
 
        Use the `Add-WhiskeyCredential` function to add credentials to the build.
        "

        return
    }

    if( -not $TaskParameter['Uri'] )
    {
        Stop-WhiskeyTask -TaskContext $TaskContext -Message "Property 'Uri' is mandatory. It should be the URL to the instance of Bitbucket Server where the tag should be created:
 
        $exampleTask
        "

        return
    }

    $commitHash = $TaskContext.BuildMetadata.ScmCommitID
    if( -not $commitHash )
    {
        Stop-WhiskeyTask -TaskContext $TaskContext -PropertyDescription '' -Message ('Unable to identify a valid commit to tag. Are you sure you''re running under a build server?')
        return
    }

    if( $TaskParameter['ProjectKey'] -and $TaskParameter['RepositoryKey'] )
    {
        $projectKey = $TaskParameter['ProjectKey']
        $repoKey = $TaskParameter['RepositoryKey']
    }
    elseif( $TaskContext.BuildMetadata.ScmUri -and $TaskContext.BuildMetadata.ScmUri.Segments )
    {
        $uri = [uri]$TaskContext.BuildMetadata.ScmUri
        $projectKey = $uri.Segments[-2].Trim('/')
        $repoKey = $uri.Segments[-1] -replace '\.git$',''
    }
    else
    {
        Stop-WhiskeyTask -TaskContext $TaskContext -PropertyDescription '' -Message ("Unable to determine the repository where we should create the tag. Either create a `GIT_URL` environment variable that is the URI used to clone your repository, or add your repository''s project and repository keys as `ProjectKey` and `RepositoryKey` properties, respectively, on this task:
 
        Publish:
        - PublishBitbucketServerTag:
            CredentialID: $($TaskParameter['CredentialID'])
            Uri: $($TaskParameter['Uri'])
            ProjectKey: PROJECT_KEY
            RepositoryKey: REPOSITORY_KEY
       "
)
        return
    }

    $credentialID = $TaskParameter['CredentialID']
    $credential = Get-WhiskeyCredential -Context $TaskContext -ID $credentialID -PropertyName 'CredentialID'
    $connection = New-BBServerConnection -Credential $credential -Uri $TaskParameter['Uri']
    $tag = $TaskContext.Version.SemVer2NoBuildMetadata
    Write-WhiskeyVerbose -Context $TaskContext -Message ('[{0}] [{1}] [{2}] {3} -> {4}' -f $TaskParameter['Uri'],$projectKey,$repoKey,$commitHash,$tag)
    New-BBServerTag -Connection $connection -ProjectKey $projectKey -force -RepositoryKey $repoKey -Name $tag -CommitID $commitHash -ErrorAction Stop
}



function Publish-WhiskeyBuildMasterPackage
{
    [CmdletBinding()]
    [Whiskey.Task("PublishBuildMasterPackage")]
    [Whiskey.RequiresTool('PowerShellModule::BuildMasterAutomation','BuildMasterAutomationPath',Version='0.6.*',VersionParameterName='BuildMasterAutomationVersion')]
    param(
        [Parameter(Mandatory=$true)]
        [Whiskey.Context]
        $TaskContext,

        [Parameter(Mandatory=$true)]
        [hashtable]
        $TaskParameter
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    Import-WhiskeyPowerShellModule -Name 'BuildMasterAutomation'

    $applicationName = $TaskParameter['ApplicationName']
    if( -not $applicationName )
    {
        Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Property ''ApplicationName'' is mandatory. It must be set to the name of the application in BuildMaster where the package should be published.')
        return
    }

    $releaseName = $TaskParameter['ReleaseName']
    if( -not $releaseName )
    {
        Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Property ''ReleaseName'' is mandatory. It must be set to the release name in the BuildMaster application where the package should be published.')
        return
    }

    $buildmasterUri = $TaskParameter['Uri']
    if( -not $buildmasterUri )
    {
        Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Property ''Uri'' is mandatory. It must be set to the BuildMaster URI where the package should be published.')
        return
    }

    $apiKeyID = $TaskParameter['ApiKeyID']
    if( -not $apiKeyID )
    {
        Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Property ''ApiKeyID'' is mandatory. It should be the ID of the API key to use when publishing the package to BuildMaster. Use the `Add-WhiskeyApiKey` to add your API key.')
        return
    }

    $apiKey = Get-WhiskeyApiKey -Context $TaskContext -ID $TaskParameter['ApiKeyID'] -PropertyName 'ApiKeyID'
    $buildMasterSession = New-BMSession -Uri $TaskParameter['Uri'] -ApiKey $apiKey

    $version = $TaskContext.Version.SemVer2

    $variables = $TaskParameter['PackageVariable']

    $release = Get-BMRelease -Session $buildMasterSession -Application $applicationName -Name $releaseName -ErrorAction Stop
    if( -not $release )
    {
        Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Unable to create and deploy a release package in BuildMaster. Either the ''{0}'' application doesn''t exist or it doesn''t have a ''{1}'' release.' -f $applicationName,$releaseName)
        return
    }

    $release | Format-List | Out-String | Write-WhiskeyVerbose -Context $TaskContext

    if( $TaskParameter['PackageName'] )
    {
        $packageName = $TaskParameter['PackageName']
    }
    else
    {
        $packageName = '{0}.{1}.{2}' -f $version.Major,$version.Minor,$version.Patch
    }

    $package = New-BMPackage -Session $buildMasterSession -Release $release -PackageNumber $packageName -Variable $variables -ErrorAction Stop
    $package | Format-List | Out-String | Write-WhiskeyVerbose -Context $TaskContext

    if( ConvertFrom-WhiskeyYamlScalar -InputObject $TaskParameter['SkipDeploy'] )
    {
        Write-WhiskeyVerbose -Context $TaskContext -Message ('Skipping deploy. SkipDeploy property is true')
    }
    else
    {
        $optionalParams = @{ 'Stage' = $TaskParameter['StartAtStage'] }

        $deployment = Publish-BMReleasePackage -Session $buildMasterSession -Package $package @optionalParams -ErrorAction Stop
        $deployment | Format-List | Out-String | Write-WhiskeyVerbose -Context $TaskContext
    }
}



function Publish-WhiskeyNodeModule
{
    [Whiskey.Task("PublishNodeModule")]
    [Whiskey.RequiresTool("Node", "NodePath", VersionParameterName='NodeVersion')]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [Whiskey.Context]
        # The context the task is running under.
        $TaskContext,

        [Parameter(Mandatory=$true)]
        [hashtable]
        # The parameters/configuration to use to run the task. Should be a hashtable that contains the following item:
        #
        # * `WorkingDirectory` (Optional): Provides the default root directory for the NPM `publish` task. Defaults to the directory where the build's `whiskey.yml` file was found. Must be relative to the `whiskey.yml` file.
        $TaskParameter
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    $workingDirectory = (Get-Location).ProviderPath

    $npmRegistryUri = [uri]$TaskParameter['NpmRegistryUri']
    if (-not $npmRegistryUri) 
    {
        Stop-WhiskeyTask -TaskContext $TaskContext -Message 'Property ''NpmRegistryUri'' is mandatory and must be a URI. It should be the URI to the registry where the module should be published. E.g.,
         
    Build:
    - PublishNodeModule:
        NpmRegistryUri: https://registry.npmjs.org/
    '

        return
    }

    if (!$TaskContext.Publish)
    {
        return
    }
    
    $npmConfigPrefix = '//{0}{1}:' -f $npmregistryUri.Authority,$npmRegistryUri.LocalPath

    $credentialID = $TaskParameter['CredentialID']
    if( -not $credentialID )
    {
        Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Property ''CredentialID'' is mandatory. It should be the ID of the credential to use when publishing to ''{0}'', e.g.
     
    Build:
    - PublishNodeModule:
        NpmRegistryUri: {0}
        CredentialID: NpmCredential
     
    Use the `Add-WhiskeyCredential` function to add the credential to the build.
    '
 -f $npmRegistryUri)
        return
    }
    $credential = Get-WhiskeyCredential -Context $TaskContext -ID $credentialID -PropertyName 'CredentialID'
    $npmUserName = $credential.UserName
    $npmEmail = $TaskParameter['EmailAddress']
    if( -not $npmEmail )
    {
        Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Property ''EmailAddress'' is mandatory. It should be the e-mail address of the user publishing the module, e.g.
     
    Build:
    - PublishNodeModule:
        NpmRegistryUri: {0}
        CredentialID: {1}
        EmailAddress: somebody@example.com
    '
 -f $npmRegistryUri,$credentialID)
        return
    }
    $npmCredPassword = $credential.GetNetworkCredential().Password
    $npmBytesPassword  = [System.Text.Encoding]::UTF8.GetBytes($npmCredPassword)
    $npmPassword = [System.Convert]::ToBase64String($npmBytesPassword)

    try
    {
        $packageNpmrc = New-Item -Path '.npmrc' -ItemType File -Force
        Add-Content -Path $packageNpmrc -Value ('{0}_password="{1}"' -f $npmConfigPrefix, $npmPassword)
        Add-Content -Path $packageNpmrc -Value ('{0}username={1}' -f $npmConfigPrefix, $npmUserName)
        Add-Content -Path $packageNpmrc -Value ('{0}email={1}' -f $npmConfigPrefix, $npmEmail)
        Add-Content -Path $packageNpmrc -Value ('registry={0}' -f $npmRegistryUri)
        Write-WhiskeyVerbose -Context $TaskContext -Message ('Creating .npmrc at {0}.' -f $packageNpmrc)
        Get-Content -Path $packageNpmrc |
            ForEach-Object {
                if( $_ -match '_password' )
                {
                    return $_ -replace '=(.*)$','=********'
                }
                return $_
            } |
            Write-WhiskeyVerbose -Context $TaskContext

        Invoke-WhiskeyNpmCommand -Name 'prune' -ArgumentList '--production' -BuildRootPath $TaskContext.BuildRoot -ErrorAction Stop
        Invoke-WhiskeyNpmCommand -Name 'publish' -BuildRootPath $TaskContext.BuildRoot -ErrorAction Stop
    }
    finally
    {
        if (Test-Path $packageNpmrc)
        {
            Write-WhiskeyVerbose -Context $TaskContext -Message ('Removing .npmrc at {0}.' -f $packageNpmrc)
            Remove-Item -Path $packageNpmrc
        }
    }
}


function Publish-WhiskeyPowerShellModule
{
    [Whiskey.Task("PublishPowerShellModule")]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [Whiskey.Context]
        $TaskContext,

        [Parameter(Mandatory)]
        [hashtable]
        $TaskParameter,

        [Whiskey.Tasks.ValidatePath(Mandatory,PathType='Directory')]
        [string]
        $Path
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    if( -not $TaskParameter.ContainsKey('RepositoryName') )
    {
        Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Property "RepositoryName" is mandatory. It should be the name of the PowerShell repository you want to publish to, e.g.
 
        Build:
        - PublishPowerShellModule:
            Path: mymodule
            RepositoryName: PSGallery
        '
)
        return
    }
    $repositoryName = $TaskParameter['RepositoryName']

    $apiKeyID = $TaskParameter['ApiKeyID']
    if( -not $apiKeyID )
    {
        Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Property "ApiKeyID" is mandatory. It must be the ID of the API key to use when publishing to the "{0}" repository. Use the `Add-WhiskeyApiKey` function to add API keys to the build.' -f $repositoryName)
        return
    }

    $apiKey = Get-WhiskeyApiKey -Context $TaskContext -ID $apiKeyID -PropertyName 'ApiKeyID'

    $manifestPath = '{0}\{1}.psd1' -f $Path,($Path | Split-Path -Leaf)
    if( $TaskParameter.ContainsKey('ModuleManifestPath') )
    {
        $manifestPath = $TaskParameter['ModuleManifestPath']
    }
    if( -not (Test-Path -Path $manifestPath -PathType Leaf) )
    {
        Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Module manifest path "{0}" either does not exist or is a directory.' -f $manifestPath)
        return
    }

    $manifest = Get-Content $manifestPath
    $versionString = 'ModuleVersion = ''{0}.{1}.{2}''' -f ( $TaskContext.Version.SemVer2.Major, $TaskContext.Version.SemVer2.Minor, $TaskContext.Version.SemVer2.Patch )
    $manifest = $manifest -replace 'ModuleVersion\s*=\s*(''|")[^''"]*(''|")', $versionString
    $prereleaseString = 'Prerelease = ''{0}''' -f $TaskContext.Version.SemVer2.Prerelease  
    $manifest = $manifest -replace 'Prerelease\s*=\s*(''|")[^''"]*(''|")', $prereleaseString
    $manifest | Set-Content $manifestPath

    Import-WhiskeyPowerShellModule -Name 'PackageManagement','PowerShellGet'

    $commonParams = @{}
    if( $VerbosePreference -in @('Continue','Inquire') )
    {
        $commonParams['Verbose'] = $true
    }
    if( $DebugPreference -in @('Continue','Inquire') )
    {
        $commonParams['Debug'] = $true
    }
    if( (Test-Path -Path 'variable:InformationPreference') )
    {
        $commonParams['InformationAction'] = $InformationPreference
    }

    Get-PackageProvider -Name 'NuGet' -ForceBootstrap @commonParams | Out-Null

    if( -not (Get-PSRepository -Name $repositoryName -ErrorAction Ignore @commonParams) )
    {
        $publishLocation = $TaskParameter['RepositoryUri']
        if( -not $publishLocation )
        {
            Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Property "RepositoryUri" is mandatory since there is no registered repository named "{0}". The "RepositoryUri" must be the URI to the PowerShall repository to publish to. The repository will be registered for you.' -f $repositoryName)
            return
        }

        $credentialParam = @{ }
        if( $TaskParameter.ContainsKey('CredentialID') )
        {
            $credentialParam['Credential'] = Get-WhiskeyCredential -Context $TaskContext -ID $TaskParameter['CredentialID'] -PropertyName 'CredentialID'
        }

        Register-PSRepository -Name $repositoryName -SourceLocation $publishLocation -PublishLocation $publishLocation -InstallationPolicy Trusted -PackageManagementProvider NuGet @credentialParam -ErrorAction Stop @commonParams
    }

    # Use the Force switch to allow publishing versions that come *before* the latest version.
    Publish-Module -Path $Path `
                   -Repository $repositoryName `
                   -NuGetApiKey $apiKey `
                   -Force `
                   -ErrorAction Stop `
                   @commonParams
}




function Publish-WhiskeyProGetAsset
{
    [Whiskey.Task("PublishProGetAsset")]
    [Whiskey.RequiresTool('PowerShellModule::ProGetAutomation','ProGetAutomationPath',Version='0.9.*',VersionParameterName='ProGetAutomationVersion')]
    [CmdletBinding()]
    param(
        [Whiskey.Context]
        # The context this task is operating in. Use `New-WhiskeyContext` to create context objects.
        $TaskContext,

        [hashtable]
        # The parameters/configuration to use to run the task.
        $TaskParameter
    )


    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    Import-WhiskeyPowerShellModule -Name 'ProGetAutomation'

    $message = "
    Build:
    - PublishProGetAsset:
        CredentialID: ProGetCredential
        Path:
        - ""path/to/file.txt""
        - ""path/to/anotherfile.txt""
        Uri: http://proget.dev.webmd.com/
        AssetPath:
        - ""path/to/exampleAsset""
        - ""path/toanother/file.txt""
        AssetDirectory: 'versions'
        "

    if( -not $TaskParameter['Path'] )
    {
        Stop-WhiskeyTask -TaskContext $TaskContext -Message ("Please add a valid Path Parameter to your whiskey.yml file:" + $message)
        return
    }

    if( -not $TaskParameter['AssetDirectory'] )
    {
        Stop-WhiskeyTask -TaskContext $TaskContext -Message ("Please add a valid Directory Parameter to your whiskey.yml file:" + $message)
        return
    }

    if( -Not $TaskParameter['CredentialID'])
    {
        Stop-WhiskeyTask -TaskContext $TaskContext -Message ("CredentialID is a mandatory property. It should be the ID of the credential to use when connecting to ProGet. Add the credential with the `Add-WhiskeyCredential` function:" + $message)
        return
    }

    $credential = Get-WhiskeyCredential -Context $TaskContext -ID $TaskParameter['CredentialID'] -PropertyName 'CredentialID'

    $session = New-ProGetSession -Uri $TaskParameter['Uri'] -Credential $credential

    foreach($path in $TaskParameter['Path']){
        if( $TaskParameter['AssetPath'] -and @($TaskParameter['AssetPath']).count -eq @($TaskParameter['Path']).count){
            $name = @($TaskParameter['AssetPath'])[$TaskParameter['Path'].indexOf($path)]
        }
        else
        {
            Stop-WhiskeyTask -TaskContext $TaskContext -Message ("There must be the same number of `Path` items as `AssetPath` Items. Each asset must have both a `Path` and an `AssetPath` in the whiskey.yml file." + $message)
            return
        }
        Set-ProGetAsset -Session $session -DirectoryName $TaskParameter['AssetDirectory'] -Path $name -FilePath $path
    }
}


function Publish-WhiskeyProGetUniversalPackage
{
    [CmdletBinding()]
    [Whiskey.Task("PublishProGetUniversalPackage")]
    [Whiskey.RequiresTool('PowerShellModule::ProGetAutomation','ProGetAutomationPath',Version='0.9.*',VersionParameterName='ProGetAutomationVersion')]
    param(
        [Parameter(Mandatory=$true)]
        [Whiskey.Context]
        $TaskContext,

        [Parameter(Mandatory=$true)]
        [hashtable]
        $TaskParameter
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    Import-WhiskeyPowerShellModule -Name 'ProGetAutomation'

    $exampleTask = 'Publish:
        - PublishProGetUniversalPackage:
            CredentialID: ProGetCredential
            Uri: https://proget.example.com
            FeedName: UniversalPackages'



    if( -not $TaskParameter['CredentialID'] )
    {
        Stop-WhiskeyTask -TaskContext $TaskContext -Message "CredentialID is a mandatory property. It should be the ID of the credential to use when connecting to ProGet:
 
        $exampleTask
 
        Use the `Add-WhiskeyCredential` function to add credentials to the build."

        return
    }

    if( -not $TaskParameter['Uri'] )
    {
        Stop-WhiskeyTask -TaskContext $TaskContext -Message "Uri is a mandatory property. It should be the URI to the ProGet instance where you want to publish your package:
 
        $exampleTask
        "

        return
    }

    if( -not $TaskParameter['FeedName'] )
    {
        Stop-WhiskeyTask -TaskContext $TaskContext -Message "FeedName is a mandatory property. It should be the name of the universal feed in ProGet where you want to publish your package:
 
        $exampleTask
        "

        return
    }

    $credential = Get-WhiskeyCredential -Context $TaskContext -ID $TaskParameter['CredentialID'] -PropertyName 'CredentialID'

    $session = New-ProGetSession -Uri $TaskParameter['Uri'] -Credential $credential

    if( -not ($TaskParameter.ContainsKey('Path')) )
    {
        $TaskParameter['Path'] = Join-Path -Path ($TaskContext.OutputDirectory | Split-Path -Leaf) -ChildPath '*.upack'
    }

    $errorActionParam = @{ }
    $allowMissingPackages = $false
    if( $TaskParameter.ContainsKey('AllowMissingPackage') )
    {
        $allowMissingPackages = $TaskParameter['AllowMissingPackage'] | ConvertFrom-WhiskeyYamlScalar
    }

    if( $allowMissingPackages )
    {
        $errorActionParam['ErrorAction'] = 'Ignore'
    }
    $packages = $TaskParameter['Path'] |
                    Resolve-WhiskeyTaskPath -TaskContext $TaskContext -PropertyName 'Path' @errorActionParam |
                    Where-Object {
                        if( -not $TaskParameter.ContainsKey('Exclude') )
                        {
                            return $true
                        }

                        foreach( $exclusion in $TaskParameter['Exclude'] )
                        {
                            if( $_ -like $exclusion )
                            {
                                return $false
                            }
                        }

                        return $true
                    }


    if( $allowMissingPackages -and -not $packages )
    {
        Write-WhiskeyVerbose -Context $TaskContext -Message ('There are no packages to publish.')
        return
    }

    if( -not $packages )
    {
        Stop-WhiskeyTask -TaskContext $TaskContext -PropertyDescription '' -Message ('Found no packages to publish. By default, the PublishProGetUniversalPackage task publishes all files with a .upack extension in the output directory. Check your whiskey.yml file to make sure you''re running the `ProGetUniversalPackage` task before this task (or some other task that creates universal ProGet packages). To publish other .upack files, set this task''s `Path` property to the path to those files. If you don''t want your build to fail when there are missing packages, then set this task''s `AllowMissingPackage` property to `true`.' -f $TaskContext.OutputDirectory)
        return
    }

    $feedName = $TaskParameter['FeedName']
    $taskPrefix = '[{0}] [{1}]' -f $session.Uri,$feedName

    $optionalParam = @{ }
    if( $TaskParameter['Timeout'] )
    {
        $optionalParam['Timeout'] = $TaskParameter['Timeout']
    }
    if( $TaskParameter['Overwrite'] )
    {
        $optionalParam['Force'] = $TaskParameter['Overwrite'] | ConvertFrom-WhiskeyYamlScalar
    }

    Write-WhiskeyVerbose -Context $TaskContext -Message ('{0}' -f $taskPrefix)
    foreach( $package in $packages )
    {
        Write-WhiskeyVerbose -Context $TaskContext -Message ('{0} {1}' -f (' ' * $taskPrefix.Length),$package)
        Publish-ProGetUniversalPackage -Session $session -FeedName $feedName -PackagePath $package @optionalParam -ErrorAction Stop
    }
}



function Set-WhiskeyVariable 
{
    [CmdletBinding()]
    [Whiskey.Task("SetVariable",SupportsClean=$true,SupportsInitialize=$true)]
    param(
        [Parameter(Mandatory=$true)]
        [Whiskey.Context]
        $TaskContext,

        [Parameter(Mandatory=$true)]
        [hashtable]
        $TaskParameter
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    foreach( $key in $TaskParameter.Keys )
    {
        if( $key -match '^WHISKEY_' )
        {
            Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Variable ''{0}'' is a built-in Whiskey variable and can not be changed.' -f $key)
            continue
        }
        Add-WhiskeyVariable -Context $TaskContext -Name $key -Value $TaskParameter[$key]
    }
}



function Set-WhiskeyVariableFromPowerShellDataFile
{
    [CmdletBinding()]
    [Whiskey.Task('SetVariableFromPowerShellDataFile')]
    param(
        [Parameter(Mandatory)]
        [Whiskey.Context]
        $TaskContext,

        [Parameter(Mandatory)]
        [hashtable]
        $TaskParameter,

        [Whiskey.Tasks.ValidatePath(Mandatory,PathType='File')]
        [string]
        $Path
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    $data = Import-PowerShellDataFile -Path $Path
    if( -not $data )
    {
        Stop-WhiskeyTask -TaskContext $TaskContext -PropertyName 'Path' -Message ('Failed to parse PowerShell Data File "{0}". Make sure this is a properly formatted PowerShell data file. Use the `Import-PowerShellDataFile` cmdlet.' -f $Path)
        return
    }

    function Set-VariableFromData
    {
        param(
            [object]
            $Variable,

            [hashtable]
            $Data,
            
            [string]
            $ParentPropertyName = ''
        )

        foreach( $propertyName in $Variable.Keys )
        {
            $variableName = $Variable[$propertyName]
            if( -not $Data.ContainsKey($propertyName) )
            {
                Stop-WhiskeyTask -TaskContext $TaskContext -PropertyName 'Variables' -Message ('PowerShell Data File "{0}" does not contain "{1}{2}" property.' -f $Path,$ParentPropertyName,$propertyName)
                continue
            }

            $variableValue = $Data[$propertyName]
            if( $variableName | Get-Member 'Keys' )
            {
                Set-VariableFromData -Variable $variableName -Data $variableValue -ParentPropertyName ('{0}{1}.' -f $ParentPropertyName,$propertyName)
                continue
            }

            Add-WhiskeyVariable -Context $TaskContext -Name $variableName -Value $variableValue
        }
    }

    Set-VariableFromData -Variable $TaskParameter['Variables'] -Data $data
}


function Set-WhiskeyVariableFromXml
{
    [Whiskey.Task("SetVariableFromXml")]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [Whiskey.Context]
        $TaskContext,

        [Parameter(Mandatory)]
        [hashtable]
        $TaskParameter,

        [Whiskey.Tasks.ValidatePath(Mandatory,PathType='File')]
        [string]
        $Path
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    Write-WhiskeyVerbose -Context $TaskContext -Message ($Path)
    [xml]$xml = $null
    try
    {
        $xml = Get-Content -Path $Path -Raw
    }
    catch
    {
        $Global:Error.RemoveAt(0)
        Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Exception reading XML from file "{0}": {1}"' -f $Path,$_)
        return
    }

    $nsManager = New-Object -TypeName 'Xml.XmlNamespaceManager' -ArgumentList $xml.NameTable
    $prefixes = $TaskParameter['NamespacePrefixes']
    if( $prefixes -and ($prefixes | Get-Member 'Keys') )
    {
        foreach( $prefix in $prefixes.Keys )
        {
            $nsManager.AddNamespace($prefix, $prefixes[$prefix])
        }
    }

    $allowMissingNodes = $TaskParameter['AllowMissingNodes'] | ConvertFrom-WhiskeyYamlScalar

    $variables = $TaskParameter['Variables']
    if( $variables -and ($variables | Get-Member 'Keys') )
    {
        foreach( $variableName in $variables.Keys )
        {
            $xpath = $variables[$variableName]
            $value = $xml.SelectNodes($xpath, $nsManager) | ForEach-Object {
                if( $_ | Get-Member 'InnerText' )
                {
                    $_.InnerText
                }
                elseif( $_ | Get-Member '#text' )
                {
                    $_.'#text'
                }
            }
            $exists = ' '
            if( $value -eq $null )
            {
                if( -not $allowMissingNodes )
                {
                    Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Variable {0}: XPath expression "{1}" matched no elements/attributes in XML file "{2}".' -f $variableName,$xpath,$Path)
                    return
                }
                $value = ''
                $exists = '!'
            }
            Write-WhiskeyVerbose -Context $TaskContext -Message (' {0} {1}' -f $exists,$xpath)
            Write-WhiskeyVerbose -Context $TaskContext -Message (' {0} = {1}' -f $variableName,($value | Select-Object -First 1))
            $value | Select-Object -Skip 1 | ForEach-Object {
                Write-WhiskeyVerbose -Context $TaskContext -Message (' {0} {1}' -f (' ' * $variableName.Length),$_)
            }
            Add-WhiskeyVariable -Context $TaskContext -Name $variableName -Value $value
        }
    }
}



function Set-WhiskeyTaskDefaults
{
    [CmdletBinding()]
    [Whiskey.Task("TaskDefaults",SupportsClean=$true,SupportsInitialize=$true)]
    param(
        [Parameter(Mandatory=$true)]
        [Whiskey.Context]
        $TaskContext,

        [Parameter(Mandatory=$true)]
        [hashtable]
        $TaskParameter
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    foreach ($taskName in $TaskParameter.Keys)
    {
        foreach ($propertyName in $TaskParameter[$taskName].Keys)
        {
            Add-WhiskeyTaskDefault -Context $TaskContext -TaskName $taskName -PropertyName $propertyname -Value $TaskParameter[$taskName][$propertyName] -Force
        }
    }
}



 function Set-WhiskeyVersion
{
    [CmdletBinding()]
    [Whiskey.Task("Version")]
    param(
        [Parameter(Mandatory)]
        [Whiskey.Context]
        $TaskContext,

        [Parameter(Mandatory)]
        [hashtable]
        $TaskParameter,

        [Whiskey.Tasks.ValidatePath(PathType='File')]
        [string]
        $Path
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    function ConvertTo-SemVer
    {
        param(
            [Parameter(Mandatory=$true,ValueFromPipeline=$true)]
            $InputObject,
            $PropertyName,
            $VersionSource
        )

        process
        {
            [SemVersion.SemanticVersion]$semver = $null
            if( -not [SemVersion.SemanticVersion]::TryParse($rawVersion,[ref]$semver) )
            {
                if( $VersionSource )
                {
                    $VersionSource = ' ({0})' -f $VersionSource
                }
                $optionalParam = @{ }
                if( $PropertyName )
                {
                    $optionalParam['PropertyName'] = $PropertyName
                }
                Stop-WhiskeyTask -TaskContext $TaskContext -Message ('''{0}''{1} is not a semantic version. See http://semver.org for documentation on semantic versions.' -f $rawVersion,$VersionSource) @optionalParam
            }
            return $semver
        }
    }

    [Whiskey.BuildVersion]$buildVersion = $TaskContext.Version
    [SemVersion.SemanticVersion]$semver = $buildVersion.SemVer2

    if( $TaskParameter[''] )
    {
        $rawVersion = $TaskParameter['']
        $semVer = $rawVersion | ConvertTo-SemVer -PropertyName 'Version'
    }
    elseif( $TaskParameter['Version'] )
    {
        $rawVersion = $TaskParameter['Version']
        $semVer = $rawVersion | ConvertTo-SemVer -PropertyName 'Version'
    }
    elseif( $Path )
    {
        $fileInfo = Get-Item -Path $Path
        if( $fileInfo.Extension -eq '.psd1' )
        {
            $rawVersion = Test-ModuleManifest -Path $Path -ErrorAction Ignore  | Select-Object -ExpandProperty 'Version'
            if( -not $rawVersion )
            {
                Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Unable to read version from PowerShell module manifest ''{0}'': the manifest is invalid or doesn''t contain a ''ModuleVersion'' property.' -f $Path)
                return
            }
            Write-WhiskeyVerbose -Context $TaskContext -Message ('Read version ''{0}'' from PowerShell module manifest ''{1}''.' -f $rawVersion,$Path)
            $semver = $rawVersion | ConvertTo-SemVer -VersionSource ('from PowerShell module manifest ''{0}''' -f $Path)
        }
        elseif( $fileInfo.Name -eq 'package.json' )
        {
            try
            {
                $rawVersion = Get-Content -Path $Path -Raw | ConvertFrom-Json | Select-Object -ExpandProperty 'Version' -ErrorAction Ignore
            }
            catch
            {
                Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Node package.json file ''{0}'' contains invalid JSON.' -f $Path)
                return
            }
            if( -not $rawVersion )
            {
                Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Unable to read version from Node package.json ''{0}'': the ''Version'' property is missing.' -f $Path)
                return
            }
            Write-WhiskeyVerbose -Context $TaskContext -Message ('Read version ''{0}'' from Node package.json ''{1}''.' -f $rawVersion,$Path)
            $semVer = $rawVersion | ConvertTo-SemVer -VersionSource ('from Node package.json file ''{0}''' -f $Path)
        }
        elseif( $fileInfo.Extension -eq '.csproj' )
        {
            [xml]$csprojXml = $null
            try
            {
                $csprojxml = Get-Content -Path $Path -Raw
            }
            catch
            {
                Stop-WhiskeyTask -TaskContext $TaskContext -Message ('.NET .cspoj file ''{0}'' contains invalid XMl.' -f $Path)
                return
            }

            if( $csprojXml.DocumentElement.Attributes['xmlns'] )
            {
                Stop-WhiskeyTask -TaskContext $TaskContext -Message ('.NET .csproj file ''{0}'' has an "xmlns" attribute. .NET Core/Standard .csproj files should not have a default namespace anymore (see https://docs.microsoft.com/en-us/dotnet/core/migration/). Please remove the "xmlns" attribute from the root "Project" document element. If this is a .NET framework .csproj, it doesn''t support versioning. Use the Whiskey Version task''s Version property to version your assemblies.' -f $Path)
                return
            }
            $csprojVersionNode = $csprojXml.SelectSingleNode('/Project/PropertyGroup/Version')
            if( -not $csprojVersionNode )
            {
                Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Element ''/Project/PropertyGroup/Version'' does not exist in .NET .csproj file ''{0}''. Please create this element and set it to the MAJOR.MINOR.PATCH version of the next version of your assembly.' -f $Path)
                return
            }
            $rawVersion = $csprojVersionNode.InnerText
            Write-WhiskeyVerbose -Context $TaskContext -Message ('Read version ''{0}'' from .NET Core .csproj ''{1}''.' -f $rawVersion,$Path)
            $semver = $rawVersion | ConvertTo-SemVer -VersionSource ('from .NET .csproj file ''{0}''' -f $Path)
        }
        elseif( $fileInfo.Name -eq 'metadata.rb' )
        {
            $metadataContent = Get-Content -Path $Path -Raw
            $metadataContent = $metadataContent.Split([Environment]::NewLine) | Where-Object { $_ -ne '' }

            $rawVersion = $null
            foreach( $line in $metadataContent )
            {
                if( $line -match '^\s*version\s+[''"](\d+\.\d+\.\d+)[''"]' )
                {
                    $rawVersion = $Matches[1]
                    break
                }
            }

            if( -not $rawVersion )
            {
                Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Unable to locate property "version ''x.x.x''" in metadata.rb file "{0}"' -f $Path )
                return
            }

            Write-WhiskeyVerbose -Context $TaskContext -Message ('Read version "{0}" from metadata.rb file "{1}".' -f $rawVersion,$Path)
            $semver = $rawVersion | ConvertTo-SemVer -VersionSource ('from metadata.rb file "{0}"' -f $Path)
        }
    }

    $prerelease = $TaskParameter['Prerelease']
    if( $prerelease -isnot [string] )
    {
        $foundLabel = $false
        foreach( $object in $prerelease )
        {
            foreach( $map in $object )
            {
                if( -not ($map | Get-Member -Name 'Keys') )
                {
                    Stop-WhiskeyTask -TaskContext $TaskContext -PropertyName 'Prerelease' -Message ('Unable to find keys in ''[{1}]{0}''. It looks like you''re trying use the Prerelease property to map branches to prerelease versions. If you want a static prerelease version, the syntax should be:
 
    Build:
    - Version:
        Prerelease: {0}
 
If you want certain branches to always have certain prerelease versions, set Prerelease to a list of key/value pairs:
 
    Build:
    - Version:
        Prerelease:
        - feature/*: alpha.$(WHISKEY_BUILD_NUMBER)
        - develop: beta.$(WHISKEY_BUILD_NUMBER)
    '
 -f $map,$map.GetType().FullName)
                    return
                }
                foreach( $wildcardPattern in $map.Keys )
                {
                    if( $TaskContext.BuildMetadata.ScmBranch -like $wildcardPattern )
                    {
                        Write-WhiskeyVerbose -Context $TaskContext -Message ('{0} -like {1}' -f $TaskContext.BuildMetadata.ScmBranch,$wildcardPattern)
                        $foundLabel = $true
                        $prerelease = $map[$wildcardPattern]
                        break
                    }
                    else
                    {
                        Write-WhiskeyVerbose -Context $TaskContext -Message ('{0} -notlike {1}' -f $TaskContext.BuildMetadata.ScmBranch,$wildcardPattern)
                    }
                }

                if( $foundLabel )
                {
                    break
                }
            }

            if( $foundLabel )
            {
                break
            }
        }

        if( -not $foundLabel )
        {
            $prerelease = ''
        }
    }

    if( $prerelease )
    {
        $buildSuffix = ''
        if( $semver.Build )
        {
            $buildSuffix = '+{0}' -f $semver.Build
        }

        $rawVersion = '{0}.{1}.{2}-{3}{4}' -f $semver.Major,$semver.Minor,$semver.Patch,$prerelease,$buildSuffix
        if( -not [SemVersion.SemanticVersion]::TryParse($rawVersion,[ref]$semver) )
        {
            Stop-WhiskeyTask -TaskContext $TaskContext -PropertyName 'Prerelease' -Message ('''{0}'' is not a valid prerelease version. Only letters, numbers, hyphens, and periods are allowed. See http://semver.org for full documentation.' -f $prerelease)
            return
        }
    }

    $build = $TaskParameter['Build']
    if( $build )
    {
        $prereleaseSuffix = ''
        if( $semver.Prerelease )
        {
            $prereleaseSuffix = '-{0}' -f $semver.Prerelease
        }

        $build = $build -replace '[^A-Za-z0-9\.-]','-'
        $rawVersion = '{0}.{1}.{2}{3}+{4}' -f $semver.Major,$semver.Minor,$semver.Patch,$prereleaseSuffix,$build
        if( -not [SemVersion.SemanticVersion]::TryParse($rawVersion,[ref]$semver) )
        {
            Stop-WhiskeyTask -TaskContext $TaskContext -PropertyName 'Build' -Message ('''{0}'' is not valid build metadata. Only letters, numbers, hyphens, and periods are allowed. See http://semver.org for full documentation.' -f $build)
            return
        }
    }

    # Build metadata is only available when running under a build server.
    if( $TaskContext.ByDeveloper )
    {
        $semver = New-Object -TypeName 'SemVersion.SemanticVersion' $semver.Major,$semVer.Minor,$semVer.Patch,$semver.Prerelease
    }

    $buildVersion.SemVer2 = $semver
    Write-WhiskeyInfo -Context $TaskContext -Message ('Building version {0}' -f $semver)
    $buildVersion.Version = [version]('{0}.{1}.{2}' -f $semver.Major,$semver.Minor,$semver.Patch)
    Write-WhiskeyVerbose -Context $TaskContext -Message ('Version {0}' -f $buildVersion.Version)
    $buildVersion.SemVer2NoBuildMetadata = New-Object 'SemVersion.SemanticVersion' ($semver.Major,$semver.Minor,$semver.Patch,$semver.Prerelease)
    Write-WhiskeyVerbose -Context $TaskContext -Message ('SemVer2NoBuildMetadata {0}' -f $buildVersion.SemVer2NoBuildMetadata)
    $semver1Prerelease = $semver.Prerelease
    if( $semver1Prerelease )
    {
        $semver1Prerelease = $semver1Prerelease -replace '[^A-Za-z0-9]',''
    }
    $buildVersion.SemVer1 = New-Object 'SemVersion.SemanticVersion' ($semver.Major,$semver.Minor,$semver.Patch,$semver1Prerelease)
    Write-WhiskeyVerbose -Context $TaskContext -Message ('SemVer1 {0}' -f $buildVersion.SemVer1)
}



function New-WhiskeyZipArchive
{
    [Whiskey.Task("Zip")]
    [Whiskey.RequiresTool('PowerShellModule::Zip','ZipPath',Version='0.3.*',VersionParameterName='ZipVersion')]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [Whiskey.Context]
        $TaskContext,

        [Parameter(Mandatory)]
        [hashtable]
        $TaskParameter
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    Import-WhiskeyPowerShellModule -Name 'Zip'

    $archivePath = $TaskParameter['ArchivePath']
    if( -not [IO.Path]::IsPathRooted($archivePath) )
    {
        $archivePath = Join-Path -Path $TaskContext.BuildRoot -ChildPath $archivePath
    }
    $archivePath = Join-Path -Path $archivePath -ChildPath '.'  # Get PowerShell to convert directory separator characters.
    $archivePath = [IO.Path]::GetFullPath($archivePath)
    $buildRootRegex = '^{0}(\\|/)' -f [regex]::Escape($TaskContext.BuildRoot)
    if( $archivePath -notmatch $buildRootRegex )
    {
        Stop-WhiskeyTask -TaskContext $TaskContext -Message ('ArchivePath: path to ZIP archive "{0}" is outside the build root directory. Please change this path so it is under the "{1}" directory. We recommend using a relative path, as that it will always be resolved relative to the build root directory.' -f $archivePath,$TaskContext.BuildRoot)
        return
    }

    $behaviorParams = @{ }
    if( $TaskParameter['CompressionLevel'] )
    {
        [IO.Compression.CompressionLevel]$compressionLevel = [IO.Compression.CompressionLevel]::NoCompression
        if( -not [Enum]::TryParse($TaskParameter['CompressionLevel'], [ref]$compressionLevel) )
        {
            Stop-WhiskeyTask -TaskContext $TaskContext -PropertyName 'CompressionLevel' -Message ('Value "{0}" is an invalid compression level. Must be one of: {1}.' -f $TaskParameter['CompressionLevel'],([enum]::GetValues([IO.Compression.CompressionLevel]) -join ', '))
            return
        }
        $behaviorParams['CompressionLevel'] = $compressionLevel
    }

    if( $TaskParameter['EntryNameEncoding'] )
    {
        $entryNameEncoding = $TaskParameter['EntryNameEncoding']
        [int]$codePage = 0
        if( [int]::TryParse($entryNameEncoding,[ref]$codePage) )
        {
            try
            {
                $entryNameEncoding = [Text.Encoding]::GetEncoding($codePage)
            }
            catch
            {
                Write-Error -ErrorRecord $_
                Stop-WhiskeyTask -TaskContext $TaskContext -Message ('EntryNameEncoding: An encoding with code page "{0}" does not exist. To get a list of encodings, run `[Text.Encoding]::GetEncodings()` or see https://docs.microsoft.com/en-us/dotnet/api/system.text.encoding . Use the encoding''s `CodePage` or `WebName` property as the value of this property.' -f $entryNameEncoding)
                return
            }
        }
        else
        {
            try
            {
                $entryNameEncoding = [Text.Encoding]::GetEncoding($entryNameEncoding)
            }
            catch
            {
                Write-Error -ErrorRecord $_
                Stop-WhiskeyTask -TaskContext $TaskContext -Message ('EntryNameEncoding: An encoding named "{0}" does not exist. To get a list of encodings, run `[Text.Encoding]::GetEncodings()` or see https://docs.microsoft.com/en-us/dotnet/api/system.text.encoding . Use the encoding''s "CodePage" or "WebName" property as the value of this property.' -f $entryNameEncoding)
                return
            }
        }
        $behaviorParams['EntryNameEncoding'] = $entryNameEncoding
    }

    $parentPathParam = @{ }
    $sourceRoot = $TaskContext.BuildRoot
    if( $TaskParameter.ContainsKey('SourceRoot') )
    {
        $sourceRoot = $TaskParameter['SourceRoot'] | Resolve-WhiskeyTaskPath -TaskContext $TaskContext -PropertyName 'SourceRoot'
        $parentPathParam['ParentPath'] = $sourceRoot
    }

    $sourceRootRegex = '^{0}' -f ([regex]::Escape($sourceRoot))

    Write-WhiskeyInfo -Context $TaskContext -Message ('Creating ZIP archive "{0}".' -f ($archivePath -replace $sourceRootRegex,'').Trim('\','/'))
    $archiveDirectory = $archivePath | Split-Path -Parent
    if( -not (Test-Path -Path $archiveDirectory -PathType Container) )
    {
        New-Item -Path $archiveDirectory -ItemType 'Directory' -Force | Out-Null
    }

    if( -not $TaskParameter['Path'] )
    {
        Stop-WhiskeyTask -TaskContext $TaskContext -Message ('Property "Path" is required. It must be a list of paths, relative to your whiskey.yml file, of files or directories to include in the ZIP archive.')
        return
    }

    New-ZipArchive -Path $archivePath @behaviorParams -Force

    foreach( $item in $TaskParameter['Path'] )
    {
        $override = $False
        if( (Get-Member -InputObject $item -Name 'Keys') )
        {
            $sourcePath = $null
            $override = $True
            foreach( $key in $item.Keys )
            {
                $destinationItemName = $item[$key]
                $sourcePath = $key
            }
        }
        else
        {
            $sourcePath = $item
        }

        $sourcePaths = $sourcePath | Resolve-WhiskeyTaskPath -TaskContext $TaskContext -PropertyName 'Path' @parentPathParam
        if( -not $sourcePaths )
        {
            return
        }

        foreach( $sourcePath in $sourcePaths )
        {
            $relativePath = $sourcePath -replace $sourceRootRegex,''
            $relativePath = $relativePath.Trim([IO.Path]::DirectorySeparatorChar, [IO.Path]::AltDirectorySeparatorChar)

            $addParams = @{ BasePath = $sourceRoot }
            $overrideInfo = ''
            if( $override )
            {
                $addParams = @{ PackageItemName = $destinationItemName }
                $overrideInfo = ' -> {0}' -f $destinationItemName
            }

            if( (Test-Path -Path $sourcePath -PathType Leaf) )
            {
                Write-WhiskeyInfo -Context $TaskContext -Message (' compressing file {0}{1}' -f $relativePath,$overrideInfo)
                Add-ZipArchiveEntry -ZipArchivePath $archivePath -InputObject $sourcePath @addParams @behaviorParams
                continue
            }

            function Find-Item
            {
                param(
                    [Parameter(Mandatory)]
                    $Path
                )

                if( (Test-Path -Path $Path -PathType Leaf) )
                {
                    return Get-Item -Path $Path
                }

                $Path = Join-Path -Path $Path -ChildPath '*'
                & {
                        Get-ChildItem -Path $Path -Include $TaskParameter['Include'] -Exclude $TaskParameter['Exclude'] -File
                        Get-Item -Path $Path -Exclude $TaskParameter['Exclude'] |
                            Where-Object { $_.PSIsContainer }
                    }  |
                    ForEach-Object {
                        if( $_.PSIsContainer )
                        {
                            Find-Item -Path $_.FullName
                        }
                        else
                        {
                            $_
                        }
                    }
            }

            if( $override )
            {
                $addParams['BasePath'] = $sourcePath
                $addParams['EntryParentPath'] = $destinationItemName
                $addParams.Remove('PackageItemName')
                $overrideInfo = ' -> {0}' -f $destinationItemName
            }

            $typeDesc = 'directory '
            if( $TaskParameter['Include'] -or $TaskParameter['Exclude'] )
            {
                $typeDesc = 'filtered directory'
            }

            Write-WhiskeyInfo -Context $TaskContext -Message (' compressing {0} {1}{2}' -f $typeDesc,$relativePath,$overrideInfo)
            Find-Item -Path $sourcePath |
                Add-ZipArchiveEntry -ZipArchivePath $archivePath @addParams @behaviorParams
        }
    }
}