BuildMasterAutomation.psm1


enum BMRaftItemTypeCode
{
    Module = 3
    Script = 4
    DeploymentPlan = 6
    Pipeline = 8
}

Add-Type -AssemblyName 'System.Web'

$script:warnings = @{}

function Write-WarningOnce
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, Position=0, ParameterSetName='Message', ValueFromPipeline)]
        [String] $Message
    )

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

        if( $script:warnings[$Message] )
        {
            return
        }

        Write-Warning -Message $Message
        $script:warnings[$Message] = $true
    }
}


$functionsDir = Join-Path -Path $PSScriptRoot -ChildPath 'Functions'
if( (Test-Path -Path $functionsDir -PathType Container) )
{
    foreach( $item in (Get-ChildItem -Path $functionsDir -Filter '*.ps1') )
    {
        . $item.FullName
    }
}



function Add-BMObjectParameter
{
    <#
    .SYNOPSIS
    Adds id or name values to a parameter hashtable (i.e. a hashtable used as the body of a request to a BuildMaster API
    endpoint).
 
    .DESCRIPTION
    Many of BuildMaster's APIs take an ID or a name. For example, many of the Release and Build Deployment methods
    accept either an `applicationId` parameter *or* an `applicationName` parameter. This function exists to allow
    BuildMasterAutomation functions to accept an object, an object's id or an object's name as a parameter. Pipe the
    hashtable that will be used as the body of a request to the BuildMaster APIs to `Add-BMObjectParameter`. Pass the
    name of the object type to the `Name` parameter and the object/id/name/value to the `Value` parameter.
 
    If the value passed is `$null`, nothing happens. If the value passed is a byte or an integer, the function adds a
    `$($Name)Id` parameter to the hashtable. If the value passed is a string, the function adds a `$($Name)Name`
    parameter. Otherwise, `Add-BMObjectParameter` the first property on the property named `id`, `$($Name)Id`, `name`,
    or `$($Name)Name` is added as `$($Name)Id` or `$($Name)Name` respectively.
 
    If the parameter must be a name parameter, use the `AsName` switch. If the parameter must be an id parameter, use
    the `AsID` switch.
 
    If the hashtable will be used as the body to a native API endpoint, use the `-ForNativeApi` switch. The native API
    uses `$($Name)_Id` and `$($Name)_Name` patterns for its id and name parameters.
 
    If you want to return the original hasthable so you can add more than one parameter to the hashtable, use the
    `-PassThru` switch.
 
    .EXAMPLE
    $parameters | Add-BMObjectParameter -Name 'release' -Value $release
 
    Demonstrates how to add the id property from an object to a hashtable used as the body to a BuildMaster API
    endpoint. In this case, `$release` is a release object returned by the BuildMaster APi, so has a `releaseId`
    property. `Add-BMObjectParameter` will add a `releaseId` key to the hashtable with a value of `$release.releaseId`.
 
    .EXAMPLE
    $parameters | Add-BMObjectParameter -Name 'release' -Value $releaseId
 
    Demonstrates how to add an id to a hashtable used as the body to a BuildMaster API. In this case, `$releaseId` is
    the id of a release. `Add-BMObjectParameter` will add a `releaseId` key to the hashtable with a value of
    `$releaseId`.
 
    .EXAMPLE
    $parameters | Add-BMObjectParameter -Name 'release' -Value $releaseName
 
    Demonstrates how to add a name to a hashtable used as the body to a BuildMaster API. In this case, `$releaseName` is
    the name of a release. `Add-BMObjectParameter` will add a `releaseName` key to the hashtable with a value of
    `$releaseName`.
 
    .EXAMPLE
    $parameters | Add-BMObjectParameter -Name 'pipeline' -Value $pipeline -AsName
 
    Demonstrates how to force `Add-BMObjectParameter` to ignore any id properties and only use name properties, if they
    exist, by using the `AsName` switch.
 
    .EXAMPLE
    $parameters | Add-BMObjectParameter -Name 'application' -Value $app -ForNativeApi
 
    Demonstrates how to add an id parameter to a parameter hashtable used as the body of a request to the BuildMaster
    *Native* API. In this case, `$app` is an application object returned by the BuildMaster API. `Add-BMObjectParameter`
    will add an `application_Id` key to the hasthable with a value of `$app.application_Id`.
 
    .EXAMPLE
    $parameter | Add-BMObjectParameter -Name 'application' -Value $app -PassThru | Add-BMObjectParameter -Name 'pipeline' -Value $pipeline
 
    Demonstrates how you can use the `PassThru` switch to add multiple parameters to a parameters hashtable using a
    pipeline.
    #>

    [CmdletBinding(DefaultParameterSetName='IdOrName')]
    param(
        # The hashtable to add the parameter to.
        [Parameter(Mandatory, ValueFromPipeline)]
        [hashtable] $Parameter,

        # The name of the parameter, *without* the `Id` or `Name` suffix. The suffix is added automatically based on the
        # type of the parameter value.
        [Parameter(Mandatory)]
        [String] $Name,

        # The object, id, or name.
        [Parameter(Mandatory)]
        [AllowEmptyString()]
        [AllowNull()]
        [Object] $Value,

        # If true, will return the hashtable.
        [switch] $PassThru,

        # The parameters are being used in the native API, which has a different naming convention. If true, parameter
        # names will use an underscore in the parameter name added to the hashtable, e.g. `_Id` or `_Name`.
        [switch] $ForNativeApi,

        # If set, id properties on the incoming object will be ignored.
        [Parameter(Mandatory, ParameterSetName='AsName')]
        [switch] $AsName,

        # If set, name properties on the incoming object will be ignored.
        [Parameter(Mandatory, ParameterSetName='AsID')]
        [switch] $AsID
    )

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

        try
        {
            if ($null -eq $Value)
            {
                return
            }

            $nameParamNameSuffix = 'Name'
            $idParamNameSuffix = 'Id'
            if ($ForNativeApi)
            {
                $nameParamNameSuffix = "_$($nameParamNameSuffix)"
                $idParamNameSuffix = "_$($idParamNameSuffix)"
            }
            $nameParamName = "$($Name)$($nameParamNameSuffix)"
            $idParamName = "$($Name)$($idParamNameSuffix)"

            if ($AsName)
            {
                $name = $Value | Get-BMObjectName -ObjectTypeName $Name
                if (-not $name)
                {
                    return
                }
                $Parameter[$nameParamName] = $name
                return
            }

            if ($AsId)
            {
                $id = $Value | Get-BMObjectID -ObjectTypeName $Name
                if (-not $id)
                {
                    return
                }
                $Parameter[$idParamName] = $id
                return
            }

            $id = $Value | Get-BMObjectId -ObjectTypeName $Name -ErrorAction Ignore
            if ($id)
            {
                $Parameter[$idParamName] = $id
                return
            }

            $name = $Value | Get-BMObjectName -ObjectTypeName $Name -ErrorAction Ignore
            if ($name)
            {
                $Parameter[$nameParamName] = $name
                return
            }

            $msg = "Object ""$($Value)"" isn't an id or name, nor does it have any $($Name)Id, $($Name)_Id, " +
                   "$($Name)Name, or $($Name)_Name properties."
            Write-Error -Message $msg -ErrorAction $ErrorActionPreference
        }
        finally
        {
            if ($PassThru)
            {
                $Parameter | Write-Output
            }
        }
    }
}




function Add-BMParameter
{
    <#
    .SYNOPSIS
    Adds values to a parameter hashtable (i.e. a hashtable used as the body of a request to a BuildMaster API endpoint).
 
    .DESCRIPTION
    The `Add-BMParameter` function adds values to a parameter hashtable. Pipe the hashtable to the function (or pass it
    to the `Parameter` parameter). Pass the parameter name to the `Name` parameter and the value to the `Value`
    parameter. If the value is not null, it will be added to the hashtable.
 
    This function lets you simplify adding optional parameters to a parameter hashtable. Instead of:
 
         if ($null -ne $Value)
         {
            $parameters[$Name] = $Value
         }
 
    this function lets you write:
 
        $parameters | Add-BMParameter -Name $Name -Value $Value
 
    It also lets you chain multiple parameters together by using the `-PassThru` switch:
 
        $parameters |
            Add-BMParameter -Name $Name1 -Value $Value1 -PassThru |
            Add-BMParameter -Name $Name2 -Value $Value2 -PassThru |
            Add-BMParameter -Name $Name3 -Value $Value3
 
    .EXAMPLE
    $parameters | Add-BMParameter -Name $Name -Value $Value
 
    Demonstrates how to add an optional parameter to the parameter hashtable `$parameters`. In this case, if `$Value`
    is not null, `Add-BMParameter` adds `$Value` into `$parameters` using key `$Name`, e.g.
    `$parameters[$Name] = $Value`.
 
    .EXAMPLE
    $parameters | Add-BMParameter -Name $Name -Value $Value -PassThru | Add-BMParameter -Name $Name2 -Value $Value2
 
    Demonstrates how you can add multiple parameters to a parameter hashtable by using the `-PassThru` switch, which
    returns the parameter hashtable, which be piped to `Add-BMParameter`.
    #>

    [CmdletBinding()]
    param(
        # The hashtable to add the parameter to.
        [Parameter(Mandatory, ValueFromPipeline)]
        [hashtable] $Parameter,

        # The name of the parameter.
        [Parameter(Mandatory)]
        [String] $Name,

        # The value of the parameter. If the value is not null, it is added to the `$Parameter` hashtable using the
        # name argument as the key.
        [Parameter(Mandatory)]
        [AllowEmptyString()]
        [AllowNull()]
        [Object] $Value,

        # If set, returns the hashtable piped (or passed to parameter `$Parameter). This lets you create a pipeline of
        # calls to `Add-BMParameter`.
        [switch] $PassThru
    )

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

        if ($null -ne $Value)
        {
            if ($Value -is [hashtable])
            {
                $Parameter[$Name] = $Value[$Name]
            }
            elseif ($Value -is [Enum])
            {
                $enumType = [Enum]::GetUnderlyingType($Value.GetType())
                $Parameter[$Name] = [Convert]::ChangeType($Value, $enumType)
            }
            else
            {
                $Parameter[$Name] = $Value
            }
        }

        if ($PassThru)
        {
            return $Parameter
        }
    }


}




function Add-BMPipelineMember
{
    <#
    .SYNOPSIS
    Adds `Pipeline_Name` and `Pipeline_Id` properties to an object.
 
    .DESCRIPTION
    In BuildMaster 6.2, pipeline objects are now rafts and now have `RaftItem_Name` and `RaftItem_Id` properties instead
    of `Pipeline_Name` and `Pipeline_Id` objects. This function adds `Pipeline_Name` and `Pipeline_Id` *alias*
    properties to an object that alias the `RaftItem_Name` and `RaftItem_Id` properties, respectively. You should only
    pipe raft items that represent pipelines to this function. It does *not* validate the incoming object.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [Object] $Pipeline,

        [switch] $PassThru
    )

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

        if (-not $Pipeline)
        {
            return
        }

        $Pipeline |
            Add-Member -Name 'Pipeline_Name' -MemberType AliasProperty -Value 'RaftItem_Name' -PassThru |
            Add-Member -Name 'Pipeline_Id' -MemberType AliasProperty -Value 'RaftItem_Id' -PassThru:$PassThru
    }
}


function Add-PSTypeName
{
    <#
    .SYNOPSIS
    Adds a BuildMaster type name to an object.
 
    .DESCRIPTION
    The `Add-PSTypeName` function adds BuildMaster type names to an object. These types don't actually exist. The type
    names are used by PowerShell to decide what formats to use when displaying an object.
 
    If the `Server` switch is set, it adds a `Inedo.BuildMaster.Server` type name.
 
    If the `RaftItem` switch is set, it adds a `Inedo.BuildMaster.RaftItem` type name.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [Object] $InputObject,

        [Parameter(Mandatory, ParameterSetName='Server')]
        [switch] $Server,

        [Parameter(Mandatory, ParameterSetName='RaftItem')]
        [switch] $RaftItem
    )

    process
    {
        Set-StrictMode -Version 'Latest'

        $typeName = 'Inedo.BuildMaster.{0}' -f $PSCmdlet.ParameterSetName
        $InputObject.pstypenames.Add( $typeName )
        $InputObject | Write-Output
    }
}


function ConvertFrom-BMNativeApiByteValue
{
    <#
    .SYNOPSIS
    Converts a binary value returned from the BuildMaster native API as a `byte[]` object to a string.
 
    .DESCRIPTION
    Some of the objects returned by the BuildMaster native API have properties that are typed as byte arrays, i.e.
    `byte[]`.This function converts these values into strings. Pipe the value to the function (or pass it to the
    `InputObject` parameter).
 
    .EXAMPLE
    ConvertFrom-BMNativeApiByteValue 'ZW1wdHk='
 
    Demonstrates how to convert a `byte[]` value returned by a BuildMaster native API into its original string/text.
 
    .EXAMPLE
    'ZW1wdHk=' | ConvertFrom-BMNativeApiByteValue
 
    Demonstrates that you can pipe values to `ConvertFrom-BMNativeApiByteValue`.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipeline, Position=0)]
        [String] $InputObject
    )

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

    process
    {
        $bytes = [Convert]::FromBase64String($InputObject)
        [Text.Encoding]::UTF8.GetString($bytes) | Write-Output
    }

    end
    {
    }
}



function ConvertFrom-BMOtterScriptExpression
{
    <#
    .SYNOPSIS
    Converts an Otterscript expression into a PowerShell object.
 
    .DESCRIPTION
    The `ConvertFrom-BMOtterScriptExpression` function takes an OtterScript expression as an input and converts it into
    a PowerShell representation of the object. This function supports converting both `vector` and `map` types into
    their respective `array` and `hashtable` types in PowerShell.
 
    .LINK
    https://docs.inedo.com/docs/executionengine-otterscript-strings-and-literals
 
    .EXAMPLE
    "@(1, 2, 3, 4)" | ConvertFrom-BMOtterScriptExpression
 
    Demonstrates converting an OtterScript vector into a PowerShell array.
 
    .EXAMPLE
    "%(hello: world)" | ConvertFrom-BMOtterScriptExpression
 
    Demonstrates converting an OtterScript map into a PowerShell hashtable.
 
    .EXAMPLE
    "%(hello: %(hi: world))" | ConvertFrom-BMOtterScriptExpression
 
    Demonstrates converting nested OtterScript maps into nested PowerShell hashtables.
 
    .EXAMPLE
    "@(1, 2, @(3, 4))" | ConvertFrom-BMOtterScriptExpression
 
    Demonstrates converting nested OtterScript vectors into nested PowerShell arrays.
 
    .EXAMPLE
    "@(1, 2, %(hello: world, hi there: @(5, 6, 7)))" | ConvertFrom-BMOtterScriptExpression
 
    Demonstrates converting nested OtterScript vectors and maps into nested PowerShell arrays and hashtables.
    #>

    [CmdletBinding()]
    param(
        # The OtterScript expression to convert to a PowerShell object.
        [Parameter(Mandatory, ValueFromPipeline)]
        [String] $Value
    )

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

        function Edit-Output
        {
            param(
                [Parameter(Mandatory, ValueFromPipeline)]
                $InputObject
            )

            $InputObject = $InputObject.Trim()

            $maybeInt = 0
            if (([Int64]::TryParse($InputObject, [ref] $maybeInt)))
            {
                return $maybeInt
            }

            return $InputObject
        }
    }

    process {
        $originalValue = $Value
        $Value = $Value.Trim()

        $isMap = $Value.StartsWith('%(') -and $Value.EndsWith(')')
        $isVector = $Value.StartsWith('@(') -and $Value.EndsWith(')')
        $isScalar = -not $isMap -and -not $isVector

        if ($isScalar)
        {
            if ($Value.StartsWith('@(') -or $Value.StartsWith('%('))
            {
                $msg = "Unable to convert '${originalValue}' to a PowerShell Object because of invalid syntax. " +
                       'Returning original value.'
                Write-Warning -Message $msg
            }
            return $Value | Edit-Output
        }

        $Value = $Value -replace '\)$'

        if ($isMap)
        {
            $Value = $Value -replace '^%\('
            if (-not $Value)
            {
                return @{}
            }
        }
        else
        {
            $Value = $Value -replace '^@\('
            if (-not $Value)
            {
                return ,@()
            }
        }

        $closesNeeded = 0

        # Splitting up array or map by comma and collecting into array.
        $parsedItems = &{
            $start = 0
            $end = 0
            foreach ($i in 0..($Value.Length - 1))
            {
                $char = $Value[$i]

                if ($char -eq '(' -and ($Value[$i - 1] -eq '@' -or $Value[$i - 1] -eq '%'))
                {
                    $closesNeeded++
                    continue
                }

                if ($char -eq ')' -and $closesNeeded)
                {
                    $closesNeeded--
                    if ($i -ne ($Value.Length - 1))
                    {
                        continue
                    }
                }

                if ($closesNeeded -or ($char -ne ',' -and $i -ne ($Value.Length - 1)))
                {
                    continue
                }

                $end = $i
                $lengthOfSubstring = $end - $start

                if ($i -eq ($Value.Length - 1))
                {
                    $lengthOfSubstring++
                }

                $Value.Substring($start, $lengthOfSubstring).Trim() | Write-Output
                $start = $i + 1
            }
        }

        $invalidSyntax = $closesNeeded -gt 0

        if ($isMap)
        {
            foreach ($mapItem in $parsedItems)
            {
                if  ($mapItem -notmatch '^[\w\d\s\-]+:')
                {
                    $invalidSyntax = $true
                    break
                }
            }
        }

        if ($invalidSyntax)
        {
            $msg = "Unable to convert '${originalValue}' to a PowerShell Object because of invalid syntax. " +
                   'Returning original value.'
            Write-Warning -Message $msg
            return $originalValue | Edit-Output
        }

        if ($isVector)
        {
            return ,@($parsedItems | ConvertFrom-BMOtterScriptExpression)
        }

        $hashtable = @{}

        foreach ($kvpair in $parsedItems)
        {
            $colonIndex = $kvpair.IndexOf(':')
            $mapKey = $kvpair.Substring(0, $colonIndex) | Edit-Output
            $mapValue = $kvpair.Substring($colonIndex + 1) | ConvertFrom-BMOtterScriptExpression

            $hashtable[$mapKey] = $mapValue
        }

        return $hashtable
    }
}


function ConvertTo-BMNativeApiByteValue
{
    <#
    .SYNOPSIS
    Converts a string into a value that can be passed to a `byte[]` parameter in the BuildMaster native API.
 
    .DESCRIPTION
    Some of the parameters of the BuildMaster native API are typed as `byte[]` object. This function converts strings
    into a value that can be passed as one of these parameters. Pipe the string you want to convert (or pass it to the
    `InputObject` parameter). The function will return a value that you can pass to the BuildMaster API.
 
    If you pipe multiple strings to `ConvertTo-BMNativeApiByteValue`, the strings will be concatenated together before
    conversion.
 
    .EXAMPLE
    ConvertTo-BMNativeApiByteValue 'hello example'
 
    Demonstrates how to convert a string into value that can be passed to a `byte[]`-typed parameter on the BuildMaster
    native API.
 
    .EXAMPLE
    'hello','example' | ConvertTo-BMNativeApiByteValue
 
    Demonstrates that you can pipe strings to `ConvertTo-BMNativeApiByteValue`. All the strings piped in will be
    concatenated together before conversion.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipeline, Position=0)]
        [String] $InputObject
    )

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

        $allStrings = [Text.StringBuilder]::New()
    }

    process
    {
        [void] $allStrings.Append($InputObject)
    }

    end
    {
        $stringBytes = [Text.Encoding]::UTF8.GetBytes($allStrings.ToString())
        [Convert]::ToBase64String($stringBytes) | Write-Output
    }
}


function ConvertTo-BMOtterScriptExpression
{
    <#
    .SYNOPSIS
    Converts a PowerShell object into an OtterScript expression.
 
    .DESCRIPTION
    The `ConvertTo-BMOtterScriptExpression` function takes a PowerShell object as an input and returns a representation
    of the object in OtterScript. This function converts .NET IEnumerable and IDictionary objects to OtterScript vector
    and map types respectively.
 
    .LINK
    https://docs.inedo.com/docs/executionengine-otterscript-strings-and-literals
 
    .EXAMPLE
    ,@(1, 2, 3, 4) | ConvertTo-BMOtterScriptExpression
 
    Demonstrates turning an IEnumerable of PowerShell integers into an array of OtterScript integers. Output will be
    `@(1, 2, 3, 4)`
 
    .EXAMPLE
    @{ 'hello' = 'world'; 'goodbye' = 'world' } | ConvertTo-BMOtterScriptExpression
 
    Demonstrates turning a PowerShell IDictionary into an OtterScript map. Output will be `%(hello: world, goodbye: world)`
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [Object] $Value
    )

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

    process {
        $isDict = $Value -is [System.Collections.IDictionary]
        $isList = $Value -is [System.Collections.IEnumerable] -and $Value -is [System.Collections.ICollection]

        if ($Value -is [String] -or $Value -is [Int])
        {
            return $Value.ToString()
        }

        if (-not $isDict -and -not $isList)
        {
            $valueType = ($Value | Get-Member).TypeName | Select-Object -Unique
            $msg = "Unable to convert '${valueType}' to OtterScript expression. All values must either inherit " +
                   'the IDictionary or the IEnumarable interface or be of type String or Int.'
            Write-Error -Message $msg -ErrorAction $ErrorActionPreference
            return
        }

        if ($isDict)
        {
            $mapExpression = '%('
            $sortedKeys = $Value.Keys | Sort-Object
            foreach ($key in $sortedKeys)
            {
                if ($Value[$key] -is [System.Collections.ICollection])
                {
                    $Value[$key] = ConvertTo-BMOtterScriptExpression -Value $Value[$key]
                }
                $mapExpression += "${key}: $($Value[$key]), "
            }

            $mapExpression = $mapExpression -replace ', $'
            $mapExpression += ')'
            return $mapExpression
        }

        $result = & {
            foreach ($item in $Value)
            {
                if ($item -is [System.Collections.ICollection])
                {
                    $item = ConvertTo-BMOtterScriptExpression -Value $item
                    $item | Write-Output
                    continue
                }

                $item | Write-Output
            }
        }
        return "@($($result -join ', '))"
    }
}



function Disable-BMApplication
{
    <#
    .SYNOPSIS
    Disables a BuildMaster application
 
    .DESCRIPTION
    The `Disable-BMApplication` function disables an application in BuildMaster, which removes the application from the
    BuildMaster UI and reports. Pass the application name, id, or application object to the `Application` parameter. Or,
    pipe the name, id, or applicatoin object into the function.
 
    This function uses the native API, which can change without notice between releases. The API key you use must have
    access to the native API.
 
    .EXAMPLE
    Disable-BMApplication -Session $session -Application 494
 
    Demonstrates how to delete an application using its ID.
 
    .EXAMPLE
    'Disable Me!' | Get-BMApplication -Session $session | Disable-BMApplication -Session $session
 
    Demonstrates that you can pipe applications into `Disable-BMApplication`.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessage('PSShouldProcess', '')]
    [CmdletBinding(SupportsShouldProcess)]
    param(
        # The session to BuildMaster. Use `New-BMSession` to create a session.
        [Parameter(Mandatory)]
        [Object] $Session,

        # The application to get. Pass an application name, id, or application object.
        [Parameter(Mandatory, ValueFromPipeline)]
        [Alias('ID')]
        [Object] $Application
    )

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

        $bmApp = $Application | Get-BMApplication -Session $Session
        if (-not $bmApp)
        {
            return
        }

        $deactivateParams = @{} | Add-BMObjectParameter -Name 'Application' -Value $bmApp -AsID -ForNativeApi -PassThru
        Invoke-BMNativeApiMethod -Session $Session `
                                 -Name 'Applications_DeactivateApplication' `
                                 -Parameter $deactivateParams `
                                 -Method Post
    }
}



function Get-BMApplication
{
    <#
    .SYNOPSIS
    Gets BuildMaster applications.
 
    .DESCRIPTION
    The `Get-BMApplication` function gets all active applications from an instance of BuildMaster. Use the `Force`
    switch to include inactive applications.
 
    To get a specific application, pass its id, name (wildcards supported), or object to the `Application` parameter.
    The function writes an error if the application does not exist.
 
    .EXAMPLE
    Get-BMApplication -Session $session
 
    Demonstrates how to get all active BuildMaster applications
 
    .EXAMPLE
    Get-BMApplication -Session $session -Force
 
    Demonstrates how to get all active *and* inactive/disabled BuildMaster applications.
 
    .EXAMPLE
    Get-BMApplication -Session $session -Name 'MyApplication'
 
    Demonstrates how to get a specific application.
    #>

    [CmdletBinding(DefaultParameterSetName='AllApplications')]
    param(
        # The session to BuildMaster. Use `New-BMSession` to create a session.
        [Parameter(Mandatory)]
        [Object] $Session,

        # The application to get. Pass an application id, name (wildcards supported), or object.
        [Parameter(Mandatory, ValueFromPipeline, ParameterSetName='SpecificApplication')]
        [Alias('Name')]
        [Object] $Application,

        # Force `Get-BMApplication` to return inactive/disabled applications.
        [Parameter(ParameterSetName='AllApplications')]
        [switch] $Force
    )

    process
    {
        Set-StrictMode -Version 'Latest'
        Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
        # Invoke-BMNativeApiMethod uses POST, but we're reading data, so always make the request.
        $WhatIfPreference = $false

        $searching = $Application -and `
                     ($Application | Test-BMName) -and `
                     [wildcardpattern]::ContainsWildcardCharacters($Application)

        $parameters =
            @{
                Application_Count = 0;
                IncludeInactive_Indicator = ($Force.IsPresent -or $PSCmdlet.ParameterSetName -eq 'SpecificApplication');
            } |
            Add-BMObjectParameter -Name 'Application' -Value $Application -ForNativeApi -PassThru

        $endpoint = 'Applications_GetApplication'
        if ($PSCmdlet.ParameterSetName -eq 'AllApplications' -or $searching)
        {
            $endpoint = 'Applications_GetApplications'
        }
        $apps = @()
        Invoke-BMNativeApiMethod -Session $Session -Name $endpoint -Parameter $parameters -Method Post |
            ForEach-Object {
                if ($_ | Get-Member -Name 'Applications_Extended')
                {
                    return $_.Applications_Extended
                }
                return $_
            } |
            Where-Object {
                if ($searching)
                {
                    return $_.Application_Name -like $Application
                }
                return $true
            } |
            Tee-Object -Variable 'apps' |
            Write-Output

        if ($Application -and -not $apps -and -not $searching)
        {
            $msg = "Application ""$($Application | Get-BMObjectName -ObjectTypeName 'Application')"" does not exist."
            Write-Error -Message $msg -ErrorAction $ErrorActionPreference
        }
    }
}



function Get-BMApplicationGroup
{
    <#
    .SYNOPSIS
    Gets BuildMaster application groups.
 
    .DESCRIPTION
    The `Get-BMApplicationGroup` function gets all application groups from an instance of BuildMaster.
 
    To get a specific application group, pass its id, name (wildcards supported), or an application group object to the
    `ApplicationGroup` parameter, or pipe them into the function. If the application group isn't found, the function
    writes an error.
 
    .EXAMPLE
    Get-BMApplicationGroup -Session $session
 
    Demonstrates how to get all BuildMaster application groups.
 
    .EXAMPLE
    Get-BMApplicationGroup -Session $session -ApplicationGroup 'My Application Group'
 
    Demonstrates how to get a specific application group.
    #>

    [CmdletBinding()]
    param(
        # The session to BuildMaster. Use `New-BMSession` to create a session.
        [Parameter(Mandatory)]
        [Object] $Session,

        # The application group to get. Pass an id, name (wildcards supported), or application group object.
        [Parameter(ValueFromPipeline)]
        [Alias('Name')]
        [Object] $ApplicationGroup
    )

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

        $appGroups = @()
        $appGroupName = $ApplicationGroup | Get-BMObjectName -Strict -ErrorAction Ignore
        Invoke-BMNativeApiMethod -Session $Session -Name 'ApplicationGroups_GetApplicationGroups' -Method Post |
            Where-Object {
                if( $appGroupName )
                {
                    return $_.ApplicationGroup_Name -like $appGroupName
                }

                return $true
            } |
            Tee-Object -Variable 'appGroups' |
            Write-Output

        $searching = $appGroupName -and [wildcardpattern]::ContainsWildcardCharacters($appGroupName)
        if ($ApplicationGroup -and -not $appGroups -and -not $searching)
        {
            $msg = "Application group ""$($appGroupName)"" does not exist."
            Write-Error -Message $msg -ErrorAction $ErrorActionPreference
            return
        }
    }
}



function Get-BMBuild
{
    <#
    .SYNOPSIS
    Gets a build from BuildMaster.
 
    .DESCRIPTION
    The `Get-BMBuild` function gets a build from BuildMaster. With no parameters, it returns all builds. To get all the
    builds that are part of a release, pass a release id or object to the `Release` parameter. To get a specific build,
    pass a build id or object to the `Build` parameter.
 
    This function uses BuildMaster's
    [Release and Build Deployment API](https://docs.inedo.com/docs/buildmaster-reference-api-release-and-build).
 
    .EXAMPLE
    Get-BMBuild -Session $session
 
    Demonstrates how to get all builds.
 
    .EXAMPLE
    Get-BMBuild -Session $session -Build $build
 
    Demonstrates how to get a specific build using a build object.
 
    .EXAMPLE
    Get-BMBuild -Session $session -Build 500
 
    Demonstrates how to get a specific build using its id.
 
    .EXAMPLE
    Get-BMBuild -Session $session -Release $release
 
    Demonstrates how to get all the builds that are part of a release using a release object.
 
    .EXAMPLE
    Get-BMBuild -Session $session -Release 438
 
    Demonstrates how to get all the builds that are part of a release using the release's id.
    #>

    [CmdletBinding(DefaultParameterSetName='AllBuilds')]
    param(
        # A session object to BuildMaster. Use the `New-BMSession` function to create a session.
        [Parameter(Mandatory)]
        [Object] $Session,

        # The build to get. Can be a build id or object.
        [Parameter(Mandatory, ParameterSetName='SpecificBuild')]
        [Object] $Build,

        # The release whose builds to get. Can be a release id or object.
        [Parameter(Mandatory, ParameterSetName='ReleaseBuilds')]
        [Object] $Release
    )

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

    $parameter = @{}
    if( $PSCmdlet.ParameterSetName -eq 'SpecificBuild' )
    {
        $parameter | Add-BMObjectParameter -Name 'build' -Value $Build
    }
    elseif( $PSCmdlet.ParameterSetName -eq 'ReleaseBuilds' )
    {
        $bmRelease = $Release | Get-BMRelease -Session $session
        if (-not $bmRelease)
        {
            $msg = "Failed to get builds for release ""$($Release | Get-BMObjectName)"" because the release does not " +
                   'exist.'
            Write-Error -Message $msg -ErrorAction $ErrorActionPreference
            return
        }
        $parameter | Add-BMObjectParameter -Name 'release' -Value $bmRelease
    }

    $parameterParam = @{ }
    if ($parameter.Count)
    {
        $parameterParam['Parameter'] = $parameter
    }

    $builds = @()
    Invoke-BMRestMethod -Session $Session -Name 'releases/builds' @parameterParam -Method Post |
        Where-Object {
            # There's a bug in BuildMaster's API that returns builds for multiple releases. We don't want this.
            if( $PSCmdlet.ParameterSetName -eq 'ReleaseBuilds' )
            {
                return $_.releaseId -eq $parameter.releaseId
            }
            return $true
        } |
        Tee-Object -Variable 'builds' |
        Write-Output

    if ($PSCmdlet.ParameterSetName -eq 'SpecificBuild' -and -not $builds)
    {
        $msg = "Build ""$($Build | Get-BMObjectName -PropertyName 'buildNumber')"" does not exist."
        Write-Error -Message $msg -ErrorAction $ErrorActionPreference
    }
}



function Get-BMDeployment
{
    <#
    .SYNOPSIS
    Gets a deployment from BuildMaster.
 
    .DESCRIPTION
    The Get-BMDeployment function gets deployments from BuildMaster. Pass a deployment ID to the Deployment parameter to
    get a single deployment. To filter for one or more deployments, pass the criteria to filter by to the rest of the
    parameters. Each parameter is combined into a logical "AND" that is used to filter for deployments. Only the
    deployments that match all the parameters are returned.
 
    Pass the current BuildMaster session to the `Session` parameter.
 
    Pass the deployment id or deployment object to the `Deployment` parameter.
 
    Pass the application name, id, or object to the `Application` parameter.
 
    Pass the release name, id, or object to the `Release` parameter.
 
    Pass the build name, id, or object to the `Build` parameter.
 
    Pass the environment name, id, or object to the `Environment` parameter.
 
    Pass the release number to the `ReleaseNumber` parameter.
 
    Pass the build number to the `BuildNumber` parameter.
 
    Pass the pipeline name to the `PipelineName` parameter.
 
    Pass the pipeline stage name to the `PipelineStageName` parameter.
 
    This function uses the
    [Release and Build Deployment API](https://docs.inedo.com/docs/buildmaster-reference-api-release-and-build).
 
    .EXAMPLE
    Get-BMDeployment -Session $session
 
    Demonstrates how to get all deployments from the instance of BuildMaster.
 
    .EXAMPLE
    Get-BMDeployment -Session $session -Deploytment $deployment
 
    Demonstrates how to get a specific deployment by passing a deployment object to the `Deployment` parameter. The
    `Get-BMDeployment` function looks for an `id` property on the object.
    #>

    [CmdletBinding()]
    param(
        # The session to BuildMaster. Use `New-BMSession` to create a session.
        [Parameter(Mandatory)]
        [Object] $Session,

        # The deployment to get. You can pass a deployment id or object.
        [Parameter(Mandatory, ParameterSetName='ById')]
        [Alias('ID')]
        [Object] $Deployment,

        # The application to get deployments for. You can pass an application id, application name, or application object.
        [Parameter(ParameterSetName='ByFilter')]
        [Object] $Application,

        # The release to get deployments for. You can pass an release id, release name, or release object.
        [Parameter(ParameterSetName='ByFilter')]
        [Object] $Release,

        # The build to get deployments for. You can pass an build id, build name, build number, or build object.
        [Parameter(ParameterSetName='ByFilter')]
        [Object] $Build,

        # The environment to get deployments for. You can pass an environment id, environment name, or environment object.
        [Parameter(ParameterSetName='ByFilter')]
        [Object] $Environment,

        # The name of the pipeline to get deployments for.
        [Parameter(ParameterSetName='ByFilter')]
        [String] $Pipeline,

        # The name of the pipeline stage to get deployments for.
        [Parameter(ParameterSetName='ByFilter')]
        [String] $Stage,

        # The status of the deployments to get. Accepted values are 'pending', 'executing', 'succeeded', 'warned', or 'failed'.
        [Parameter(ParameterSetName='ByFilter')]
        [ValidateSet('pending', 'executing', 'succeeded', 'warned', 'failed')]
        [String] $Status
    )

    process
    {

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

        $parameter =
            @{ } |
            Add-BMObjectParameter -Name 'deployment' -Value $Deployment -PassThru |
            Add-BMObjectParameter -Name 'application' -Value $Application -PassThru |
            Add-BMObjectParameter -Name 'environment' -Value $Environment -PassThru |
            Add-BMObjectParameter -Name 'release' -Value $Release -PassThru

        if($Build)
        {
            if ($Build -is [string] -and -not [Int64]::TryParse($Build, [ref] $Build))
            {
                $parameter['buildNumber'] = $Build
            }
            else
            {
                $parameter = $parameter | Add-BMObjectParameter -Name 'build' -Value $Build -PassThru
            }
        }

        if($Pipeline)
        {
            $parameter['pipelineName'] = $Pipeline
        }
        if($Stage)
        {
            $parameter['pipelineStageName'] = $Stage
        }
        if($Status)
        {
            $parameter['status'] = $Status
        }

        $deployments = @()
        Invoke-BMRestMethod -Session $Session -Name 'releases/builds/deployments' -Parameter $parameter -Method Post |
            Tee-Object -Variable 'deployments' |
            Write-Output

        if (-not $deployments)
        {
            if ($PSCmdlet.ParameterSetName -eq 'ById')
            {
                $msg = "Unable to get deployment ""$($Deployment | Get-BMObjectName)"" because it does not exist."
            }
            else
            {
                $params = ($parameter.Keys | ForEach-Object { "$($_) = $($parameter[$_])"}) -join ', '
                $msg = "No deployments exist that match: ""$($params)""."
            }

            Write-Error -Message $msg -ErrorAction $ErrorActionPreference
        }
    }
}



function Get-BMEnvironment
{
    <#
    .SYNOPSIS
    Returns environments from a BuildMaster instance.
 
    .DESCRIPTION
    The `Get-BMEnvironment` function gets all the environments from an instance of BuildMaster.
 
    To return a specific environment, pass its name to the `Name` parameter. If an environment with the given name
    doesn't exist, you'll get an error. You can use wildcards to search for active environments.
 
    Pass a session object representing the instance of BuildMaster to use to the `Session` parameter. Use
    `New-BMSession` to create a session object.
 
    This function uses BuildMaster's native APIs.
 
    .EXAMPLE
    Get-BMEnvironment
 
    Demonstrates how to return a list of all BuildMaster active environments.
 
    .EXAMPLE
    Get-BMEnvironment -Name '*Dev*'
 
    Demonstrates how to use wildcards to search for active environments.
    #>

    [CmdletBinding()]
    param(
        # The session to BuildMaster. Use `New-BMSession` to create a session.
        [Parameter(Mandatory)]
        [Object] $Session,

        # The name of the environment to return. If one doesn't exist, you'll get an error. Wildcards supported when
        # passing an environment name. If no environments match the wildcard pattern, no error is returned.
        [Parameter(ValueFromPipeline)]
        [Alias('Name')]
        [Object] $Environment
    )

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

        $environments = $null

        $environmentName = $Environment | Get-BMObjectName -Strict -ErrorAction Ignore
        $searching = $environmentName -and [wildcardpattern]::ContainsWildcardCharacters($environmentName)

        Invoke-BMRestMethod -Session $Session -Name 'infrastructure/environments/list' |
            Where-Object {
                # Only return environments that match the user's search.
                if ($environmentName)
                {
                    return $_.name -like $environmentName
                }
                return $true
            } |
            ForEach-Object {
                # BuildMaster API doesn't always return these properties.
                $_ | Add-Member -MemberType NoteProperty -Name 'parentName' -Value '' -ErrorAction Ignore
                return $_
            } |
            Tee-Object -Variable 'environments' |
            Write-Output

        if ($Environment -and -not $environments -and -not $searching)
        {
            $msg = "Environment ""$($Environment | Get-BMObjectName)"" does not exist."
            Write-Error -Message $msg -ErrorAction $ErrorActionPreference
        }
    }
}


function Get-BMObjectID
{
    <#
    .SYNOPSIS
    Gets the ID from a BuildMaster object.
 
    .DESCRIPTION
    The `Get-BMObjectID` gets the value of the ID from a BuildMaster object. Pipe the object to the function (or pass
    it to the `InputObject` property). Pass the object type name to the `ObjectTypeName` property. The function
    inspects the object passed in and:
 
    * if the object is $null, returns $null.
    * if the object is a numeric value, returns it.
    * returns the value of the object's `id` property, if it exists.
    * returns the value of the object's `$(ObjectTypeName)_Id` (e.g. Raft_Id, Application_Id) property, if it exists.
    * returns the value of the object's `$(ObjectTypeName)Id` property, if it exists.
    * if it can't find an id, writes an error and returns nothing.
 
    If you know the exact name of the property you want returned as an id, pass its name to the `PropertyName`
    parameter. In this case, the function inspects the object passed in and:
 
    * if the object is $null, returns $null.
    * if the object is a numeric value, returns it.
    * returns the value of the object's `$PropertyName` property, if it exists.
 
    .EXAMPLE
    1 | Get-BMObjectID -ObjectNameType DoesNotMatter
 
    Demonstrates that `Get-BMObjectID` will always return any integer value it is passed.
 
    .EXAMPLE
    $raft | Get-BMObjectID -ObjectTypeName 'Raft'
 
    Demonstrates how to get the id from an object returned by any BuildMaster API. In this case, the object is a raft,
    and the function will return the value of the first of these properties to exist: `id`, `Raft_Id`, `RaftId`.
 
    .EXAMPLE
    $raftItem | Get-BMObjectID -PropertyName 'ApplicationGroup_Id'
 
    Demonstrates how to get the value of an id using a specific property name. In this example, if `$raftItem` is an
    integer, it will be returned, otherwise, the value of the `$raftItem.ApplicationGroup_Id` is returned.
    #>

    [CmdletBinding(DefaultParameterSetName='Default')]
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [Object] $InputObject,

        [Parameter(Mandatory, ParameterSetName='ByPropertyName')]
        [String] $PropertyName,

        [Parameter(Mandatory, ParameterSetName='ByObjectTypeName')]
        [String] $ObjectTypeName
    )

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

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

        if ($InputObject | Test-BMID)
        {
            return [int]$InputObject
        }

        if (-not $PropertyName)
        {
            $PropertyName = 'Id'
        }

        if ($InputObject | Get-Member -Name $PropertyName)
        {
            return [int]($InputObject.$PropertyName)
        }

        if ($PSBoundParameters.ContainsKey('PropertyName'))
        {
            $msg = "Object does not have a ""${PropertyName}"" property."
            Write-Error -Message $msg -ErrorAction $ErrorActionPreference
            return
        }

        if (-not $ObjectTypeName)
        {
            $ObjectTypeName = '*'
        }

        $idProperty = $InputObject | Get-Member -Name "$($ObjectTypeName)_Id"
        if (-not $idProperty)
        {
            $idProperty = $InputObject | Get-Member -Name "$($ObjectTypeName)Id"
            if (-not $idProperty)
            {
                $msg = "Object ""$($InputObject)"" is not an id and does not have ""Id"", ""$($ObjectTypeName)_Id"", " +
                       "or ""$($ObjectTypeName)Id"" properties."
                Write-Error $msg -ErrorAction $ErrorActionPreference
                return
            }
        }

        $nameCount = ($idProperty | Measure-Object).Count
        if ($nameCount -gt 1)
        {
            $msg = "Object has multiple id properties: ""$($idProperty -join '", "')"". Use the " +
                   '"PropertyName" parameter to set the name of the property to get.'
            Write-Error $msg -ErrorAction $ErrorActionPreference
            return
        }

        return [int]($InputObject.($idProperty.Name))
    }
}


function Get-BMObjectName
{
    <#
    .SYNOPSIS
    Returns the name of an object that was returned by the BuildMaster API.
 
    .DESCRIPTION
    The BuildMasterAutomation module allows you to pass ids, names, or objects as the value to many parameters. Use the
    `Get-BMObjectName` function to get the name of one of these parameter values.
 
    If passed a string or an id, those will be returned as the name. Otherwise, the function looks for a `Name`
    property, a property matching wildcard `*_Name`, and then a property matching wildcard `*Name`, and returns the
    value of the first property found. If no properties are found, the function writes an error.
 
    If an object has multiple properties that could be its name, pass the name of the property to use to the
    `PropertyName` function.
 
    .EXAMPLE
    $app | Get-BMObjectName
 
    Demonstrates how to get the name of an application object. In this case, the value of the application's
    `Application_Name` property will be returned.
    #>

    [CmdletBinding(DefaultParameterSetName='Default')]
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [Object] $InputObject,

        [Parameter(Mandatory, ParameterSetName='ByPropertyName')]
        [String] $PropertyName,

        [Parameter(Mandatory, ParameterSetName='ByObjectTypeName')]
        [String] $ObjectTypeName,

        [switch] $Strict
    )

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

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

        if ($InputObject | Test-BMName)
        {
            return $InputObject
        }

        if (-not $PropertyName)
        {
            $PropertyName = 'Name'
        }

        if ($InputObject | Get-Member -Name $PropertyName)
        {
            return $InputObject.$PropertyName
        }

        if ($PSBoundParameters.ContainsKey('PropertyName'))
        {
            if (-not $Strict -and ($InputObject | Test-BMID))
            {
                return $InputObject
            }

            $msg = "Object does not have a ""${PropertyName}"" property."
            Write-Error -Message $msg -ErrorAction $ErrorActionPreference
            return
        }

        if (-not $ObjectTypeName)
        {
            $ObjectTypeName = '*'
        }

        $nameProperty = $InputObject | Get-Member -Name "$($ObjectTypeName)_Name"
        if (-not $nameProperty)
        {
            $nameProperty = $InputObject | Get-Member -Name "$($ObjectTypeName)Name"
            if (-not $nameProperty)
            {
                if (-not $Strict -and ($InputObject | Test-BMID))
                {
                    return $InputObject
                }

                $msg = "Object ""$($InputObject)"" does not have ""Name"", ""$($ObjectTypeName)_Name"", or " +
                       """$($ObjectTypeName)Name"" properties."
                Write-Error $msg -ErrorAction $ErrorActionPreference
                return
            }
        }

        $nameCount = ($nameProperty | Measure-Object).Count
        if ($nameCount -gt 1)
        {
            $msg = "Object has multiple name properties: ""$($nameProperty -join '", "')"". Use the " +
                   '"PropertyName" parameter to set the name of the property to get.'
            Write-Error $msg -ErrorAction $ErrorActionPreference
            return
        }

        return $InputObject.($nameProperty.Name)
    }
}


function Get-BMPackage
{
    <#
    .SYNOPSIS
    Obsolete. Use `Get-BMBuild` instead.
    #>

    [CmdletBinding(DefaultParameterSetName='AllBuilds')]
    param(
        [Parameter(Mandatory)]
        [Object] $Session,

        [Parameter(Mandatory, ParameterSetName='SpecificPackage')]
        [Object] $Package,

        [Parameter(Mandatory, ParameterSetName='ReleasePackages')]
        [Object] $Release
    )

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

    $msg = 'The BuildMasterAutomation module''s "Get-BMPackage" function is obsolete and will be removed in a future ' +
           'version of BuildMasterAutomation. Use the "Get-BMBuild" function instead.'
    Write-WarningOnce $msg

    $getArgs = @{}
    if ($PSCmdlet.ParameterSetName -eq 'SpecificPackage')
    {
        $getArgs['Build'] = $Package
    }
    elseif( $PSCmdlet.ParameterSetName -eq 'ReleasePackages')
    {
        $getArgs['Release'] = $Release
    }
    Get-BMBuild -Session $Session @getArgs
}



function Get-BMPipeline
{
    <#
    .SYNOPSIS
    Gets pipelines from BuildMaster.
 
    .DESCRIPTION
    The `Get-BMPipeline` function gets all pipelines. You can filter the list of pipelines by raft, pipeline name, and
    application by using the `Raft`, `Pipeline`, and `Application` parameters, respectively. To get only pipelines in a
    specific raft, pass the raft id or a raft object to the `Raft` parameter. To get a specific pipeline, pass its name
    or a pipeline object to the `Pipeline` parameter. To get pipelines for a specific application, pass the
    application id or application object to the `Application` parameter. If using multiple filter parameters, only
    pipelines that match all the filter parameters are returned.
 
    To search for a pipeline using a wildcard, pass a wildcard pattern to the `Pipeline` parameter.
 
    This function uses the `Rafts_GetRaftItems` native API method.
 
    .EXAMPLE
    Get-BMPipeline -Session $session -Raft $raft
 
    Demonstrates how to get all the pipelines across all rafts and applications.
 
    .EXAMPLE
    Get-BMPipeline -Session $session -Raft $raft
 
    Demonstrates how to get all the pipelines for a specific raft.
 
    .EXAMPLE
    Get-BMPipeline -Session $session -Pipeline 'BuildMaster Automation'
 
    Demonstrates how to get pipelines by name. If there are multiple pipelines across rafts and applications with the
    same name, they will all be returned.
 
    .EXAMPLE
    Get-BMPipeline -Session $session -Pipeline '*Automation'
 
    Demonstrates that you can use wildcards in the `Name` parameter's value to search for pipelines.
 
    .EXAMPLE
    Get-BMPipeline -Session $session -Raft $raft -Application 39
 
    Demonstrates how to get a specific application's pipelines stored in a specific raft.
 
    .EXAMPLE
    Get-BMPipeline -Session $session -Raft $raft -Application $app -Pipeline 'Pipeline 2'
 
    Demonstrates how to get an application's pipeline using an application object and the pipeline's name.
    #>

    [CmdletBinding()]
    param(
        # A session object to BuildMaster. Use the `New-BMSession` function to creates a session.
        [Parameter(Mandatory)]
        [Object] $Session,

        # The raft in which to search for the pipeline.
        [Object] $Raft,

        # The pipeline to get. Pass a pipeline name (wildcards supported), or a pipeline object.
        [Parameter(ValueFromPipeline)]
        [Alias('Name')]
        [Object] $Pipeline,

        # The application whose pipelines to get. Passing application ids or objects are supported.
        [Alias('ApplicationID')]
        [Object] $Application
    )

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

        Get-BMRaftItem -Session $session `
                       -Raft $Raft `
                       -RaftItem $Pipeline `
                       -Application $Application `
                       -TypeCode ([BMRaftItemTypeCode]::Pipeline) |
            Add-BMPipelineMember -PassThru
    }
}



function Get-BMRaft
{
    <#
    .SYNOPSIS
    Gets rafts from BuildMaster.
 
    .DESCRIPTION
    The `Get-BMRaft` function returns all rafts from BuildMaster.
 
    To get a specific raft, pass its name, id, or raft object to the `Raft` parameter (or pipe them into the function).
    If a raft with the given ID or represented by the object doesn't exist, the function writes an error and returns.
    If a string is passed to the `Raft` parameter, and it contains wildcards, the function will return all rafts whose
    names match the wildcard pattern. Otherwise, it will return the raft with that name and if it doesn't find a raft
    with that name, it writes an error and returns nothing.
 
    This function uses the native API.
 
    .EXAMPLE
    Get-BMRaft -Session $session
 
    Demonstrates how to use `Get-BMRaft` to get all rafts.
    #>

    [CmdletBinding()]
    param(
        # The session to BuildMaster. Use the `New-BMSession` function to create a session.
        [Parameter(Mandatory)]
        [Object] $Session,

        # The raft id, name, or raft object to get. If you pass a string, and the string has wildcards, all rafts whose
        # name matches the wildcard pattern are returned.
        [Parameter(ValueFromPipeline)]
        [Object] $Raft
    )

    process
    {
        Set-StrictMode -Version 'Latest'
        Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
        $WhatIfPreference = $false  # We only get stuff in this function.

        $searching = ($Raft | Test-BMName) -and [wildcardpattern]::ContainsWildcardCharacters($Raft)
        $raftName = $Raft | Get-BMObjectName -ObjectTypeName 'Raft' -ErrorAction Ignore

        $endpointName = 'Rafts_GetRaft'
        if ($searching -or -not $Raft)
        {
            $endpointName = 'Rafts_GetRafts'
        }

        $parameters = @{}
        if ($endpointName -eq 'Rafts_GetRaft')
        {
            $raftID = $Raft | Get-BMObjectID -ObjectTypeName 'Raft' -ErrorAction Ignore
            if ($raftID)
            {
                $parameters['Raft_Id'] = $raftID
            }
            elseif ($raftName)
            {
                $parameters['Raft_Name'] = $raftName
            }
        }

        $raftPrefixScriptBlock = {
            if ($this.Raft_Id -eq 1)
            {
                return 'global'
            }

            return $this.Raft_Name
        }

        $rafts = @()
        Invoke-BMNativeApiMethod -Session $Session -Name $endpointName -Method Post -Parameter $parameters |
            Where-Object {
                if ($searching)
                {
                    return $_.Raft_Name -like $raftName
                }

                return $true
            } |
            Add-Member -Name 'Raft_Prefix' `
                       -MemberType ScriptProperty `
                       -Value $raftPrefixScriptBlock `
                       -PassThru `
                       -ErrorAction Ignore |
            Tee-Object -Variable 'rafts' |
            Write-Output

        if ($Raft -and -not $rafts -and -not $searching)
        {
            $msg = "Raft ""$($Raft | Get-BMObjectName)"" does not exist."
            Write-Error -Message $msg -ErrorAction $ErrorActionPreference
        }
    }
}


function Get-BMRaftItem
{
    <#
    .SYNOPSIS
    Gets raft items from BuildMaster.
 
    .DESCRIPTION
    The `Get-RaftItem` function gets all raft items across rafts and applications.
 
    To get only raft items in a specific raft, pass the raft's id or raft object to the `Raft` parameter.
 
    To get a specific raft item by its name, pass the name or raft item's object to the `RaftItem` parameter.
 
    To get raft items assigned to a specific application, pass the application id or application object to the
    `Application` parameter.
 
    To get raft items for a specific type, pass the type code to the `TypeCode parameter.
 
    Raft items are only returned if they match all parameters passed.
 
    Uses the BuildMaster native API.
 
    .EXAMPLE
    Get-BMRaftItem -Session $session -Raft $raftID
 
    Demonstrates how to get all the items from a specific raft. In this case, all raft items in the raft with id
    `$raftID` in any or no application are returned.
 
    .EXAMPLE
    Get-BMRaftItem -Session $session -Name '*yolo*'
 
    Demonstrates how to get raft items whose name matches a specific wildcard pattern. In this case, all raft items
    across rafts and applications whose names match `*yolo*` will be returned.
 
    .EXAMPLE
    Get-BMRaftItem -Session $session -Application $appOrIdOrName
 
    Demonstrates how to get raft items from a specific application. You can pass an application id or object to the
    `-Application` parameter.
 
    .EXAMPLE
    Get-BMRaftItem -Session $session -Raft $raftID -TypeCode Pipeline
 
    Demonstrates how to get all raft items of a specific type. In this case, all pipeline raft items across all rafts
    and applications are returned.
 
    .EXAMPLE
    Get-BMRaftItem -Session $session -Raft $raft -RaftItem 'specific' -Application $app -TypeCode Pipeline
 
    Demonstrates how to get a specific pipeline. In this case, it will return the pipeline raft item named `specific`
    from the `$raft` raft, assigned to application `$app`.
    #>

    [CmdletBinding()]
    param(
        # A session object to the BuildMaster instance to use. Use the `New-BMSession` function to create a session.
        [Parameter(Mandatory)]
        [Object] $Session,

        # The raft id or raft object whose items to return.
        [Object] $Raft,

        # The raft item to get. Pass the raft name (wildcards supported) or raft item object.
        [Parameter(ValueFromPipeline)]
        [Object] $RaftItem,

        # The application id or application object whose items to get.
        [Object] $Application,

        # The raft item types to return.
        [BMRaftItemTypeCode] $TypeCode
    )

    begin
    {
        Set-StrictMode -Version 'Latest'
        Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
        $WhatIfPreference = $false # Gets items, but the API requires a POST.

        $appFilter = $null
    }

    process
    {
        $getRaftArgs = @{}
        if ($Raft)
        {
            $getRaftArgs['Raft'] = $Raft
        }

        $searching = ($RaftItem | Test-BMName) -and [wildcardpattern]::ContainsWildcardCharacters($RaftItem)

        $raftItems = $null
        & {
                # BuildMaster's API requires a raft ID at minimum, so use the one provided by the user or search all
                # rafts.
                foreach ($currentRaft in (Get-BMRaft -Session $Session @getRaftArgs))
                {
                    $getArgs =
                        @{} | Add-BMObjectParameter -Name 'Raft' -Value $currentRaft -ForNativeApi -AsID -PassThru

                    if ($RaftItem -and -not $searching)
                    {
                        $getArgs | Add-BMObjectParameter -Name 'RaftItem' -Value $RaftItem -AsName -ForNativeApi
                    }

                    if ($PSBoundParameters.ContainsKey('TypeCode'))
                    {
                        $getArgs | Add-BMParameter -Name 'RaftItemType_Code' -Value $TypeCode
                    }

                    if (-not $Application)
                    {
                        # If no Application_Id parameter, BuildMaster's API only returns pipelines that are not
                        # associated with an application.
                        Invoke-BMNativeApiMethod -Session $Session `
                                                 -Name 'Rafts_GetRaftItems' `
                                                 -Method Post `
                                                 -Parameter $getArgs
                    }

                    if ($null -eq $appFilter)
                    {
                        if ($Application)
                        {
                            $appFilter = $Application
                        }
                        else
                        {
                            $appFilter = Get-BMApplication -Session $Session
                        }
                    }

                    # Get all raft items associated with the users application or any application.
                    foreach ($appItem in $appFilter)
                    {
                        $getArgs | Add-BMObjectParameter -Name 'Application' -Value $appItem -ForNativeApi -AsID
                        Invoke-BMNativeApiMethod -Session $Session `
                                                 -Name 'Rafts_GetRaftItems' `
                                                 -Method Post `
                                                 -Parameter $getArgs
                    }
                }
            } |
            Where-Object {
                if ($searching)
                {
                    return $_.RaftItem_Name -like $RaftItem
                }
                return $true
            } |
            Tee-Object -Variable 'raftItems' |
            Add-PSTypeName -RaftItem |
            Add-Member -Name 'Type' -MemberType ScriptProperty -Value {
                    switch ($this.RaftItemType_Code)
                    {
                        3 { return 'Module' }
                        4 { return 'Script' }
                        6 { return 'DeploymentPlan' }
                        8 { return 'Pipeline' }
                        default { return $this.RaftItemType_Code }
                    }
                } -PassThru |
            Add-Member -Name 'Content' -MemberType ScriptProperty -Value {
                    $this.Content_Bytes | ConvertFrom-BMNativeApiByteValue
                } -PassThru |
            Write-Output


        if ($RaftItem -and -not $searching -and -not $raftItems)
        {
            $appMsg = ''
            if ($Application)
            {
                $appMsg = " in application ""$($Application | Get-BMObjectName -ObjectTypeName 'Application')"""
            }
            $typeCodeName = $TypeCode | Get-BMRaftTypeDisplayName -ErrorAction Ignore
            $msg = "$($typeCodeName) ""$($RaftItem | Get-BMObjectName -ObjectTypeName 'RaftItem')""$($appMsg) " +
                   'does not exist.'
            Write-Error -Message $msg -ErrorAction $ErrorActionPreference
        }
    }
}


function Get-BMRaftTypeDisplayName
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [AllowNull() ]
        [BMRaftItemTypeCode] $TypeCode
    )

    process
    {
        if ($null -eq $TypeCode)
        {
            return 'Raft Item'
        }

        switch ($TypeCode)
        {
            'DeploymentPlan' { return 'Deployment Plan' }
            default { $TypeCode.ToString() }
        }
    }
}


function Get-BMRelease
{
    <#
    .SYNOPSIS
    Gets the release for an application in BuildMaster.
 
    .DESCRIPTION
    The `Get-BMRelease` function gets releases in BuildMaster. It uses the
    [Release and Build Deployment API](https://docs.inedo.com/docs/buildmaster-reference-api-release-and-build).
 
    To get a specific release, pass a release object, release ID, or release name to the `Release` parameter. If the
    release doesn't exist, the function will write an error.
 
    To get all the releases for a specific application, pass an application object, application ID, or application name
    to the `Application` parameter. You can get a specific application's release by passing the release's name to the
    `Name` parameter.
 
    .EXAMPLE
    Get-BMRelease -Session $session -Release $release
 
    Demonstrates how to get a specific release by passing a release object to the `Release` parameter. The
    `Get-BMRelease` function looks for an `id` or `name` property on the object.
 
    .EXAMPLE
    Get-BMRelease -Session $session -Application $app
 
    Demonstrates how to get all the releases for an application by passing an application object to the `Application`
    parameter. The application object must have a`Application_Id`, `id`, `Application_Name`, or `name` properties.
 
    .EXAMPLE
    Get-BMRelease -Session $session -Application 34
 
    Demonstrates how to get all the releases for an application by passing its ID to the `Application` parameter.
 
    .EXAMPLE
    Get-BMRelease -Session $session -Application 'BuildMasterAutomation'
 
    Demonstrates how to get all the releases for an application by passing its name to the `Application` parameter.
 
    .EXAMPLE
    Get-BMRelease -Session $session -Application 'BuildMasterAutomation' -Name '4.1'
 
    Demonstrates how to get a specific release for an application by passing the release's name to the `Name` parameter.
    In this example, the '4.1' release will be returned, if it exists.
    #>

    [CmdletBinding()]
    param(
        # The session to BuildMaster. Use `New-BMSession` to create a session.
        [Parameter(Mandatory)]
        [Object] $Session,

        # The release to get. Pass a release id, name, or object.
        [Parameter(ValueFromPipeline)]
        [Alias('Name')]
        [Object] $Release,

        # The application whose releases to get. Pass an application id, name, or object.
        [Object] $Application
    )

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

        if ($Application -and -not ($Application | Get-BMApplication -Session $Session))
        {
            return
        }

        $parameter =
            @{} |
            Add-BMObjectParameter -Name 'release' -Value $Release -PassThru |
            Add-BMObjectParameter -Name 'application' -Value $Application -PassThru

        $releases = @()
        Invoke-BMRestMethod -Session $Session -Name 'releases' -Parameter $parameter -Method Post |
            Tee-Object -Variable 'releases' |
            Write-Output

        if ($Release -and -not $releases)
        {
            $appMsg = ''
            if ($Application)
            {
                $appMsg = " in application ""$($Application | Get-BMObjectName)"""
            }

            $msg = "Release ""$($Release | Get-BMObjectName)""$($appMsg) does not exist."
            Write-Error -Message $msg -ErrorAction $ErrorActionPreference
        }
    }
}


function Get-BMServer
{
    <#
    .SYNOPSIS
    Returns servers in BuildMaster.
 
    .DESCRIPTION
    The `Get-BMServer` function gets all the servers from an instance of BuildMaster. To return a specific server,
    pipe the server's id, name (wildcards supported), or a server object to the function (or pass to the `Server`
    parameter). If the server doesn't exist, the function writes an error.
 
    The BuildMaster API returns plaintext versions of a server's API key (if it is using AES encryption). This function
    converts those keys into `SecureString`s to make it harder to accidentally view/save them.
 
    This function uses BuildMaster's infrastructure management API.
 
    .EXAMPLE
    Get-BMServer
 
    Demonstrates how to return a list of all BuildMaster servers.
 
    .EXAMPLE
    '*example*' | Get-BMServer
 
    Demonstrates how to use wildcards to search for a server.
    #>

    [CmdletBinding()]
    param(
        # The session to BuildMaster. Use `New-BMSession` to create a session.
        [Parameter(Mandatory)]
        [Object]$Session,

        # The name of the server to return. Wildcards supported. By default, all servers are returned.
        [Parameter(ValueFromPipeline)]
        [Alias('Name')]
        [Object] $Server
    )

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

        $servers = $null

        $serverName = $Server | Get-BMObjectName -Strict -ErrorAction Ignore
        $searching = $serverName -and [wildcardpattern]::ContainsWildcardCharacters($serverName)

        # BuildMaster API doesn't always return all a server's members.
        $memberNames = @(
            'name',
            'roles',
            'environments',
            'serverType',
            'hostName',
            'port',
            'encryptionType',
            'encryptionKey',
            'requireSsl',
            'credentialsName',
            'tempPath',
            'wsManUrl',
            'active',
            'variables'
        )

        Invoke-BMRestMethod -Session $Session -Name 'infrastructure/servers/list' |
            Where-Object {
                if ($serverName)
                {
                    return ($_.name -like $serverName)
                }
                return $true
            } |
            Add-PSTypeName -Server |
            ForEach-Object {
                $server = $_
                foreach ($memberName in $memberNames)
                {
                    if( -not ($server | Get-Member -Name $memberName) )
                    {
                        $server | Add-Member -MemberType NoteProperty -Name $memberName -Value $null
                    }
                }

                if( $server.encryptionKey )
                {
                    $server.encryptionKey = ConvertTo-SecureString -String $_.encryptionKey -AsPlainText -Force
                }
                $server
            } |
            Tee-Object -Variable 'servers' |
            Write-Output

        if (-not $searching -and $Server -and -not $servers)
        {
            $msg = "Server ""$($Server | Get-BMObjectName)"" does not exist."
            Write-Error -Message $msg -ErrorAction $ErrorActionPreference
        }
    }
}


function Get-BMServerRole
{
    <#
    .SYNOPSIS
    Returns the server roles.
 
    .DESCRIPTION
    The `Get-BMServerRole` function gets all the server roles from an instance of BuildMaster. To return a specific
    role, pipe a server role id, name (wildcards supported), or a server role object to the function (or pass to the
    `ServerRole` parameter). If the server isn't found, the function write an error.
 
    This function uses BuildMaster's infrastructure management API.
 
    .EXAMPLE
    Get-BMServerRole
 
    Demonstrates how to return a list of all BuildMaster server roles.
 
    .EXAMPLE
    '*Service*' | Get-BMServerRole
 
    Demonstrates how to use wildcards to search for a service role.
    #>

    [CmdletBinding()]
    param(
        # The session to BuildMaster. New `New-BMSession` to create a session.
        [Parameter(Mandatory)]
        [Object] $Session,

        # The server role to return. Pass a server role id, name (wildcards supported), or server role object.
        [Parameter(ValueFromPipeline)]
        [Object] $ServerRole
    )

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

        $roles = $null
        $serverRoleName = $ServerRole | Get-BMObjectName -Strict -ErrorAction Ignore
        $searching = $serverRoleName -and [wildcardpattern]::ContainsWildcardCharacters($serverRoleName)

        Invoke-BMRestMethod -Session $Session -Name 'infrastructure/roles/list' |
            Where-Object {
                if ($serverRoleName)
                {
                    return ($_.name -like $serverRoleName)
                }
                return $true
            } |
            Tee-Object -Variable 'roles' |
            Write-Output

        if (-not $searching -and $ServerRole -and -not $roles)
        {
            $msg = "Server role ""$($ServerRole | Get-BMObjectName)"" does not exist."
            Write-Error -Message $msg -ErrorAction $ErrorActionPreference
        }
    }
}


function Get-BMVariable
{
    <#
    .SYNOPSIS
    Gets BuildMaster variables.
 
    .DESCRIPTION
    The `Get-BMVariable` function gets BuildMaster variables. By default, it gets all global variables. It can also get
    all variables for a specific environment, server, server role, application group, and application variables.
 
    To get a specific variable, pass the variable's id, name, or object `Variable` parameter. If the variable doesn't
    exist, the function writes an error. To search for a variable, pass a wildcard string to the `Variable` parameter.
 
    To get an environment's variables, pass the environment's id, name, or object to the `Environment` parameter.
 
    To get a server role's variables, pass the server role's name to the `ServerRole` parameter.
 
    To get a server's variables, pass the server's name to the `Server` parameter.
 
    To get an application group's variables, pass the application group's name to the `ApplicationGroup` parameter.
 
    To get an application's variables, pass the application's name to the `Application` parameter.
 
    To get an OtterScript vector or map as a string, use the `Raw` switch.
 
    This function uses BuildMaster's [Variables Management](https://docs.inedo.com/docs/buildmaster-reference-api-variables)
    API. Due to a bug in BuildMaster, when getting application or application group variables, it uses BuildMaster's
    native API.
 
    .EXAMPLE
    Get-BMVariable
 
    Demonstrates how to get all global variables.
 
    .EXAMPLE
    Get-BMVariable -Session $session -Name 'Var'
 
    Demonstrates how to get a specific global variable.
 
    .EXAMPLE
    Get-BMVariable -Session $session -Environment 'Dev'
 
    Demonstrates how to all an environment's variables.
 
    .EXAMPLE
    Get-BMVariable -Session $session -Name 'Var' -Environment 'Dev'
 
    Demonstrates how to get a specific variable in an environment.
 
    .EXAMPLE
    Get-BMVariable -Session $session -ServerRole 'WebApp'
 
    Demonstrates how to get all variables in a specific server role.
 
    .EXAMPLE
    Get-BMVariable -Session $session -Name 'Var' -ServerRole 'WebApp'
 
    Demonstrates how to get a specific variable in a server role.
 
    .EXAMPLE
    Get-BMVariable -Session $session -Server 'example.com'
 
    Demonstrates how to get all variables for a specific server.
 
    .EXAMPLE
    Get-BMVariable -Session $session -Name 'Var' -Server 'example.com'
 
    Demonstrates how to get a specific variable in a server.
 
    .EXAMPLE
    Get-BMVariable -Session $session -ApplicationGroup 'WebApps'
 
    Demonstrates how to get all variables from a specific application group.
 
    .EXAMPLE
    Get-BMVariable -Session $session -Name 'Var' -ApplicationGroup 'WebApps'
 
    Demonstrates how to get a specific variable from an application group.
 
    .EXAMPLE
    Get-BMVariable -Session $session -Application 'www'
 
    Demonstrates how to get all variables from a specific application.
 
    .EXAMPLE
    Get-BMVariable -Session $session -Name 'Var' -Application 'www'
 
    Demonstrates how to get a specific variable from an application.
 
    .EXAMPLE
    Get-BMVariable -Session $session -Name 'Var' -Application 'www' -Raw
 
    Demonstrates how to get a specific variable from an application as a string.
    #>

    [CmdletBinding(DefaultParameterSetName='global')]
    param(
        # The session to BuildMaster. Use `New-BMSession` to create a session.
        [Parameter(Mandatory)]
        [object] $Session,

        # The variable to get. Pass a variable id, name, or object. If you pass a string, wildcards are supported, and
        # only variables whose name equal or match the string will be returned.
        [Parameter(ValueFromPipeline)]
        [Object] $Name,

        # The application of the variable. Pass an application id, name, or object.
        [Parameter(Mandatory, ParameterSetName='application')]
        [Alias('ApplicationName')]
        [Object] $Application,

        # The application group of the variable. Pass an application group id, name, or object.
        [Parameter(Mandatory, ParameterSetName='application-group')]
        [Alias('ApplicationGroupName')]
        [Object] $ApplicationGroup,

        # The environment of the variable. Pass an environment id, name, or object.
        [Parameter(Mandatory, ParameterSetName='environment')]
        [Alias('EnvironmentName')]
        [Object] $Environment,

        # The server of the variable. Pass an server id, name, or object.
        [Parameter(Mandatory, ParameterSetName='server')]
        [Alias('ServerName')]
        [Object] $Server,

        # The server role of the variable. Pass an server role id, name, or object.
        [Parameter(Mandatory, ParameterSetName='role')]
        [Alias('ServerRoleName')]
        [Object] $ServerRole,

        # Return the variable's value, not an object representing the variable.
        [switch] $ValueOnly,

        # Return the variable's value as a string rather than converting to a PowerShell object.
        [switch] $Raw
    )

    process
    {
        Set-StrictMode -Version 'Latest'
        Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
        $WhatIfPreference = $false  # This function does not modify any data, but uses POST requests.

        $variableArg = @{}
        if ($Name)
        {
            $variableArg['Variable'] = $Name
        }

        Invoke-BMVariableEndpoint -Session $session `
                                  @variableArg `
                                  -EntityTypeName $PSCmdlet.ParameterSetName `
                                  -BoundParameter $PSBoundParameters |
            ForEach-Object {
                if ($ValueOnly -and $Raw)
                {
                    return $_.Value
                }

                if ($ValueOnly)
                {
                    return ConvertFrom-BMOtterScriptExpression $_.Value
                }

                if ($Raw)
                {
                    return $_
                }

                $_.Value = ConvertFrom-BMOtterScriptExpression $_.Value
                return $_
            } |
            Write-Output

    }
}


function Invoke-BMNativeApiMethod
{
    <#
    .SYNOPSIS
    Calls a method on BuildMaster's "native" API.
 
    .DESCRIPTION
    The `Invoke-BMNativeApiMethod` calls a method on BuildMaster's "native" API. From Inedo:
 
    > This API endpoint should be avoided if there is an alternate API endpoint available, as those are much easier to use and will likely not change.
 
    In other words, use a native API at your own peril.
 
    When using the `WhatIf` parameter, only web requests that use the `Get` HTTP method are made.
 
    .EXAMPLE
    Invoke-BMNativeApiMethod -Session $session -Name 'Applications_CreateApplication' -Parameter @{ Application_Name = 'fubar' }
 
    Demonstrates how to call `Invoke-BMNativeApiMethod`. In this example, it is calling the `Applications_CreateApplication` method to create a new application named `fubar`.
    #>

    [CmdletBinding(SupportsShouldProcess=$true)]
    param(
        [Parameter(Mandatory=$true)]
        [object]
        # A session object that represents the BuildMaster instance to use. Use the `New-BMSession` function to create session objects.
        $Session,

        [Parameter(Mandatory=$true)]
        [string]
        # The name of the API method to use. The list can be found at http://inedo.com/support/documentation/buildmaster/reference/api/native, or under your local BuildMaster instance at /reference/api
        $Name,

        [Microsoft.PowerShell.Commands.WebRequestMethod]
        # The HTTP/web method to use. The default is `GET`.
        $Method = [Microsoft.PowerShell.Commands.WebRequestMethod]::Get,

        [hashtable]
        # Any parameters to pass to the endpoint. The keys/values are sent in the body of the request as a JSON object.
        $Parameter
    )

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

    $parameterParam = @{ }
    if( $Parameter -and $Parameter.Count )
    {
        $parameterParam['Parameter'] = $Parameter
        $parameterParam['AsJson'] = $true
    }

    Invoke-BMRestMethod -Session $Session -Name ('json/{0}' -f $Name) -Method $Method @parameterParam
}


function Invoke-BMRestMethod
{
    <#
    .SYNOPSIS
    Invokes a BuildMaster REST method.
 
    .DESCRIPTION
    The `Invoke-BMRestMethod` invokes a BuildMaster REST API method. You pass the path to the endpoint (everything after
    `/api/`) via the `Name` parameter, the HTTP method to use via the `Method` parameter, and the parameters to pass in
    the body of the request via the `Parameter` parameter. This function converts the `Parameter` hashtable to a
    URL-encoded query string and sends it in the body of the request. You can send the parameters as JSON by adding the
    `AsJson` parameter. You can pass your own custom body to the `Body` parameter. If you do, make sure you set an
    appropriate content type for the request with the `ContentType` parameter.
 
    You also need to pass an object that represents the BuildMaster instance and API key to use when connecting via the
    `Session` parameter. Use the `New-BMSession` function to create a session object.
 
    When using the `WhatIf` parameter, only web requests that use the `Get` HTTP method are made.
    #>

    [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName='NoBody')]
    param(
        # A session object to BuildMaster. Use the `New-BMSession` function to create a session.
        [Parameter(Mandatory)]
        [Object] $Session,

        # The name of the API to use. The should be everything after `/api/` in the method's URI.
        [Parameter(Mandatory)]
        [String] $Name,

        # The HTTP/web method to use. The default is `GET`.
        [Microsoft.PowerShell.Commands.WebRequestMethod] $Method =
            [Microsoft.PowerShell.Commands.WebRequestMethod]::Get,

        # The parameters to pass to the method's endpoint. They are sent in the request body as URL-encoded, name/value
        # pairs, e.g. `name1=value1&name2=value2`. To send them as a JSON object, use the `AsJson` switch.
        [Parameter(Mandatory, ParameterSetName='BodyFromHashtable')]
        [hashtable] $Parameter,

        # Send the request body as JSON. Otherwise, the data is sent as name/value pairs.
        [Parameter(ParameterSetName='BodyFromHashtable')]
        [switch] $AsJson,

        # The body to send.
        [Parameter(Mandatory, ParameterSetName='CustomBody')]
        [String] $Body,

        # The content type of the web request.
        #
        # By default,
        #
        # * if passing a value to the `Parameter` parameter, the content type is set to
        # `application/x-www-form-urlencoded`
        # * if passing a value to the `Parameter` parameter and you're using the `AsJson` switch, the content type is
        # set to `application/json`.
        #
        # Otherwise, the content type is not set. If you're passing your own body to the `Body` parameter, you may have
        # to set the appropriate content type for BuildMaster to respond.
        [String] $ContentType
    )

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

    $uri = '{0}api/{1}' -f $Session.Url,$Name

    $debugBody = ''
    $webRequestParam = @{ }
    if ($Body)
    {
        $webRequestParam['Body'] = $debugBody = $Body
    }
    elseif( $Parameter )
    {
        if( $AsJson )
        {
            $Body = $Parameter | ConvertTo-Json -Depth 100
            $debugBody = $Body -replace '("API_Key": +")[^"]+','$1********'
            $encryptionKeyRegex = '"encryptionKey":( +)"([^"]+)"'
            if( $debugBody -match $encryptionKeyRegex )
            {
                $maskLength = $Matches[2].Length
                $mask = '*' * $maskLength
                $debugBody = $debugBody -replace $encryptionKeyRegex,('"encryptionKey":$1"{0}"' -f $mask)
            }
            if( -not $ContentType )
            {
                $ContentType = 'application/json; charset=utf-8'
            }
        }
        else
        {
            $bodyBuilder = [Text.StringBuilder]::New()
            $valueToMask = ''
            foreach ($paramName in $Parameter.Keys)
            {
                $paramValue = $Parameter[$paramName]
                if ($bodyBuilder.Length -gt 0)
                {
                    [void]$bodyBuilder.Append('&')
                }
                [void]$bodyBuilder.Append([Web.HttpUtility]::UrlEncode($paramName))
                [void]$bodyBuilder.Append('=')
                [void]$bodyBuilder.Append([Web.HttpUtility]::UrlEncode($paramValue))

                if ($paramName -eq 'API_Key')
                {
                    $valueToMask = $paramValue
                }
            }
            $Body = $bodyBuilder.ToString()
            $debugBody = $Body
            if ($valueToMask)
            {
                $debugBody = $debugBody -replace [regex]::Escape($valueToMask), '********'

            }

            if( -not $ContentType )
            {
                $ContentType = 'application/x-www-form-urlencoded; charset=utf-8'
            }
        }
        $webRequestParam['Body'] = $Body
    }

    if( $ContentType )
    {
        $webRequestParam['ContentType'] = $ContentType
    }

    $headers = @{
                    'X-ApiKey' = $Session.ApiKey;
                }

    # $DebugPreference = 'Continue'
    Write-Debug -Message ('{0} {1}' -f $Method.ToString().ToUpperInvariant(),($uri -replace '\b(API_Key=)([^&]+)','$1********'))
    if( $ContentType )
    {
        Write-Debug -Message ('Content-Type: {0}' -f $ContentType)
    }
    foreach( $headerName in $headers.Keys )
    {
        $value = $headers[$headerName]
        if( $headerName -eq 'X-ApiKey' )
        {
            $value = '*' * 8
        }

        Write-Debug -Message ('{0}: {1}' -f $headerName,$value)
    }

    if ($debugBody)
    {
        ($debugBody -split ([regex]::Escape([Environment]::NewLine))) | Write-Debug
    }

    try
    {
        if( $Method -eq [Microsoft.PowerShell.Commands.WebRequestMethod]::Get -or $PSCmdlet.ShouldProcess($Uri,$Method) )
        {
            Invoke-RestMethod -Method $Method -Uri $uri @webRequestParam -Headers $headers |
                ForEach-Object { $_ } |
                Where-Object { $_ }
        }
    }
    catch
    {
        $Global:Error.RemoveAt(0)
        Write-Error -ErrorRecord $_ -ErrorAction $ErrorActionPreference
    }
}


function Invoke-BMVariableEndpoint
{
    [CmdletBinding(DefaultParameterSetName='Get')]
    param(
        [Parameter(Mandatory)]
        [Object] $Session,

        [Parameter(Mandatory, ParameterSetName='Delete')]
        [Parameter(ParameterSetName='Get')]
        [Parameter(Mandatory, ParameterSetName='Set')]
        [Object] $Variable,

        [Parameter(Mandatory, ParameterSetName='Set')]
        [String] $Value,

        [Parameter(Mandatory)]
        [ValidateSet('application', 'application-group', 'environment', 'global', 'server', 'role')]
        [String] $EntityTypeName,

        [Parameter(Mandatory)]
        [hashtable] $BoundParameter,

        [Parameter(Mandatory, ParameterSetName='Delete')]
        [switch] $ForDelete
    )

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

    $variableName = ''
    $getting = $PSCmdlet.ParameterSetName -eq 'Get'
    $deleting = $ForDelete.IsPresent
    $updating = $PSCmdlet.ParameterSetName -eq 'Set'
    if ($Variable)
    {
        $variableName = $Variable | Get-BMObjectName -Strict
        if (-not $variableName -and ($deleting -or $updating))
        {
            return
        }
    }

    $searching = $getting -and $variableName -and [wildcardpattern]::ContainsWildcardCharacters($variableName)

    $variablePathSegment = ''
    if ($variableName -and -not $searching)
    {
        $variablePathSegment = "/$([Uri]::EscapeDataString($variableName))"
    }

    $entityPathSegment = "global$($variablePathSegment)"

    $entityName = ''
    $bmEntity = $null
    $entityDesc = ''

    if ($EntityTypeName -ne 'global')
    {
        $entityTypeDescriptions = @{
            'application' = 'application';
            'application-group' = 'application group';
            'environment' = 'environment';
            'server' = 'server';
            'role' = 'server role'
        }
        $entityDesc = $entityTypeDescriptions[$EntityTypeName]
        $entityDescCapitalized = [char]::ToUpperInvariant($entityDesc[0]) + $entityDesc.Substring(1)

        $entityToParamNameMap = @{
            'application' = 'Application';
            'application-group' = 'ApplicationGroup';
            'environment' = 'Environment';
            'server' = 'Server';
            'role' = 'ServerRole';
        }

        # What parameter has the variable's entity?
        $paramName = $entityToParamNameMap[$EntityTypeName]

        # Get the entity.
        $entity = $BoundParameter[$paramName]

        $getEntityArg = @{
            $paramName = $entity;
        }
        # Check if the entity exists in BuildMaster.
        $bmEntity = & "Get-BM$($paramName)" -Session $Session @getEntityArg -ErrorAction Ignore
        if (-not $bmEntity)
        {
            $entityName = $entity | Get-BMObjectName
            $msg = "$($entityDescCapitalized) ""$($entityName)"" does not exist."
            if ($deleting)
            {
                $msg = "Unable to delete variable ""$($variableName)"" because the $($entityDesc) " +
                       """$($entityName)"" does not exist."
            }
            elseif ($updating)
            {
                $msg = "Unable to set variable ""$($variableName)"" because the $($entityDesc) ""$($entityName)"" " +
                       'does not exist.'
            }
            Write-Error -Message $msg -ErrorAction $ErrorActionPreference
            return
        }

        # Get the entity's name.
        $entityName = $bmEntity | Get-BMOBjectName -Strict -ObjectTypeName $paramName

        # Create the entity-specific endpoint path.
        $entityPathSegment = "$($entityTypeName)/$([Uri]::EscapeDataString($entityName))$($variablePathSegment)"
    }

    $endpointPath = "variables/$($entityPathSegment)"

    $variables = @{}

    $nativeApiEntityIdParam = @{}
    [Object[]] $nativeVariables = @()
    $useNativeApi = $EntityTypeName -in @('application', 'application-group')
    if ($EntityTypeName -eq 'application')
    {
        $nativeApiEntityIdParam['Application_Id'] = $bmEntity.Application_Id
    }
    elseif ($EntityTypeName -eq 'application-group')
    {
        $nativeApiEntityIdParam['ApplicationGroup_Id'] = $bmEntity.ApplicationGroup_Id
    }

    if (-not $updating)
    {
        if ($useNativeApi)
        {
            $nativeVariables = Invoke-BMNativeApiMethod -Session $Session `
                                                        -Name 'Variables_GetVariablesForScope' `
                                                        -Method Post `
                                                        -Parameter $nativeApiEntityIdParam

            foreach ($nativeVar in $nativeVariables)
            {
                $bytes = [Convert]::FromBase64String($nativeVar.Variable_Value)
                $variables[$nativeVar.Variable_Name] = [Text.Encoding]::UTF8.GetString($bytes)
            }
            $variables = [pscustomobject]$variables
        }
        else
        {
            $variables = Invoke-BMRestMethod -Session $session -Name $endpointPath
        }
    }

    if ($Variable -and -not $searching -and -not $variables -and -not $updating)
    {
        $msg = "Variable ""$($variableName)"" does not exist."
        if ($bmEntity)
        {
            $msg = "$($entityDescCapitalized) ""$($entityName)"" variable ""$($variableName)"" does not exist."
        }

        if ($ForDelete)
        {
            $msg = "Unable to delete variable ""$($variableName)"" because it does not exist."
            if ($bmEntity)
            {
                $msg = "Unable to delete $($entityDesc) ""$($entityName)"" variable ""$($variableName)"" because the " +
                       "variable does not exist."
            }
        }

        Write-Error -Message $msg -ErrorAction $ErrorActionPreference
        return
    }

    if ($deleting)
    {
        if ($useNativeApi)
        {
            $nativeVar = $nativeVariables | Where-Object 'Variable_Name' -EQ $variableName
            Invoke-BMNativeApiMethod -Session $session `
                                     -Name 'Variables_DeleteVariable' `
                                     -Method Post `
                                     -Parameter @{ Variable_Id = $nativeVar.Variable_Id }
        }
        Invoke-BMRestMethod -Session $session -Name $endpointPath -Method Delete
        return
    }

    if ($updating)
    {
        Invoke-BMRestMethod -Session $session -Name $endpointPath -Body $Value -Method Post
        return
    }

    if ($variables -is [String])
    {
        return [pscustomobject]@{
            Name = $variableName;
            Value = $variables;
        }
    }

    $variables |
        Get-Member -MemberType NoteProperty |
        ForEach-Object {
            return [pscustomobject]@{
                'Name' = $_.Name;
                'Value' = $variables.($_.Name);
            }
        } |
        Where-Object {
            if ($variableName)
            {
                return $_.Name -like $variableName
            }
            return $true
        } |
        Write-Output
}


function New-BMApplication
{
    <#
    .SYNOPSIS
    Creates an application in BuildMaster.
 
    .DESCRIPTION
    The `New-BMApplication` function creates an application in BuildMaster. This function uses the native BuildMaster
    API. Only a name is required to create an application. The name must be unique and not in use.
 
    These parameters are also available:
 
    * `ReleaseNumberScheme`: sets the release number scheme to use when you create a new release for the application
    Options are `MajorMinorRevision`, `MajorMinor`, or `DateBased`.
    * `BuildNumberScheme`: sets the build number scheme to use when creating new builds for an application.
    Options are `Unique`, `Sequential`, `DateBased`.
    * `Raft` to set the raft in which the application's scripts, pipelines, etc. should be saved.
 
    .EXAMPLE
    New-BMApplication -Session $session -Name 'MyNewApplication'
 
    Demonstrates the simplest way to create an application. In this example, a `MyNewApplication` application will be
    created and all its fields set to BuildMaster's default values.
 
    .EXAMPLE
    New-BMApplication -Session $session -Name 'MyNewApplication' -ReleaseNumberSchemeName MajorMinor -BuildNumberSchemeName Sequential
 
    This example demonstrates all the fields you can set when creating a new application. In this example, the new
    application will be called `MyNewApplication`, its release number scheme will be `MajorMinor`, and its build number
    schema will be `Sequential`.
    #>

    [CmdletBinding()]
    param(
        # A session object that represents the BuildMaster instance to use. Use the `New-BMSession` function to create
        # session objects.
        [Parameter(Mandatory)]
        [Object] $Session,

        # The name of the application.
        [Parameter(Mandatory)]
        [String] $Name,

        # The name of the release number scheme. Should be one of:
        #
        # * `MajorMinorRevision`
        # * `MajorMinor`
        # * `DateBased`
        [ValidateSet('MajorMinorRevision', 'MajorMinor', 'DateBased')]
        [String] $ReleaseNumberSchemeName,

        # The name of the build number scheme. Should be one of:
        #
        # * `Unique`
        # * `Sequential`
        # * `DateTimeBased`
        [ValidateSet('Unique', 'Sequential', 'DateTimeBased')]
        [String] $BuildNumberSchemeName,

        # The application group to assign. By default, the application will be ungrouped. Pass an application group id
        # or object.
        [Alias('ApplicationGroupID')]
        [Object] $ApplicationGroup,

        # The raft where the application's raft items will be stored. Pass a raft id, name, or raft object.
        [Object] $Raft
    )

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

    $application = $Name | Get-BMApplication -Session $Session -ErrorAction Ignore
    if ($application)
    {
        Write-Error -Message "Application ""$($Name)"" already exists." -ErrorAction $ErrorActionPreference
        return
    }

    # We use the value in $PSBoundParameters because it is $null if not provided by the user. PowerShell sets a
    # not-provided [String] argument value to empty string. We need $null so `Add-BMParameter` knows the parameter was
    # not provided and won't add it to the parameter hashtable.
    $parameters =
        @{} |
        Add-BMParameter -Name 'Application_Name' -Value $Name -PassThru |
        Add-BMObjectParameter -Name 'ApplicationGroup' -Value $ApplicationGroup -ForNativeApi -PassThru |
        Add-BMParameter -Name 'ReleaseNumber_Scheme_Name' `
                        -Value $PSBoundParameters['ReleaseNumberSchemeName'] `
                        -PassThru |
        Add-BMParameter -Name 'BuildNumber_Scheme_Name' -Value $PSBoundParameters['BuildNumberSchemeName'] -PassThru

    $appID = Invoke-BMNativeApiMethod -Session $Session `
                                      -Name 'Applications_CreateApplication' `
                                      -Parameter $parameters `
                                      -Method Post
    if( -not $appID )
    {
        return
    }

    if ($Raft)
    {
        $editArgs =
            @{} |
            Add-BMParameter -Name 'Application_Id' -Value $appID -PassThru |
            Add-BMParameter -Name 'Application_Name' -Value $Name -PassThru |
            Add-BMObjectParameter -Name 'Raft' -Value $Raft -AsName -ForNativeApi -PassThru
        Invoke-BMNativeApiMethod -Session $Session `
                                 -Name 'Applications_EditApplication' `
                                 -Parameter $editArgs `
                                 -Method Post
    }

    Invoke-BMNativeApiMethod -Session $Session `
                             -Name 'Applications_GetApplication' `
                             -Parameter @{ 'Application_Id' = $appID } `
                             -Method Post |
        Select-Object -ExpandProperty 'Applications_Extended'
}


function New-BMBuild
{
    <#
    .SYNOPSIS
    Creates a new build for a release.
 
    .DESCRIPTION
    The `New-BMBuild` creates a new version/build of an application. In order to deploy an application, the application
    must have a release. Then you create builds in that release, and each build is then deployed using the release's
    pipeline.
 
    .EXAMPLE
    New-BMBuild -Session $session -Release $release
 
    Demonstrates how to create a new build in the `$release` release. BuildMaster detects what application based on the
    release (since releases are always tied to applications). Verion numbers and build numbers are incremented and
    handled based on the release settings.
 
    The `$release` parameter can be:
 
    * A release object with an `id` property.
    * A release ID integer.
 
    .EXAMPLE
    New-BMBuild -Session $session -ReleaseName '53' -Application $applicatoin
 
    Demonstrates how to create a new build by using the release's name. Since release names are only unique within an
    application, you must also specify the application via the `Application` parameter.
 
    .EXAMPLE
    New-BMBuild -Session $session -Release $release -PacakgeName '56.develop' -Variable @{ ProGetPackageName = '17.1.54+developer.deadbee' }
 
    Demonstrates how to create a release with a specific name, `56.develop`, and with a build-level variable,
    `ProGetPackageName`.
    #>

    [CmdletBinding()]
    param(
        # An object that represents the instance of BuildMaster to connect to. Use the `New-BMSession` function to
        # creates a session object.
        [Parameter(Mandatory)]
        [Object] $Session,

        # The release where the build should be created. Can be:
        #
        # * a release object with an `id` property
        # * the release ID as an integer
        [Parameter(Mandatory, ParameterSetName='ByReleaseID')]
        [Object] $Release,

        # The release number where the build should be created. Release numbers are unique within an application and
        # can be duplicated between applications. If you use this parameter to identify the release, you must also
        # provide a value for the `Application` parameter.
        [Parameter(Mandatory, ParameterSetName='ByReleaseNumber')]
        [String] $ReleaseNumber,

        # The application where the release identified by the `ReleaseNumber` parameter can be found. Can be:
        #
        # * An application object with a `Application_Id`, `id`, `Application_Name`, or `name` properties.
        # * The application ID as an integer.
        # * The application name as a string.
        [Parameter(Mandatory, ParameterSetName='ByReleaseNumber')]
        [Object] $Application,

        # The build number/name. If not provided, BuildMaster generates one based on the release settings.
        [string] $BuildNumber,

        # Any build variables to set. Build variables are unique to each build.
        [hashtable] $Variable
    )

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

    $parameters = @{ }

    if( $PSCmdlet.ParameterSetName -eq 'ByReleaseID' )
    {
        $parameters | Add-BMObjectParameter -Name 'release' -Value $Release
    }
    else
    {
        $parameters['releaseNumber'] = $ReleaseNumber
        $parameters | Add-BMObjectParameter -Name 'application' -Value $Application
    }

    if( $BuildNumber )
    {
        $parameters['buildNumber'] = $BuildNumber
    }

    if( $Variable )
    {
        foreach( $key in $Variable.Keys )
        {
            $parameters[('${0}' -f $key)] = $Variable[$key]
        }
    }

    Invoke-BMRestMethod -Session $Session -Name 'releases/builds/create' -Parameter $parameters -Method Post
}


function New-BMEnvironment
{
    <#
    .SYNOPSIS
    Creates a new environment in a BuildMaster instance.
 
    .DESCRIPTION
    The `New-BMEnvironment` creates a new environment in BuildMaster. Pass the name of the environment to the `Name`
    parameter. Names may only contain letters, numbers, periods, underscores, or dashes and may not end with an
    underscore or dash. Every environment must have a unique name. If you create a environment with a duplicate name,
    the BuildMaster error returns an error.
 
    You can set an new environment's parent environment with the `ParentName` parameter. You can create an inactive
    environment by using the `Inactive` switch.
 
    To return the environment, even if it already exists, use the `PassThru` switch.
 
    This function uses BuildMaster's infrastructure management API.
 
    .EXAMPLE
    New-BMEnvironment -Session $session -Name 'DevNew'
 
    Demonstrates how to create a new environment.
 
    .EXAMPLE
    New-BMEnvironment -Session $session -Name 'DevNew' -ErrorAction Ignore -PassThru
 
    Demonstrates how to ignore if an environment already exists and to return an enviornment object representing the
    new or already existing environment.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessage('PSShouldProcess', '')]
    [CmdletBinding(SupportsShouldProcess)]
    param(
        # The session to BuildMaster. Use `New-BMSession` to create a session.
        [Parameter(Mandatory)]
        [Object] $Session,

        # The name of the environment to create. Must contain only letters, numbers, underscores, or dashes. Must begin
        # with a letter. Must not end with an underscore or dash. Must be between 1 and 50 characters long.
        [Parameter(Mandatory)]
        [ValidatePattern('^[A-Za-z][A-Za-z0-9_-]*(?<![_-])$')]
        [ValidateLength(1,50)]
        [String] $Name,

        # The name of this environment's parent environemnt.
        [String] $ParentName,

        # By default, new environments are active. If you want the environment to be inactive, use this switch.
        [switch] $Inactive,

        [switch] $PassThru
    )

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

    $parameter = @{
                    name = $Name;
                    parentName = $ParentName;
                    active = (-not $Inactive);
                 }
    $encodedName = [Uri]::EscapeDataString($Name)
    Invoke-BMRestMethod -Session $Session `
                        -Name ('infrastructure/environments/create/{0}' -f $encodedName) `
                        -Method Post `
                        -Parameter $parameter `
                        -AsJson
    if ($PassThru)
    {
        return Get-BMEnvironment -Session $Session -Environment ([pscustomobject]@{ 'Name' = $Name})
    }
}


function New-BMPackage
{
    <#
    .SYNOPSIS
    Obsolete. Use "New-BMBuild" instead.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [Object] $Session,

        [Parameter(Mandatory, ParameterSetName='ByReleaseID')]
        [Object] $Release,

        [Parameter(Mandatory, ParameterSetName='ByReleaseNumber')]
        [String] $ReleaseNumber,

        [Parameter(Mandatory, ParameterSetName='ByReleaseNumber')]
        [Object] $Application,

        [string] $PackageNumber,

        [hashtable] $Variable
    )

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

    $msg = 'The BuildMasterAutomation module''s "New-BMPackage" function is obsolete and will be removed in a future ' +
           'version of BuildMasterAutomation. Use the "Get-BMBuild" function instead.'
    Write-WarningOnce $msg

    $newArgs = @{}
    foreach ($paramName in @('Release', 'ReleaseNumber', 'Application', 'PackageNumber', 'Variable'))
    {
        if (-not $PSBoundParameters.ContainsKey($paramName))
        {
            continue
        }

        $newParamName = $paramName
        if ($paramName -eq 'PackageNumber')
        {
            $newParamName = 'BuildNumber'
        }

        $newArgs[$newParamName] = $PSBoundParameters[$paramName]
    }

    New-BMBuild -Session $Session @newArgs
}


function New-BMPipelinePostDeploymentOptionsObject
{
    <#
    .SYNOPSIS
    Creates an object to pass to `Set-BMPipeline` to set a pipeline's post-deployment options.
 
    .DESCRIPTION
    The `New-BMPipelinePostDeploymentOptionsObject` creates an object representing the post-deployment options for a
    pipeline. The object returned should be passed to the `Set-BMPipeline` function's `-PostDeploymentOption` parameter.
 
    If you don't pass any arguments, the post-deployment options object will have no properties. We don't know what the
    behavior of `Set-BMPipeline` will be in the case. We assume it will remove explicitly set values, reverting them to
    their defaults.
 
    .EXAMPLE
    New-BMPipelinePostDeploymentOptionsObject -CancelEarlierRelease $true -CreateNewRelease $true -DeployRelease $false
 
    Demonstrates how to create a post-deployment options object that sets the options to the opposite of BuildMaster's
    defaults.
 
    .EXAMPLE
    New-BMPipelinePostDeploymentOptionsObject -MarkDeployed $false
 
    Demonstrates how to turn off a single option.
    #>

    [CmdletBinding()]
    param(
        [bool] $CancelEarlierReleases,

        [bool] $CreateNewRelease,

        [bool] $MarkDeployed
    )

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

    $options = @{ }

    if ($PSBoundParameters.ContainsKey('CancelEarlierReleases'))
    {
        $options['CancelReleases'] = $CancelEarlierReleases
    }

    if ($PSBoundParameters.ContainsKey('CreateNewRelease'))
    {
        $options['CreateRelease'] = $CreateNewRelease
    }

    if ($PSBoundParameters.ContainsKey('MarkDeployed'))
    {
        $options['DeployRelease'] = $MarkDeployed
    }

    return [pscustomobject]$options
}


function New-BMPipelineStageObject
{
    <#
    .SYNOPSIS
    Creates a pipeline stage object that can be passed to `Set-BMPipeline`.
 
    .DESCRIPTION
    The `New-BMPipelineStageObject` creates an object that represents a stage of a pipeline. The object returned can be
    passed to the `Set-BMPipeline` function's `Stage` parameter. Pass the name of the stage to the `Name` parameter, the
    description to the `Description` parameter, and the stage targets to the `Target` parameter. Target objects can be
    created with the `New-BMPipelineStageTargetObject` function.
 
    .EXAMPLE
    New-BMPipelineStageObject -Name 'Example'
 
    Demonstrates how to create a stage object with just a name.
    #>

    [CmdletBinding()]
    param(
        # The stage's name.
        [Parameter(Mandatory)]
        [String] $Name,

        # The stage's description.
        [String] $Description,

        # A list of target objects for the stage. Target objects can be created with the
        # `New-BMPipelineStageTargetObject` function.
        [Parameter(ValueFromPipeline)]
        [Object[]] $Target
    )

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

        $stage = [pscustomobject]@{
            Name = $Name;
            Description = $Description;
            Targets = $null;
        }

        $targets = [Collections.ArrayList]::New()
    }

    process
    {
        foreach( $item in $Target )
        {
            [void]$targets.Add($item)
        }
    }

    end
    {
        $stage.Targets = $targets.ToArray()
        return $stage
    }
}


function New-BMPipelineStageTargetObject
{
    <#
    .SYNOPSIS
    Creates a stage target object that can be passed to the `New-BMPipelineStageObject` function.
 
    .DESCRIPTION
    The `New-BMPipelineStageTargetObject` function creates a pipeline stage target object. Pass the plan name to execute
    to the `PlanName` parameter. By default, creates an object that targets no servers.
 
    To target servers in a specific environment, pass the environment's name to the `EnvironmentName` parameter.
 
    To target all servers in an environment, use the `AllServers` switch. You must also pass an environment name.
 
    To target servers in specific roles, pass the role name(s) to the `ServerRoleName`.
 
    To target servers in specific server pools, pass the server pool names(s) to the `ServerPoolName` parameter.
 
    To target specific servers, pass the server name(s) to the `ServerName` parameter.
 
    .EXAMPLE
    New-BMPipelineStageTargetObject -PlanName 'Deploy'
 
    Demonstrates how to create a target object that executes a plan against no servers.
 
    .EXAMPLE
    New-BMPipelineStageTargetObject -PlanName 'Deploy' -EnvironmentName 'Integration'
 
    Demonstrates how to create a target object that executes a plan against servers in a specific environment. In this
    case, the `Integration` environment.
 
    .EXAMPLE
    New-BMPipelineStageTargetObject -PlanName 'Deploy' -EnvironmentName 'Integration' -ServerRoleName 'Build'
 
    Demonstrates how to create a target object that executes a plan against servers with a specific role. In this case,
    all servers with the `Build` role in the `Integration` environment will be targeted.
 
    .EXAMPLE
    New-BMPipelineStageTargetObject -PlanName 'Deploy' -EnvironmentName 'Integration' -ServerPoolName 'Build'
 
    Demonstrates how to create a target object that executes a plan against servers in a specific pool. In this case,
    all servers in the `Build` pool in the `Integration` environment will be targeted.
 
    .EXAMPLE
    New-BMPipelineStageTargetObject -PlanName 'Deploy' -EnvironmentName 'Integration' -AllServers
 
    Demonstrates how to create a target object that executes a plan against all servers in a specific environment. In
    this case, all servers in the `Integration` environment will be targeted.
 
    .EXAMPLE
    New-BMPipelineStageTargetObject -PlanName 'Deploy' -ServerName 'example.com'
 
    Demonstrates how to create a target object that executes a plan against a specific server. In this case, only the
    `example.com` server will be targed.
    #>

    [CmdletBinding(DefaultParameterSetName=0)]
    param(
        [Parameter(Mandatory)]
        [String] $PlanName,

        [Parameter(ParameterSetName=0)]
        [Parameter(ParameterSetName=1)]
        [Parameter(Mandatory, ParameterSetName=2)]
        [Parameter(ParameterSetName=3)]
        [Parameter(ParameterSetName=4)]
        [String] $EnvironmentName,

        [Parameter(Mandatory, ParameterSetName=1)]
        [String[]] $ServerName,

        [Parameter(Mandatory, ParameterSetName=2)]
        [switch] $AllServers,

        [Parameter(Mandatory, ParameterSetName=3)]
        [String[]] $ServerRoleName,

        [Parameter(Mandatory, ParameterSetName=4)]
        [String[]] $ServerPoolName
    )

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

    $target = [pscustomobject]@{
        PlanName = $PlanName;
        EnvironmentName = $EnvironmentName;
        ServerNames = @();
        ServerRoleNames = @();
    }

    if( $PSCmdlet.ParameterSetName -ne 0 )
    {
        if( $PSCmdlet.ParameterSetName -eq 1 )
        {
            $target.ServerNames = $ServerName
        }
        elseif( $PSCmdlet.ParameterSetName -eq 3 )
        {
            $target.ServerRoleNames = $ServerRoleName
        }
        elseif( $PSCmdlet.ParameterSetName -eq 4 )
        {
            $target.ServerRoleNames = $ServerPoolName
        }
        $target | Add-Member -Name 'DefaultServerContext' -Value $PSCmdlet.ParameterSetName -MemberType NoteProperty
    }

    return $target
}


function New-BMRelease
{
    <#
    .SYNOPSIS
    Creates a new release for an application in BuildMaster.
 
    .DESCRIPTION
    The `New-BMRelease` function creates a release for an application in BuildMaster. It uses the BuildMaster
    [Release and Build Deployment API](https://docs.inedo.com/docs/buildmaster-reference-api-release-and-build).
 
    .EXAMPLE
    New-BMRelease -Session $session -Application 'BuildMasterAutomation' -Number '1.0' -Pipeline 'PowerShellModule'
 
    Demonstrates how to create a release using application/pipeline names. In this example, creates a `1.0` release for
    the `BuildMasterAutomation` application using the `PowerShellModule` pipeline.
 
    .EXAMPLE
    New-BMRelease -Session $session -Application 25 -Number '2.0' -Pipeline 'Deploy'
 
    Demonstrates how to create a release using an application id and pipeline name.. In this example, creates a `2.0`
    release for the application whose ID is `25` using the pipeline whose name is `Deploy`.
 
    .EXAMPLE
    New-BMRelease -Session $session -Application $app -Number '3.0' -Pipeline $pipeline
 
    Demonstrates how to create a release using application and pipeline objects. In this example, creates a `3.0`
    release for the application `$app` using the pipeline `$pipeline`.
 
    .EXAMPLE
    New-BMRelease -Session $session -Name 'BMA 1.0' -Application 'BuildMasterAutomation' -Number '1.0' -Pipeline 'PowerShellModule'
 
    Demonstrates how to create a release with a custom name. In this example, the release would be named `BMA 1.0`.
    #>

    [CmdletBinding()]
    param(
        # The session to BuildMaster. Use `New-BMSession` to create a session object.
        [Parameter(Mandatory)]
        [Object] $Session,

        # The application where the release should be created. Pass an application id, name, or object.
        [Parameter(Mandatory, ValueFromPipeline)]
        [Object] $Application,

        # The release number, e.g. 1, 2, 3, 1.0, 2.0, etc.
        [Parameter(Mandatory)]
        [String] $Number,

        # The pipeline the release should use. Pass a pipeline name or object.
        [Object] $Pipeline,

        # The name of the release. By default, BuildMaster uses the release number, i.e. the value of the `Number`
        # parameter.
        [String] $Name
    )

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

        $parameters = @{
                            releaseNumber = $Number;
                            releaseName = $Name;
                       }

        $parameters |
            Add-BMObjectParameter -Name 'application' -Value $Application -PassThru |
            Add-BMObjectParameter -Name 'RaftItem' -Value $Pipeline -PassThru |
            Add-BMObjectParameter -Name 'pipeline' -Value $Pipeline -AsName

        Invoke-BMRestMethod -Session $Session -Name 'releases/create' -Method Post -Parameter $parameters
    }
}



function New-BMServer
{
    <#
    .SYNOPSIS
    Creates a new server in a BuildMaster instance.
 
    .DESCRIPTION
    The `New-BMServer` function creates a new server in BuildMaster. Pass the name of the server to the `Name` parameter. Names may only contain letters, numbers, underscores, or dashes; they must begin with a letter; they must not end with dash or underscore. Pass the server type to the `Type` parameter. Type must be one of 'windows', 'powershell', 'ssh', or 'local'.
 
    Every server must have a unique name. If you create a server with a duplicate name, you'll get an error.
 
    This function uses BuildMaster's infrastructure management API.
 
    Pass a session object representing the instance of BuildMaster to use to the `Session` parameter. Use the `New-BMSession` function to create session objects.
 
    .LINK
    https://inedo.com/support/documentation/buildmaster/reference/api/infrastructure#data-specification
 
    .EXAMPLE
    New-BMServer -Session $session -Name 'example.com' -Windows
 
    Demonstrates how to create a new server that uses the Inedo Agent on Windows that doesn't encrypt the communication between the agent and the server.
 
    .EXAMPLE
    New-BMServer -Session $session -Name 'example.com' -Windows -EncryptionKey 'DEADBEEDEADBEEDEADBEEDEADBEEDEAD'
 
    Demonstrates how to create a new server that uses the Inedo Agent on Windows and uses an AES encryption key to encrypt the communication between the agent and server
 
    .EXAMPLE
    New-BMServer -Session $session -Name 'example.com' -Windows -Ssl -ForceSsl
 
    Demonstrates how to create a new server that uses the Inedo Agent on Windows and uses SSL to protect server to agent communications. As of BuildMaster 6.1.8, you *must* use the `ForceSsl` switch, otherwise SSL won't actually be enabled.
 
    .EXAMPLE
    New-BMServer -Session $session -Name 'example.com' -Ssh
 
    Demonstrates how to create a new server that uses SSH.
 
    .EXAMPLE
    New-BMServer -Session $session -Name 'example.com' -PowerShell
 
    Demonstrates how to create a new server that uses PowerShell Remoting.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessage('PSShouldProcess', '')]
    [Diagnostics.CodeAnalysis.SuppressMessage('PSAvoidUsingPlainTextForPassword', '')]
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory)]
        # An object representing the instance of BuildMaster to connect to. Use `New-BMSession` to create session objects.
        [object]$Session,

        [Parameter(Mandatory)]
        [ValidatePattern('^[A-Za-z][A-Za-z0-9_-]*(?<![_-])$')]
        [ValidateLength(1,50)]
        # The name of the server to create. Must contain only letters, numbers, underscores, or dashes. Must begin with a letter. Must not end with an underscore or dash. Must be between 1 and 50 characters long.
        [string]$Name,

        [Parameter(Mandatory,ParameterSetName='Local')]
        # Create a local server.
        [Switch]$Local,

        [Parameter(Mandatory,ParameterSetName='Windows')]
        [Parameter(Mandatory,ParameterSetName='WindowsAes')]
        [Parameter(Mandatory,ParameterSetName='WindowsSsl')]
        # Create a Windows server.
        [Switch]$Windows,

        [Parameter(Mandatory,ParameterSetName='Ssh')]
        # Create an SSH server.
        [Switch]$Ssh,

        [Parameter(ParameterSetName='Windows')]
        [Parameter(ParameterSetName='WindowsAes')]
        [Parameter(ParameterSetName='WindowsSsl')]
        [Parameter(ParameterSetName='Ssh')]
        # The server's host name. The default is to use the server's name.
        [string]$HostName,

        [Parameter(ParameterSetName='Windows')]
        [Parameter(ParameterSetName='WindowsAes')]
        [Parameter(ParameterSetName='WindowsSsl')]
        [Parameter(ParameterSetName='Ssh')]
        # The port to use. When adding a Windows server, the default is `46336`. When adding an SSH server, the default is `22`.
        [uint16]$Port,

        [Parameter(Mandatory,ParameterSetName='WindowsAes')]
        # The encryption key to use for the server. When passed, also automatically sets the server's encryption type to AES. Only used by Windows agents.
        [securestring]$EncryptionKey,

        [Parameter(Mandatory,ParameterSetName='WindowsSsl')]
        # Use SSL to communicate with the server's agent. Only used by Windows agents.
        [Switch]$Ssl,

        [Parameter(ParameterSetName='WindowsSsl')]
        # The server's agent only uses SSL. Only used by Windows agents.
        [Switch]$ForceSsl,

        [Parameter(Mandatory,ParameterSetName='PowerShell')]
        # Create a PowerShell server.
        [Switch]$PowerShell,

        [Parameter(ParameterSetName='PowerShell')]
        # The PowerShell remoting URL to use.
        [string]$WSManUrl,

        [Parameter(ParameterSetName='Ssh')]
        [Parameter(ParameterSetName='PowerShell')]
        # The name of the credential to use when connecting to the server via SSH or PowerShell Remoting.
        [string]$CredentialName,

        [Parameter(ParameterSetName='Ssh')]
        [Parameter(ParameterSetName='PowerShell')]
        # The temp path directory to use when connecting to the server via SSH or PowerShell Remoting. Default is `/tmp/buildmaster`.
        [string]$TempPath,

        # The environment(s) the server belongs in.
        [string[]]$Environment,

        # The server roles the server is part of.
        [string[]]$Role,

        # Any server-level variables to add to the server.
        [hashtable]$Variable,

        [Switch]
        # If set, creates the server but marks it as inactive.
        $Inactive
    )

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

    $parameter = @{
                    'active' = (-not $InActive.IsPresent);
                 }

    if( -not $HostName )
    {
        $HostName = $Name
    }

    if( -not $TempPath )
    {
        $TempPath = '/tmp/buildmaster'
    }

    $serverType = $null
    if( $Windows )
    {
        if( -not $Port )
        {
            $Port = 46336
        }

        $serverType = 'windows'
        $parameter['hostName'] = $HostName
        $parameter['port'] = $Port

        if( $EncryptionKey )
        {
            $parameter['encryptionKey'] = (New-Object 'pscredential' 'encryptionkey',$EncryptionKey).GetNetworkCredential().Password
            $parameter['encryptionType'] = 'aes'
        }

        if( $Ssl )
        {
            $parameter['encryptionType'] = 'ssl'
            $parameter['requireSsl'] = $ForceSsl.IsPresent
        }
    }
    elseif( $Ssh )
    {
        if( -not $Port )
        {
            $Port = 22
        }

        $serverType = 'ssh'
        $parameter['hostName'] = $HostName
        $parameter['port'] = $Port
    }
    elseif( $PowerShell )
    {
        $serverType = 'powershell'

        if( $WSManUrl )
        {
            $parameter['wsManUrl'] = $WSManUrl
        }
    }
    elseif( $Local )
    {
        $serverType = 'local'
    }
    else
    {
        throw 'Don''t know how you got to this code. Well done!'
    }
    $parameter['serverType'] = $serverType;

    if( $Ssh -or $PowerShell )
    {
        if( $CredentialName )
        {
            $parameter['credentialsName'] = $CredentialName
        }

        if( $TempPath )
        {
            $parameter['tempPath'] = $TempPath
        }
    }

    if( $Environment )
    {
        $parameter['environments'] = $Environment
    }

    if( $Role )
    {
        $parameter['roles'] = $Role
    }

    if( $Variable )
    {
        $parameter['variables'] = $Variable
    }

    $encodedName = [uri]::EscapeDataString($Name)
    Invoke-BMRestMethod -Session $Session -Name ('infrastructure/servers/create/{0}' -f $encodedName) -Method Post -Parameter $parameter -AsJson
}


function New-BMServerRole
{
    <#
    .SYNOPSIS
    Creates a new server role in a BuildMaster instance.
 
    .DESCRIPTION
    The `New-BMServerRole` creates a new server role in BuildMaster. Pass the name of the role to the `Name` parameter. Names may only contain letters, numbers, spaces, periods, underscores, or dashes.
     
    Every role must have a unique name. If you create a role with a duplicate name, you'll get an error.
     
    This function uses BuildMaster's infrastructure management API.
 
    Pass a session object representing the instance of BuildMaster to use to the `Session` parameter. Use the `New-BMSession` function to create session objects.
 
    .EXAMPLE
    New-BMServerRole -Session $session -Name 'My Role'
 
    Demonstrates how to create a new server role.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory)]
        # An object representing the instance of BuildMaster to connect to. Use `New-BMSession` to create session objects.
        [object]$Session,

        [Parameter(Mandatory)]
        [ValidatePattern('^[A-Za-z0-9 ._-]+$')]
        # The name of the server role to create.
        [string]$Name
    )

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

    $encodedName = [uri]::EscapeDataString($Name)
    Invoke-BMRestMethod -Session $Session -Name ('infrastructure/roles/create/{0}' -f $encodedName) -Method Post -Body '{}'
}


function New-BMSession
{
    <#
    .SYNOPSIS
    Creates a session object used to communicate with a BuildMaster instance.
 
    .DESCRIPTION
    The `New-BMSession` function creates and returns a session object that is required by any function in the
    BuildMasterAutomation module that communicates with BuildMaster. The session includes BuildMaster's URL and the
    credentials to use when making requests to BuildMaster's APIs
 
    .EXAMPLE
    $session = New-BMSession -Url 'https://buildmaster.com' -Credential $credential
 
    Demonstrates how to call `New-BMSession`. In this case, the returned session object can be passed to other
    BuildMasterAutomation module functions to communicate with BuildMaster at `https://buildmaster.com` with the
    credential in `$credential`.
    #>

    [CmdletBinding()]
    param(
        # The URI to the BuildMaster instance to use.
        [Parameter(Mandatory)]
        [Alias('Uri')]
        [Uri] $Url,

        # The API key to use when making requests to BuildMaster.
        [Parameter(Mandatory)]
        [String] $ApiKey
    )

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

    return [pscustomobject]@{
            Url = $Url;
            ApiKey = $ApiKey;
        } |
        Add-Member -Name 'Uri' -MemberType AliasProperty -Value 'Url' -PassThru
}


function Publish-BMReleaseBuild
{
    <#
    .SYNOPSIS
    Deploys a release build in BuildMaster.
 
    .DESCRIPTION
    The `Publish-BMReleaseBuild` deploys a release build in BuildMaster. The build is deployed using the pipeline
    assigned to the release the build is part of. This function uses BuildMaster's
    [Release and Build Deployment API](http://inedo.com/support/documentation/buildmaster/reference/api/release-and-build).
 
    Pass the build to deploy to the `Build` parameter. This can be a build object or a build ID (as an integer).
 
    To deploy a build, it must be part of a release that has a pipeline. That pipeline must have at least one stage and
    that stage must have a plan. If none of these conditions are met, you'll get no object back with no errors written.
 
    .EXAMPLE
    Publish-BMReleaseBuild -Session $session -Build $build
 
    Demonstrates how to deploy a build by passing a build object to the `Build` parameter. This object must have an `id`
    or `pipeline_id` property.
 
    .EXAMPLE
    Publish-BMReleaseBuild -Session $session -Build 383
 
    Demonstrates how to deploy a build by passing its ID to the `Build` parameter.
 
    .EXAMPLE
    Publish-BMReleaseBuild -Session $session -Build $build -Stage $stage
 
    Demonstrates how to deploy a build to a specific stage of the release pipeline. By default, a build will deploy to
    the first stage of the assigned pipeline.
    #>

    [CmdletBinding()]
    param(
        # The session to BuildMaster. Use the `New-BMSession` function to create a session.
        [Parameter(Mandatory)]
        [Object] $Session,

        # The build to deploy. Can be a build id or object.
        [Parameter(Mandatory)]
        [Object] $Build,

        # The name of the pipeline stage where the build will be deployed.
        [String] $Stage,

        # Instructs BuildMaster to run the deploy even if the deploy to previous stages failed or the stage isn't the
        # first stage.
        [switch] $Force
    )

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

    $parameters = @{} | Add-BMObjectParameter -Name 'build' -Value $Build -PassThru

    if( $Stage )
    {
        $parameters['toStage'] = $Stage
    }

    if( $Force )
    {
        $parameters['force'] = 'true'
    }

    Invoke-BMRestMethod -Session $Session -Name 'releases/builds/deploy' -Parameter $parameters -Method Post
}



function Publish-BMReleasePackage
{
    <#
    .SYNOPSIS
    Obsolete. Use "Publish-BMReleaseBuild" instead.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [Object] $Session,

        [Parameter(Mandatory)]
        [Object] $Package,

        [String] $Stage,

        [switch] $Force
    )

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

    $msg = 'The BuildMasterAutomation module''s "Publish-BMReleasePackage" function is obsolete and will be removed ' +
           'in a future version of BuildMasterAutomation. Use the "Get-BMBuild" function instead.'
    Write-WarningOnce $msg

    $publishArgs = @{}
    foreach ($paramName in @('Stage', 'Force'))
    {
        if (-not $PSBoundParameters.ContainsKey($paramName))
        {
            continue
        }

        $publishArgs[$newParamName] = $PSBoundParameters[$paramName]
    }

    Publish-BMReleaseBuild -Session $Session -Build $Package @publishArgs
}



function Remove-BMApplication
{
    <#
    .SYNOPSIS
    Deletes applications in BuildMaster.
 
    .DESCRIPTION
    The `Remove-BMApplication` function removes an application from BuildMaster. Pass the application's id, name, or
    object to the `Application` parameter. If the application is disabled, it will be deleted. To delete the application
    even if it's still enabled, use the `Force` (switch).
 
    Use `Disable-BMApplication` to disable an application.
 
    Uses the BuildMaster native API.
 
    .EXAMPLE
    Remove-BMApplication -Session $session -Application $bmApp
 
    Demonstrates how to delete an application by passing an application object to the `Application` parameter.
 
    .EXAMPLE
    Remove-BMApplication -Session $session -Application 432
 
    Demonstrates how to delete an application by passing its id to the `Application` parameter.
 
    .EXAMPLE
    Remove-BMApplication -Session $session -Application 'So Long'
 
    Demonstrates how to delete an application by passing its name to the `Application` parameter.
 
    .EXAMPLE
    $bmApp,433,'So Long 2' | Remove-BMApplication -Session $session
 
    Demonstrates that you can pipe application ids, names, and/or objects to `Remove-BMApplication`.
 
    .EXAMPLE
    Remove-BMApplication -Session $session -Application $bmApp -Force
 
    Demonstrates how to delete an application even if its still enabled by using the `Force` (switch).
    #>

    [CmdletBinding()]
    param(
        # The session to BuildMaster. Use `New-BMSession` to create a session.
        [Parameter(Mandatory)]
        [Object] $Session,

        # The application's id, name, or object to delete. Accepts pipeline input.
        [Parameter(Mandatory, ValueFromPipeline)]
        [Object] $Application,

        # If set, will delete an active application.
        [switch] $Force
    )

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

        $bmApp = $Application | Get-BMApplication -Session $Session -ErrorAction Ignore
        if (-not $bmApp)
        {
            $msg = "Cannot delete application ""$($Application | Get-BMObjectName)"" because it does not exist."
            Write-Error -Message $msg -ErrorAction $ErrorActionPreference
            return
        }

        if (-not $Force -and $bmApp.Active_Indicator -eq 'Y')
        {
            $msg = "Cannot delete application ""$($bmApp.Application_Name)"" because it is active. Use the " +
                   '"Disable-BMApplication" function to disable the application then delete it, or use this ' +
                   'function''s -Force (switch) to delete this active application.'
            Write-Error -Message $msg -ErrorAction $ErrorActionPreference
            return
        }

        $appArg = @{} | Add-BMObjectParameter -Name 'Application' -Value $bmApp -ForNativeApi -PassThru
        Invoke-BMNativeApiMethod -Session $Session `
                                 -Name 'Applications_PurgeApplicationData' `
                                 -Parameter $appArg `
                                 -Method Post |
            Out-Null
    }
}



function Remove-BMEnvironment
{
    <#
    .SYNOPSIS
    Deletes environments in BuildMaster.
 
    .DESCRIPTION
    The `Remove-BMEnvironment` function removes an environment from BuildMaster. Pass the environment's id, name, or
    object to the `Environment` parameter (or pipe them to `Remove-BMEnvironment`).
 
    Uses the BuildMaster
    [Infrastructure Management API](https://docs.inedo.com/docs/buildmaster-reference-api-infrastructure).
 
    .EXAMPLE
    Remove-BMEnvironment -Session $session -Environment $env
 
    Demonstrates how to delete an environment by passing an environment object to the `Environment` parameter.
 
    .EXAMPLE
    Remove-BMEnvironment -Session $session -Environment 432
 
    Demonstrates how to delete an environment by passing its id to the `Environment` parameter.
 
    .EXAMPLE
    Remove-BMEnvironment -Session $session -Environment 'So Long'
 
    Demonstrates how to delete an environment by passing its name to the `Environment` parameter.
 
    .EXAMPLE
    $env,433,'So Long 2' | Remove-BMEnvironment -Session $session
 
    Demonstrates that you can pipe environment ids, names, and/or objects to `Remove-BMEnvironment`.
    #>

    [CmdletBinding()]
    param(
        # The session to BuildMaster. Use `New-BMSession` to create a session.
        [Parameter(Mandatory)]
        [Object] $Session,

        # The environment's id, name, or object to delete. Accepts pipeline input.
        [Parameter(Mandatory, ValueFromPipeline)]
        [Object] $Environment
    )

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

        $env = $Environment | Get-BMEnvironment -Session $Session -ErrorAction Ignore
        if (-not $env)
        {
            $msg = "Cannot delete environment ""$($env | Get-BMObjectName)"" because it does not exist."
            Write-Error -Message $msg -ErrorAction $ErrorActionPreference
            return
        }

        $apiName = "infrastructure/environments/delete/$($env.name)"
        Invoke-BMRestMethod -Session $Session -Name $apiName -Method Delete | Out-Null
    }
}



function Remove-BMPipeline
{
    <#
    .SYNOPSIS
    Removes a pipeline from BuildMaster.
 
    .DESCRIPTION
    The `Remove-BMPipeline` function removes a pipeline from BuildMaster. Pass the pipeline's name or object to the
    `Pipeline` parameter (or pipe it into the function). The pipeline will be deleted, and its change history will be
    preserved.
 
    To also delete the pipeline's change history, use the `PurgeHistory` switch.
 
    .EXAMPLE
    Remove-BMPipeline -Session $session -Pipeline 'Tutorial'
 
    Demonstrates how to delete a pipeline using its name.
 
    .EXAMPLE
    Remove-BMPipeline -Session $session -Pipeline $pipeline
 
    Demonstrates how to delete a pipeline using a pipeline object.
 
    .EXAMPLE
    $pipeline, 'Tutorial' | Remove-BMPipeline -Session $session
 
    Demonstrates that you can pipe pipeline names and objects into `Remove-BMPipeline`.
 
    .EXAMPLE
    Remove-BMPipeline -Session $session -Pipeline $pipeline -PurgeHistory
 
    Demonstrates how to remove the pipeline's change history along with the pipeline by using the `PurgeHistory` switch.
    #>

    [CmdletBinding()]
    param(
        # The session to BuildMaster. Use `New-BMSession` to create a session.
        [Parameter(Mandatory)]
        [Object] $Session,

        # The raft in which the pipeline is stored. Pass the raft id or raft object.
        [Object] $Raft,

        # The name or pipeline object of the pipeline to delete.
        [Parameter(Mandatory, ValueFromPipeline)]
        [Object] $Pipeline,

        # If set, deletes the pipeline's change history. The default behavior preserve's the change history.
        [switch] $PurgeHistory
    )

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

        $Pipeline | Remove-BMRaftItem -Session $session -Raft $Raft -PurgeHistory:$PurgeHistory
    }
}


function Remove-BMRaft
{
    <#
    .SYNOPSIS
    Removes rafts from BuildMaster.
 
    .DESCRIPTION
    The `Remove-BMRaft` function removes a raft from BuildMaster. Pass its id, name, or raft object to the `Raft`
    parameter (or pipe to the function). If the raft exists, it is deleted. If it doesn't exist, an error is written.
 
    Uses BuildMaster's native API.
 
    .EXAMPLE
    Get-BMRaft -Session $session -Name 'delete me' | Remove-BMRaft -Session $Session
 
    Demonstrates how to delete a raft by using a raft object.
 
    .EXAMPLE
    'delete me' | Remove-BMRaft -Session $Session
 
    Demonstrates how to delete a raft by using the raft's name.
 
    .EXAMPLE
    134 | Remove-BMRaft -Session $Session
 
    Demonstrates how to delete a raft by using its id.
    #>

    [CmdletBinding()]
    param(
        # The session to BuildMaster. Use `New-BMSession` to create a session.
        [Parameter(Mandatory)]
        [Object] $Session,

        # The raft id, name, or raft object to delete.
        [Parameter(Mandatory, ValueFromPipeline)]
        [Object] $Raft
    )

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

        $raftId = $Raft | Get-BMObjectID -ObjectTypeName 'Raft' -ErrorAction Ignore
        $raftName = $Raft | Get-BMObjectName -ObjectTypeName 'Raft' -ErrorAction Ignore
        $bmRaft =
            Get-BMRaft -Session $Session |
            Where-Object {
                if ($null -ne $raftId -and $_.Raft_Id -eq $raftId)
                {
                    return $true
                }

                if ($null -ne $raftName -and $_.Raft_Name -eq $raftName)
                {
                    return $true
                }

                return $false
            }

        if (-not $bmRaft)
        {
            $msg = "Unable to delete raft ""$($Raft | Get-BMObjectName)"" because it does not exist."
            Write-Error -Message $msg -ErrorAction $ErrorActionPreference
            return
        }

        $parameter = @{ 'Raft_Id' = $bmRaft.Raft_Id }
        Invoke-BMNativeApiMethod -Session $Session -Name 'Rafts_DeleteRaft' -Method Post -Parameter $parameter
    }
}


function Remove-BMRaftItem
{
    <#
    .SYNOPSIS
    Removes a raft item from BuildMaster.
 
    .DESCRIPTION
    The `Remove-BMRaftItem` function removes a raft item from BuildMaster. Pass the raft item's id or a raft item
    object to the `RaftItem` parameter (or pipe them into the function). The raft item will be deleted, and its change
    history will be preserved.
 
    To also delete the raft item's change history, use the `PurgeHistory` switch.
 
    .EXAMPLE
    Remove-BMRaftItem -Session $session -RaftItem 'Tutorial'
 
    Demonstrates how to delete a raft item using its name.
 
    .EXAMPLE
    Remove-BMRaftItem -Session $session -RaftItem $raftItem
 
    Demonstrates how to delete a raft item using a raft item object.
 
    .EXAMPLE
    $raftItem, 'Tutorial' | Remove-BMRaftItem -Session $session
 
    Demonstrates that you can pipe raft item names and objects into `Remove-BMRaftItem`.
 
    .EXAMPLE
    Remove-BMRaftItem -Session $session -RaftItem $raftItem -PurgeHistory
 
    Demonstrates how to remove the raft item's change history along with the raft item by using the `PurgeHistory`
    switch.
    #>

    [CmdletBinding()]
    param(
        # The session to BuildMaster. Use `New-BMSession` to create a session.
        [Parameter(Mandatory)]
        [Object] $Session,

        # The raft where the raft item is stored.
        [Object] $Raft,

        # The raft item id or raft item object of the raft item to delete.
        [Parameter(Mandatory, ValueFromPipeline)]
        [Object] $RaftItem,

        # If set, deletes the raft item's change history. The default behavior preserve's the change history.
        [switch] $PurgeHistory
    )

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

        $existingRaftItem = $RaftItem | Get-BMRaftItem -Session $Session -Raft $Raft -ErrorAction Ignore
        if (-not $existingRaftItem)
        {
            $appMsg = $RaftItem | Get-BMObjectName -ObjectTypeName 'Application' -ErrorAction Ignore
            if ($appMsg)
            {
                $appMsg = " in application $($appMsg)"
            }
            $msg = "Could not delete raft item ""$($RaftItem | Get-BMObjectName -ObjectTypeName 'RaftItem')""" +
                "$($appMsg) because it does not exist."
            Write-Error -Message $msg -ErrorAction $ErrorActionPreference
            return
        }

        $numRaftItems = ($existingRaftItem | Measure-Object).Count
        if ($numRaftItems -gt 1)
        {
            $msg = "Could not delete raft item ""$($RaftItem | Get-BMObjectName -ObjectTypeName 'RaftItem')"" " +
                   "because there are $($numRaftItems) that match."
            Write-Error -Message $msg -ErrorAction $ErrorActionPreference
            return
        }

        $raftParams =
            @{} |
            Add-BMObjectParameter -Name 'RaftItem' -Value $existingRaftItem -ForNativeApi -PassThru |
            Add-BMParameter -Name 'PurgeHistory_Indicator' -Value $PurgeHistory.IsPresent -PassThru

        Invoke-BMNativeApiMethod -Session $Session `
                                -Name 'Rafts_DeleteRaftItem' `
                                -Parameter $raftParams `
                                -Method Post |
            Out-Null
    }
}


function Remove-BMServer
{
    <#
    .SYNOPSIS
    Removes a server from BuildMaster.
 
    .DESCRIPTION
    The `Remove-BMServer` function removes a server from BuildMaster. Pass the name of the server to remove to the
    `Name` parameter. If the server doesn't exist, an error is written. To ignore if the server exists or not, set the
    `ErrorAction` parameter to `Ignore`.
 
    Pass the session to the BuildMaster instance where you want to delete the server to the `Session` parameter. Use
    `New-BMSession` to create a session object.
 
    This function uses BuildMaster's infrastructure management API.
 
    .EXAMPLE
    Remove-BMServer -Session $session -Name 'example.com'
 
    Demonstrates how to delete a server.
 
    .EXAMPLE
    Get-BMServer -Session $session -Name 'example.com' | Remove-BMServer -Session $session
 
    Demonstrates that you can pipe the objects returned by `Get-BMServer` into `Remove-BMServer` to remove those
    servers.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "")]
    [CmdletBinding(SupportsShouldProcess)]
    param(
        # The instance of BuildMaster to connect to.
        [Parameter(Mandatory)]
        [Object] $Session,

        # The server to delete. Pass a server id, name, or a server object.
        [Parameter(Mandatory, ValueFromPipeline)]
        [Alias('Name')]
        [Object] $Server
    )

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

        $foundServer = $Server | Get-BMServer -Session $session -ErrorAction Ignore
        if (-not $foundServer)
        {
            $msg = "Could not delete server ""$($Server | Get-BMObjectName)"" because it does not exist."
            Write-Error -Message $msg -ErrorAction $ErrorActionPreference
            return
        }

        $encodedName = [Uri]::EscapeDataString(($Server | Get-BMObjectName))
        Invoke-BMRestMethod -Session $Session `
                            -Name ('infrastructure/servers/delete/{0}' -f $encodedName) `
                            -Method Delete
    }
}


function Remove-BMServerRole
{
    <#
    .SYNOPSIS
    Removes a server role from BuildMaster.
 
    .DESCRIPTION
    The `Remove-BMServerRole` removes a server role from BuildMaster. Pass the name of the role to remove to the `Name` parameter. If the server role doesn't exist, nothing happens.
 
    Pass the session to the BuildMaster instance where you want to delete the role to the `Session` parameter. Use `New-BMSession` to create a session object.
 
    This function uses BuildMaster's infrastructure management API.
 
    .EXAMPLE
    Remove-BMServerRole -Session $session -Name 'Server Role'
 
    Demonstrates how to delete a server role.
 
    .EXAMPLE
    Get-BMServerRole -Session $session -Name 'My Role' | Remove-BMServerRole -Session $session
 
    Demonstrates that you can pipe the objects returned by `Get-BMServerRole` into `Remove-BMServerRole` to remove those roles.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "")]
    [CmdletBinding(SupportsShouldProcess)]
    param(
        # An object representing the instance of BuildMaster to connect to. Use `New-BMSession` to create session objects.
        [Parameter(Mandatory)]
        [Object] $Session,

        # The server role to delete. Pass a server role id, name, or a server role object.
        [Parameter(Mandatory, ValueFromPipeline)]
        [Object] $ServerRole
    )

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

        $role = $ServerRole | Get-BMServerRole -Session $Session -ErrorAction Ignore
        if (-not $role)
        {
            $msg = "Cannot delete server role ""$($ServerRole | Get-BMObjectName)"" because it does not exist."
            Write-Error -Message $msg -ErrorAction $ErrorActionPreference
            return
        }

        $encodedName = [uri]::EscapeDataString(($ServerRole | Get-BMObjectName))
        Invoke-BMRestMethod -Session $Session -Name ('infrastructure/roles/delete/{0}' -f $encodedName) -Method Delete
    }
}


function Remove-BMVariable
{
    <#
    .SYNOPSIS
    Deletes BuildMaster variables.
 
    .DESCRIPTION
    The `Remove-BMVariable` function deletes BuildMaster variables. By default, it deletes global variables. It can also
    delete variables for a specific environment, server, server role, application group, and application variables.
 
    Pass the name of the variable to delete to the `Name` parameter. If no variable exists to delete, you'll get an
    error.
 
    To delete an environment's variables, pass the environment's name to the `EnvironmentName` parameter.
 
    To delete a server role's variables, pass the server role's name to the `ServerRoleName` parameter.
 
    To delete a server's variables, pass the server's name to the `ServerName` parameter.
 
    To delete an application group's variables, pass the application group's name to the `ApplicationGroupName` parameter.
 
    To delete an application's variables, pass the application's name to the `ApplicationName` parameter.
 
    Pass a session object representing the instance of BuildMaster to use to the `Session` parameter. Use
    `New-BMSession` to create a session object.
 
    This function uses BuildMaster's [Variables Management](https://docs.inedo.com/docs/buildmaster-reference-api-variables)
    API. Due to a bug in BuildMaster, when getting application or application group variables, it uses BuildMaster's
    native API.
 
 
    .EXAMPLE
    Remove-BMVariable -Session $session -Name 'Var'
 
    Demonstrates how to delete a global variable.
 
    .EXAMPLE
    Remove-BMVariable -Session $session -Name 'Var' -EnvironmentName 'Dev'
 
    Demonstrates how to delete a variable in an environment.
 
    .EXAMPLE
    Remove-BMVariable -Session $session -Name 'Var' -ServerRoleName 'WebApp'
 
    Demonstrates how to delete a variable in a server role.
 
    .EXAMPLE
    Remove-BMVariable -Session $session -Name 'Var' -ServerName 'example.com'
 
    Demonstrates how to delete a variable in a server.
 
    .EXAMPLE
    Remove-BMVariable -Session $session -Name 'Var' -ApplicationGroupName 'WebApps'
 
    Demonstrates how to delete a variable from an application group.
 
    .EXAMPLE
    Remove-BMVariable -Session $session -Name 'Var' -ApplicationName 'www'
 
    Demonstrates how to delete a variable from an application.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "")]
    [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName='global')]
    param(
        # The session to BuildMaster. Use `New-PSSession` to create a new session.
        [Parameter(Mandatory)]
        [Object]$Session,

        # The variable to delete. Pass a variable name or variable object.
        [Parameter(Mandatory, ValueFromPipeline)]
        [Object] $Variable,

        # The application of the variable to delete. Pass an application id, name, or object.
        [Parameter(Mandatory, ParameterSetName='application')]
        [Object] $Application,

        # The application group of the variable to delete. Pass an application group id, name, or object.
        [Parameter(Mandatory, ParameterSetName='application-group')]
        [Object] $ApplicationGroup,

        # The environment of the variable to delete. Pass an environment id, name, or object.
        [Parameter(Mandatory, ParameterSetName='environment')]
        [Object] $Environment,

        # The server of the variable to delete. Pass a server id, name, or object.
        [Parameter(Mandatory, ParameterSetName='server')]
        [Object] $Server,

        # The server role of the variable to delete. Pass a server role id, name, or object.
        [Parameter(Mandatory, ParameterSetName='role')]
        [Object] $ServerRole
    )

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

        Invoke-BMVariableEndpoint -Session $session `
                                  -Variable $Variable `
                                  -EntityTypeName $PSCmdlet.ParameterSetName `
                                  -BoundParameter $PSBoundParameters `
                                  -ForDelete
    }

}


function Set-BMPipeline
{
    <#
    .SYNOPSIS
    Creates a new pipeline in BuildMaster.
 
    .DESCRIPTION
    The `Set-BMPipeline` function creates or updates a pipeline in BuildMaster. Pass the name of the pipeline to the
    `Name` parameter. Pass the raft id or a raft object where the pipeline should be saved to the `Raft` parameter. A
    global pipeline will be created with no stages.
 
    To assign the pipeline to an application, pass the application's id, name or an application object to the
    `Application` parameter.
 
    To set the stages of the pipeline, use the `New-BMPipelineStageObject` and `New-BMPipelineStageTargetObject`
    functions to create the stages, then pass them to the `Stage` parameter. If the pipeline exists, you can use
    `Get-BMPipeline` to get the pipeline, modify its stage objects, then pass them to the `Stage` parameter.
 
    To set the post-deployment options of the pipeline, use `New-BMPipelinePostDeploymentOptionsObject` to create a
    post-deployment options object and pass that object to the `PostDeploymentOption` parameter.
 
    To set the color of the pipeline, pass the color in CSS RGB color format (e.g. `#aabbcc`) to the `Color` parameter.
 
    If you want stage order to be enforced by the pipeline, use the `EnforceStageSequence` switch.
 
    Any parameters *not* provided are not sent to BuildMaster in the create/update request. Those values won't be
    updated by BuildMaster. Any parameter you pass will cause the respective pipeline property to get updated to that
    value.
 
    This function uses [BuildMaster's native API](http://inedo.com/support/documentation/buildmaster/reference/api/native).
 
    .EXAMPLE
    Set-BMPipeline -Session $session -Name 'Powershell Module'
 
    Demonstrates how to create or update a global pipeline. In this example a pipeline named `PowerShell Module` will be
    created/updated that has no stages and uses BuildMaster's default values.
 
    .EXAMPLE
    Set-BMPipeline -Session $session -Name 'PowerShell Module' -Application $app
 
    Demonstrates how to create or update a pipeline for a specific application. In this example, the pipeline will be
    called `PowerShell Module` and it will be assigned to the `$app` application.
    #>

    [CmdletBinding(DefaultParameterSetName='Global')]
    param(
        # The session to BuildMaster. Use `New-BMSession` to create a session.
        [Parameter(Mandatory)]
        [Object] $Session,

        # The name of the pipeline.
        [Parameter(Mandatory)]
        [String] $Name,

        # The raft where the pipeline should be saved. Use `Get-BMRaft` to see a list of rafts. By default, uses
        # BuildMaster's default raft. Can be the ID of a raft or a raft object.
        [Parameter(Mandatory, ParameterSetName='Global')]
        [Object] $Raft,

        # The application to assign the pipeline to. Pass an application's id, name or an application object.
        [Parameter(Mandatory, ParameterSetName='Application')]
        [Object] $Application,

        # The background color BuildMaster should use when displaying the pipeline's name in the UI. Should be a CSS
        # hexadecimal color, e.g. `#ffffff`
        [String] $Color,

        # Stage configuration for the pipeline. Should be a list of objects returned by `New-BMPipelineStageObject` or
        # returned from `Get-BMPipeline`.
        [Object[]] $Stage,

        # If set, stage sequences will be enforced, i.e. builds can't be deployed to any stage. The default value in the
        # BuildMaster UI for this property is `true`.
        [switch] $EnforceStageSequence,

        # The post deploy options to use when creating the pipeline. Use the `New-BMPipelinePostDeploymentOptionsObject`
        # function to create a post-deployment option object.
        [Object] $PostDeploymentOption,

        # If set, a pipeline object will be returned.
        [switch] $PassThru
    )

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

    $pipeline = [pscustomobject]@{
        Name = $Name;
        Color = $Color;
        EnforceStageSequence = $EnforceStageSequence.IsPresent;
        Stages = $Stage;
    }

    if ($PostDeploymentOption)
    {
        $pipeline | Add-Member -Name 'PostDeploymentOptions' -MemberType NoteProperty -Value $PostDeploymentOption
    }

    $setRaftItemArgs = @{}
    if ($Raft)
    {
        $setRaftItemArgs['Raft'] = $Raft
    }

    if ($Application)
    {
        $setRaftItemArgs['Application'] = $Application
    }

    $Name |
        Set-BMRaftItem -Session $Session `
                       -TypeCode ([BMRaftItemTypeCode]::Pipeline) `
                       -Content ($pipeline | ConvertTo-Json -Depth 100) `
                       -PassThru:$PassThru `
                       @setRaftItemArgs |
        Add-BMPipelineMember -PassThru
}



function Set-BMRaft
{
    <#
    .SYNOPSIS
    Creates and updates rafts in BuildMaster.
 
    .DESCRIPTION
    The `Set-BMRaft` function creates and updates rafts in BuildMaster. To create a raft, pass its name to the `Raft`
    parameter (or pipe the name to `Set-BMRaft`), its optional XML configuration to the `Configuration` parameter, and
    the raft's environment id, name, or environment object to the `Environment` parameter.
 
    To update a raft, pass its name, id, or raft object to the `Raft` parameter, and any new/changed values passed to
    the `Configuration` and `Environment` parameter.
 
    If you want the newly created or updated raft to be returned, use the `PassThru` switch.
 
    Uses the BuildMaster native API.
 
    .EXAMPLE
    'New Raft' | Set-Raft -Session $session
 
    Demonstrates how to create a new Raft by piping its name to the `Set-BMRaft` function.
 
    .EXAMPLE
    Get-BMRaft -Name 'Update Me!' | Set-Raft -Session $session -Configuration $newConfig -Environment $newEnv
 
    Demonstrates how to update an existing raft by piping the raft object to `Set-Raft`, new configuration to the
    `Configuration` parameter, and new environment to the `Environment` parameter.
 
    .EXAMPLE
    $bmRaft = 'New Or Updated Raft' | Set-Raft -Session $session -PassThru
 
    Demonstrates how to get a raft object for the new or updated raft by using the `PassThru` switch.
    #>

    [CmdletBinding()]
    param(
        # The session to BuildMaster. Use `New-BMSession` to create a session.
        [Parameter(Mandatory)]
        [Object] $Session,

        # The raft's name, id, or raft object. When creating a raft, this *must* be the new raft's name.
        [Parameter(Mandatory, ValueFromPipeline)]
        [Object] $Raft,

        # The XML configuration for the raft.
        [String] $Configuration,

        # ***OBSOLETE.*** Not used and has no affect.
        [Parameter(DontShow)]
        [Object] $Environment,

        # If set, the new/updated raft will be returned.
        [switch] $PassThru
    )

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

        $bmEnv = $null
        if ($Environment)
        {
            $msg = 'The Set-BMRaft function''s "Environment" parameter is obsolete and usages should be removed.'
            Write-Warning -Message $msg
        }

        $raftID = $Raft | Get-BMObjectID -ObjectTypeName 'Raft' -ErrorAction Ignore
        $raftName = $Raft | Get-BMObjectName -ObjectTypeName 'Raft' -ErrorAction Ignore
        $bmRaft = Get-BMRaft -Session $Session | Where-Object {
            if ($null -ne $raftID -and ($_.Raft_Id -eq $raftID))
            {
                return $true
            }

            if ($null -ne $raftName -and $_.Raft_Name -eq $raftName)
            {
                return $true
            }

            return $false
        }

        if ($bmRaft -and $null -eq $raftName)
        {
            $raftName = $bmRaft.Raft_Name
        }

        $parameters =
            @{} |
            Add-BMObjectParameter -Name 'Raft' -Value $bmRaft -AsID -ForNativeApi -PassThru | `
            Add-BMObjectParameter -Name 'Raft' -Value $raftName -AsName -ForNativeApi -PassThru | `
            Add-BMParameter -Name 'Raft_Configuration' -Value $PSBoundParameters['Configuration'] -PassThru | `
            Add-BMObjectParameter -Name 'Environment' -Value $bmEnv -PassThru

        $id = Invoke-BMNativeApiMethod -Session $Session `
                                       -Name 'Rafts_CreateOrUpdateRaft' `
                                       -Method Post `
                                       -Parameter $parameters

        if ($PassThru)
        {
            return Get-BMRaft -Session $Session | Where-Object 'Raft_Id' -EQ $id
        }

    }
}


function Set-BMRaftItem
{
    <#
    .SYNOPSIS
    Creates or updates a raft item in BuildMaster.
 
    .DESCRIPTION
    The `Set-BMRaftItem` function creates or updates a raft item in BuildMaster. Pass the raft item's name to the
    `Name` parameter. Pass the raft item's raft id or a raft object to the `Raft` parameter. Pass the object's
    type to the `TypeCode` parameter. The raft item will be created if it doesn't exist, or updated if it does.
 
    To assign the raft item to a specific application, pass the application's id, name or an application object to the
    `Application` parameter. The application must be configured to use the same raft as the raft item. If it doesn't,
    you'll get an error.
 
    Pass the raft item's content to the `Content` parameter.
 
    Pass the username of the user creating/updating the raft item to the `UserName` parameter. The default username is
    `DOMAIN\UserName` if the current computer is in a Microsoft domain, `UserName@MachineName` otherwise.
 
    If you want the created/updated raft item's object to be returned, use the `PassThru` switch.
 
    When creating scripts, the name of the raft item should end with an extension for the type of script it is, e.g.
    `.ps1` for PowerShell scripts, `.sh` for shell scripts, etc.
 
    Parameters not passed will not be sent to BuildMaster, and typically BuildMaster leaves those values as-is. Any
    parameter that you pass will get sent to BuildMaster and the respective raft item properties will be updated.
 
    .EXAMPLE
    Set-BMRaftItem -Session $session -Raft $raft -TypeCode Pipeline -Name 'Pipeline'
 
    Demonstrates how to use Set-BMRaftItem. In this example, an empty, global pipeline will be created in the raft
    represented by the `$raft` object.
 
    .EXAMPLE
    Set-BMRaftItem -Session $session -Raft $raft -TypeCode Module -Name 'Module' -Application $app -Content $otterScript
 
    Demonstrates how to use Set-BMRaftItem. In this example, a module will be created/updated for the application
    represented by the `$app` object with the content in the `$otterScript` variable.
    #>

    [CmdletBinding(DefaultParameterSetName='Global')]
    param(
        # The session to BuildMaster. Use `New-BMSession` to create a session.
        [Parameter(Mandatory)]
        [Object] $Session,

        # The raft where the raft item should be saved. Pass the raft id, name or a raft object.
        [Parameter(Mandatory, ParameterSetName='Global')]
        [Object] $Raft,

        # The type of the raft item. Valid values are:
        #
        # * Module (for creating OtterScript modules)
        # * Script (for creating PowerShell, batch, or shell scripts; make sure the raft item's name ends with the
        # extension for that script type)
        # * DeploymentPlan (for creating a deployment plan)
        # * Pipeline (for creating a pipeline; use `Set-BMPipeline` instead)
        [Parameter(Mandatory)]
        [BMRaftItemTypeCode] $TypeCode,

        # The raft item. To create a raft item, must be a name. If updating an existing raft item, can be a raft item
        # id, name, or raft item object.
        #
        # If the raft item is a script, its name must end with the extension of the script type, e.g. `.ps1` for
        # PowerShell scripts, `.sh` for shell scripts, etc.
        [Parameter(Mandatory, ValueFromPipeline)]
        [Object] $RaftItem,

        # The application the raft item belongs to. Pass the application's id, name, or an application object.
        [Parameter(Mandatory, ParameterSetName='Application')]
        [Object] $Application,

        # The content of the raft item.
        [String] $Content,

        # The username of the individual who is creating/updating the raft item.
        [String] $UserName,

        # If set, will return an object representing the created/update raft item.
        [switch] $PassThru
    )

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

    if( -not $UserName )
    {
        $UserName = [Environment]::UserName
        if ([Environment]::UserDomainName)
        {
            $UserName = "$([Environment]::UserDomainName)\$($UserName)"
        }
        else
        {
            $UserName = "$($UserName)@$([Environment]::MachineName)"
        }
    }

    $contentBytes = $Content | ConvertTo-BMNativeApiByteValue
    $setRaftArgs =
        @{
            RaftItem_Name = ($RaftItem | Get-BMObjectName);
            RaftItemType_Code = $TypeCode;
            ModifiedOn_Date = [DateTimeOffset]::Now;
            ModifiedBy_User_Name = $UserName;
        } |
        Add-BMParameter -PassThru -Name 'ModifiedBy_User_Name' -Value $UserName |
        Add-BMParameter -PassThru -Name 'Content_Bytes' -Value $contentBytes |
        Add-BMParameter -PassThru -Name 'Active_Indicator' -Value $true

    if ($Raft)
    {
        $Raft = $Raft | Get-BMRaft -Session $Session
        if (-not $Raft)
        {
            return
        }
    }

    # Make sure the application exists and use its raft to store the raft item.
    if ($Application)
    {
        $bmApp = Get-BMApplication -Session $Session -Application $Application
        if (-not $bmApp)
        {
            return
        }
        $setRaftArgs['Application_Id'] = $bmApp.Application_Id

        if ($bmApp.Raft_Name)
        {
            $Raft = $bmApp.Raft_Name | Get-BMRaft -Session $Session
        }

        if (-not $Raft)
        {
            # If an application isn't assigned to a raft, BuildMaster stores its code in the default raft.
            $Raft = Get-BMRaft -Session $Session -Raft 1
            if (-not $Raft)
            {
                $appName = $bmApp.Application_Name
                $raftItemName = $RaftItem | Get-BMObjectName
                $typeName = $TypeCode | Get-BMRaftTypeDisplayName
                $msg = "Failed to save the ""$($appName)"" application's ""$($raftItemName)"" $($typeName) because " +
                       'the application is configured to use the default raft but the default raft does not exist.'
                Write-Error -Message $msg -ErrorAction $ErrorActionPreference
            }
        }

    }

    $setRaftArgs['Raft_Id'] = $Raft.Raft_Id

    Invoke-BMNativeApiMethod -Session $Session `
                             -Name 'Rafts_CreateOrUpdateRaftItem' `
                             -Parameter $setRaftArgs `
                             -Method Post |
        Out-Null

    if ($PassThru)
    {
        $RaftItem | Get-BMRaftItem -Session $Session -Raft $Raft -Application $Application -TypeCode $TypeCode
    }
}


function Set-BMRelease
{
    <#
    .SYNOPSIS
    Updates a release in BuildMaster.
 
    .DESCRIPTION
    The `Set-BMRelease` function creates a BuildMaster release or updates an existing release. Pass the release's
    pipeline name or a pipeline object to the `Pipeline` object. Pass the release's name to the `Name` parameter.
 
    This function uses the BuildMaster native API endpoint "Releases_CreateOrUpdateRelease".
 
    If any parameter isn't passed, and the release exists, the respective properties on the release won't be updated.
 
    .EXAMPLE
    Set-BMRelease -Session $session -Release $release -Pipeline 'My Pipeline' -Name 'My New Name'
 
    Demonstrates how to update the pipeline and name of a release.
    #>

    [CmdletBinding()]
    param(
        # The session to BuildMaster. Use the `New-BMSession` object to create the session.
        [Parameter(Mandatory)]
        [Object] $Session,

        # The release to update. Pass the release's id, name, or a release object.
        [Parameter(Mandatory)]
        [Object] $Release,

        # The release's pipeline.
        [Alias('PipelineID')]
        [Object] $Pipeline,

        # The release's name. If the release exists, its name will be changed to this value.
        [String] $Name
    )

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

        $bmRelease = Get-BMRelease -Session $Session -Release $Release
        if( -not $bmRelease )
        {
            return
        }

        if ($Pipeline)
        {
            $bmPipeline = $Pipeline | Get-BMPipeline -Session $Session
        }
        else
        {
            $bmPipeline =
                $bmRelease.pipelineName | Get-BMPipeline -Session $Session -Application $bmRelease.applicationId
        }

        if (-not $bmPipeline)
        {
            return
        }

        $raft = $bmPipeline.Raft_Id | Get-BMRaft -Session $Session
        if (-not $raft)
        {
            return
        }

        if( -not $Name )
        {
            $Name = $bmRelease.name
        }

        $parameter = @{
            Application_Id = $bmRelease.ApplicationId;
            Release_Number = $bmRelease.number;
            Pipeline_Name = "$($raft.Raft_Prefix)::$($bmPipeline.RaftItem_Name)";
            Release_Name = $Name;
        }
        Invoke-BMNativeApiMethod -Session $Session `
                                 -Name 'Releases_CreateOrUpdateRelease' `
                                 -Method Post `
                                 -Parameter $parameter | Out-Null

        Get-BMRelease -Session $Session -Release $bmRelease -Application $bmRelease.ApplicationId
    }
}


function Set-BMVariable
{
    <#
    .SYNOPSIS
    Create or set a BuildMaster variable.
 
    .DESCRIPTION
    The `Set-BMVariable` function creates or sets the value of a BuildMaster variable. By default, it creates/sets
    global variables. It can also set environment, server, server role, application group, and application variables.
 
    Pass the variable's name to the `Name` parameter. Pass the variable's value to the `Value` parameter. If the raw
    flag is used then the variable is passed as-is to buildmaster, otherwise it will be converted to an OtterScript
    value. If the variable is a PowerShell hashtable then it will be converted to an OtterScript map. If the
    variable is a PowerShell array then it will be converted to an OtterScript vector. All other types will be left as
    their default string representation.
 
    To set an environment's variable, pass the environment's name to the `EnvironmentName` parameter.
 
    To set a server role's variable, pass the server role's name to the `ServerRoleName` parameter.
 
    To set a server's variable, pass the server's name to the `ServerName` parameter.
 
    To set an application group's variable, pass the application group's name to the `ApplicationGroupName` parameter.
 
    To set an application's variable, pass the application's name to the `ApplicationName` parameter.
 
    Pass a session object representing the instance of BuildMaster to use to the `Session` parameter. Use
    `New-BMSession` to create a session object.
 
    This function uses BuildMaster's variables API.
 
    .EXAMPLE
    Set-BMVariable -Session $session -Name 'Var' -Value 'Value'
 
    Demonstrates how to create or set a global variable.
 
    .EXAMPLE
    Set-BMVariable -Session $session -Name 'Var' -Value 'Value' -EnvironmentName 'Dev'
 
    Demonstrates how to create or set a variable in an environment.
 
    .EXAMPLE
    Set-BMVariable -Session $session -Name 'Var' -Value 'Value' -ServerRoleName 'WebApp'
 
    Demonstrates how to create or set a variable for a server role.
 
    .EXAMPLE
    Set-BMVariable -Session $session -Name 'Var' -Value 'Value' -ServerName 'example.com'
 
    Demonstrates how to create or set a variable for a server.
 
    .EXAMPLE
    Set-BMVariable -Session $session -Name 'Var' -Value 'Value' -ApplicationGroupName 'WebApps'
 
    Demonstrates how to create or set a variable for an application group.
 
    .EXAMPLE
    Set-BMVariable -Session $session -Name 'Var' -Value 'Value' -ApplicationName 'www'
 
    Demonstrates how to create or set a variable for an application.
 
    .EXAMPLE
    Set-BMVariable -Session $session -Name 'var' -Value @('hi', 'there') -ApplicationName 'www'
 
    Demonstrates how to set the variable 'var' to an OtterScript vector.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessage('PSShouldProcess', '')]
    [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName='global')]
    param(
        # The session to BuildMaster. Use `New-BMSession` to create a session.
        [Parameter(Mandatory)]
        [Object] $Session,

        # The name of the variable to create.
        [Parameter(Mandatory)]
        [String] $Name,

        # The variable's value. If a PowerShell array or hashtable is passed in it will be converted to the equivalent
        # OtterScript expression.
        [Parameter(Mandatory)]
        [Object] $Value,

        # The name of the application where the variable should be created. The default is to create a global variable.
        [Parameter(Mandatory,ParameterSetName='application')]
        [Alias('ApplicationName')]
        [Object] $Application,

        # The name of the application group where the variable should be created. The default is to create a global variable.
        [Parameter(Mandatory,ParameterSetName='application-group')]
        [Alias('ApplicationGroupName')]
        [Object] $ApplicationGroup,

        # The name of the environment where the variable should be created. The default is to create a global variable.
        [Parameter(Mandatory,ParameterSetName='environment')]
        [Alias('EnvironmentName')]
        [Object] $Environment,

        # The name of the server where the variable should be created. The default is to create a global variable.
        [Parameter(Mandatory,ParameterSetName='server')]
        [Alias('ServerName')]
        [Object] $Server,

        # The name of the server role where the variable should be created. The default is to create a global variable.
        [Parameter(Mandatory,ParameterSetName='role')]
        [Alias('ServerRoleName')]
        [Object] $ServerRole
    )

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

    $Value = ConvertTo-BMOtterScriptExpression -Value $Value

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

    Invoke-BMVariableEndpoint -Session $Session `
                              -Variable $Name `
                              -Value $Value `
                              -EntityTypeName $PSCmdlet.ParameterSetName `
                              -BoundParameter $PSBoundParameters
}

 function Stop-BMRelease
{
    <#
    .SYNOPSIS
    Cancels a release.
 
    .DESCRIPTION
    The `Stop-BMRelease` function cancels a BuildMaster release. It calls the `Releases_CancelRelease` native API
    endpoint. Pass the application name, id, or application object to the `Application` parameter and the release
    number to the `Number parameter. You can optionally provide a reason for cancelling the release by using the
    `Release` parameter.
 
    .EXAMPLE
    Stop-BMRelease -Session $session -Application 11 -Number 1.1
 
    Demonstrates how to cancel a release. In this case, the `1.1` release of the application whose ID is `11` is
    cancelled.
 
    .EXAMPLE
    Stop-BMRelease -Session $session -Application 'BuildMaster Automation' -Number 1.1
 
    Demonstrates how to cancel a release. In this case, the `1.1` release of the `BuildMaster Automation` application is
    cancelled.
    #>

    [CmdletBinding()]
    param(
        # The session to BuildMaster. Use `New-BMSession` to create a session.
        [Parameter(Mandatory)]
        [Object] $Session,

        # The application name, id, or application object that should be cancelled.
        [Parameter(Mandatory)]
        [Alias('ApplicationID')]
        [Object] $Application,

        # The release number, e.g. 1, 2, 3, 1.0, 2.0, etc.
        [Parameter(Mandatory)]
        [String] $Number,

        # The reason for cancelling the release.
        [String] $Reason
    )

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

    $bmApp = $Application | Get-BMApplication -Session $Session
    if (-not $bmApp)
    {
        return
    }

    $parameter = @{
            Application_Id = $bmApp.Application_Id;
            Release_Number = $Number;
            CancelledReason_Text = $Reason;
        }

    Invoke-BMNativeApiMethod -Session $Session -Name 'Releases_CancelRelease' -Parameter $parameter -Method Post
}


function Test-BMID
{
    <#
    .SYNOPSIS
    Tests if an object is a BuildMaster ID.
 
    .DESCRIPTION
    The `Test-BMID` function tests if an object is actually an ID. An ID is any signed or unsigned integer type,
    including bytes.
 
    .EXAMPLE
    1 | Test-BMID
 
    Returns `$true`.
 
    .EXAMPLE
    '1' | Test-BMID
 
    Returns `$false`.
 
    .EXAMPLE
    $null | Test-BMID
 
    Returns `$false`.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipeline, Position=0)]
        [AllowNull()]
        [AllowEmptyString()]
        [Object] $ID
    )

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

        if ($null -eq $ID)
        {
            return $false
        }

        $intTypes = @(
            [TypeCode]::Byte,
            [TypeCode]::Int16,
            [TypeCode]::Int32,
            [TypeCode]::Int64,
            [TypeCode]::SByte,
            [TypeCode]::UInt16,
            [TypeCode]::UInt32,
            [TypeCode]::UInt64
        )

        return ([Type]::GetTypeCode($ID.GetType()) -in $intTypes)
    }
}


function Test-BMName
{
    <#
    .SYNOPSIS
    Tests if an object is a BuildMaster name.
 
    .DESCRIPTION
    The `Test-BMName` function tests if an object is actually a name.
 
    .EXAMPLE
    1 | Test-BMName
 
    Returns `$false`.
 
    .EXAMPLE
    'YOLO' | Test-BMName
 
    Returns `$true`.
 
    .EXAMPLE
    '$null | Test-BMName
 
    Returns `$false`.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipeline, Position=0)]
        [AllowNull()]
        [AllowEmptyString()]
        [Object] $Name
    )

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

        if ($null -eq $Name)
        {
            return $false
        }

        return ($Name -is [String])
    }
}


function Test-BMObject
{
    <#
    .SYNOPSIS
    Tests if an object is a BuildMaster object.
 
    .DESCRIPTION
    The `Test-BMObject` function tests if an object is actually a name.
 
    .EXAMPLE
    1 | Test-BMObject
 
    Returns `$false`.
 
    .EXAMPLE
    'YOLO' | Test-BMObject
 
    Returns `$false`.
 
    .EXAMPLE
    Get-BMApplication -Session $session -Name 'MyApp' | Test-BMObject
 
    Returns `$true`.
 
    .EXAMPLE
    $null | Test-BMObject
 
    Returns `$false`.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipeline, Position=0)]
        [AllowNull()]
        [AllowEmptyString()]
        [Object] $Object
    )

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

        if ($null -eq $Object)
        {
            return $false
        }

        return (-not ($Object | Test-BMID) -and -not ($Object | Test-BMName))
    }
}

# 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)
        }
    }

}