Chain Deployment

Octopus.Script exported 2023-02-23 by LeoDOD belongs to ‘Octopus’ category.

Triggers a deployment of another project in Octopus

Parameters

When steps based on the template are included in a project’s deployment process, the parameters below can be set.

API Key

Chain_ApiKey =

An Octopus API Key with appropriate permissions to perform the deployment

Project Name

Chain_ProjectName =

Name of the Octopus project that should be deployed

Channel Name

Chain_Channel =

The project channel to use when finding or creating the release to deploy

Leave blank to use the default channel

Release Number

Chain_ReleaseNum =

Release number to use for the deployment

Leave blank to use the latest release in the channel

Create new release?

Chain_CreateOption =

If a release should be created as part of this deployment

The release is created using either the Release Number if specified, or from the project release version template / donor package step if not specified

A release will not be created if it is found to already exist

Update Variable Snapshot?

Chain_SnapshotVariables = False

Should variables in the release be updated before deploying?

By updating the variables, the current snapshot will be discarded, and the latest variables (as seen on the Variables tab) will be imported

Environment Name

Chain_DeployTo = #{Octopus.Environment.Name}

The name of an environment to deploy to

Multiple environments can be deployed to by entering the name of a lifecycle phase

Tenants

Chain_Tenants =

Leave blank to perform an untenanted deployment

A list of tenants & tenant tags to deploy. Tenant Tags are specified in Canonical Name format:

Tag Set Name/Tag Name

Individual tenants can be listed in addition to tags

Tenant Name

Form Values

Chain_FormValues =

Provide values for prompted variables to use in the deployment

Variables should be listed one per line using the format

VariableName = Value

Steps To Skip

Chain_StepsToSkip =

A list of steps which should be skipped in the deployment

Steps should be listed one per line, and specified using either the step number (as per the deployment plan) or by the step name

Failure Handling

Chain_GuidedFailure = Default

Determines how deployment failures & guided failure mode should be handled for the deployment

Automatic failure handling is performed by using guided failure and submitting an appropriate action

The number of retry attempts can be customised by setting the following variables in indexer notion

  • Octopus.Action.StepRetryCount
  • Octopus.Action.DeploymentRetryCount

Scheduling

Chain_DeploySchedule = WaitForDeployment

Defines how the deployment should be scheduled & run

Note: Automated failure handling & post-deploy script functionality is only available when the Wait For Deployment option is selected

A user defined schedule can be set with the Use a custom expression option For an exact date & time use the following format, the day is optional and the time is in 24-hour format

[Mon/Tue/Wed/Thu/Fri/Sat/Sun] @ HH:MM

To schedule a deployment a relative number of hours & minutes in the future use

+ MMM
+ HHH:MM

Note: Reoccurring deployments & automatic retry of failed deployment are possible using a scheduled deployment and the Always Run or On Failure run conditions

Post-Deploy Script

Chain_PostDeploy =

A PowerShell script which should be run after a successful deployment

Variables are replaced in the script using the resultant Manifest VariableSet from the deployment in the binding syntax format

Variables are not available if they are:

  • Sensitive
  • Action scoped
  • Machine scoped
  • Role scoped

When performing a tenanted deployment the script will be run once for each tenant using the specific variables from their deployment

Force Package Download

Chain_ForcePackageDownload = False

Should we redownload the package for this release?

Machine List

Chain_MachineList =

A list of Machine Names which should be Targeted in the deployment

Machine Names should be listed one per line and specified using either the Machine Id or by the Machine name

Script body

Steps based on this template will execute the following PowerShell script.

<#
----- Chain Deployment -----
Authors & Credits
    Paul Marston @paulmarsy (paul@marston.me)
    Joe Waid @joewaid
    Henrik Andersson @alfhenrik
    Damian Brady @Damovisa
    Aaron Burke @aburke-incomm (aburke@incomm.com)
    Bob Hindy @bstr413
    Leo De Oliveira Dias @LeoDOD
Links
    https://library.octopus.com/step-templates/18392835-d50e-4ce9-9065-8e15a3c30954
    https://github.com/OctopusDeploy/Library/commits/master/step-templates/octopus-chain-deployment.json

----- Advanced Configuration Settings -----
Variable names can use either of the following two formats: 
    Octopus.Action.<Setting Name> - will apply to all steps in the deployment, e.g.
        Octopus.Action.DebugLogging
    Octopus.Action[Step Name].<Setting Name> - will apply to 'step name' alone, e.g.
        Octopus.Action[Provision Virtual Machine].DeploymentRetryCount

Available Settings:
    - DebugLogging - set to 'True' or 'False' to log all GET web requests
    - GuidedFailureMessage - will change the note used when submitting guided failure actions, the following variables will be replaced in the text:
        #{GuidedFailureActionIndex} - The current count of interrupts for that step e.g. 1
        #{GuidedFailureAction} - The action being submitted by the step e.g. Retry
    - DeploymentRetryCount - will override the number of times a deployment will be retried when unsuccessful and enable retrying when the failure option is set for a different option, default is 1
    - StepRetryCount - will override the number of times a deployment step will be retried before before submitting Ignore or Abort, default is 1
    - RetryWaitPeriod - an additional delay in seconds wait before retrying a failed step/deployment, default is 0
    - QueueTimeout - when scheduling a deployment for later a timeout must be provided, this allows a custom value, default is 30:00, format is hh:mm
    - OctopusServerUrl - will override the base url used for all webrequests, making it possible to chain deployments on a different Octopus instance/server, or as a workaround for misconfigured node settings

----- Changelog -----
25. Feb 9, 2023 - Bob Hindy @bstr413
	- Fixed issue caused by version 23 where script would not work with on premise Octopus servers. (Reverted most of the changes 60ae653 and d614a2d made to this step template.)
24. Sept 13, 2021 - Mark Harrison @harrisonmeister
    - Fixed issue where the Invoke-OctopusApi function would error with 404: NotFound when running Chain deployment on an Octopus instance 
      that runs under either a "virtual directory" / route prefix other than the route e.g https://my.octopus.app/octo/
23. Aug 23rd, 2021 - Ben Macpherson benjimac93
    - Use Octopus.Web.ServerUri in place of Octopus.Web.BaseUrl if present.
22. Dec 31, 2020 - Josh Slaughter @joshgk00
	- Fixed an issue where the script was unable to create a release if Chained project contained a step with multiple package references
20. Sept 3, 2020 - Mark Harrison @harrisonmeister
	- Included setting to TLS 1.2. 
19. July 17, 2020 - Aaron Burke @aburke-incomm
	- Update script handle Regex for Channel Tags in the CreateRelease Function
17. December 18, 2018 - Jim Burger @burgomg
	- Added Spaces compatibility
16. November 22, 2018 - Patrick Kearney @patrickkearney
    - Fixed an issue where the step was unable to pass a form variable containing an "=" in the value.
15. July 17, 2017 - Robert Glickman @robertglickman
    - Fixed an issue where the step would fail in Octopus 3.15+ due to templated URIs not being handled
14. May 5, 2017 - Paul Marston @paulmarsy (paul@marston.me)
    - Improved step parameter metadata & validation
    - Added changelog, documentation of advanced settings
    - Supports deploying to multiple environments in one step by specifying a lifecycle phase name e.g. 'Dev'
    - Automated retry of the entire deployment as an additional failure handling option
    - Number of step/deployment retries is configurable using a settings variable
    - Supports Octopus scheduled deployments (can be used for reoccuring scheduled deploys, or autonomous deployment retry)
    - Individual tenants as well as tenant tags can be deployed to
    - Fixing a bug where Guided Failure is always evaluated to true
    - Improved identification of valid environment&tenant promotions by using the 'deployment template' api
    - If a release version has already been created, it will be used rather than erroring trying to recreate it
    - Using 'Fail-Step' for better error logging
    - Fixed a bug where log messages with an identical timestamp were repeatedly reported
    - Added an option to wait before retrying a step/deployment
    - A release's channel is taken into account when checking if an existing release version can be used
13. Apr 21, 2017 - Paul Marston @paulmarsy (paul@marston.me)
    - Complete step template rewrite
    - Improved logging
        * Logs only written when chained deployment changes
        * Progress of deployment step states is reported
        * Errors & warnings are reported without interpretation in parent deployment
        * Manual intervention & guided failure events are reported
        * Queue position reported before deployment starts
        * Verbose logging of useful API urls
    - Multi-tenancy support and handling multiple tenant deploys from one chain step
    - Support for skipping steps
    - Support for prompted form variables
    - Create release functionality supports using the version from the incremented version template or donor package
    - Ability to snapshot update variables of a release before deploying
    - Automated handling of guided failure scenarios e.g. retry on step failure, then abort if it errors a second time
    - Transient Octopus API request failures are handled (e.g. we saw many deployments failing because of a request timeout)
    - Post-deploy script support with variable substitution performed using the manifest variable set of the chained deployment with appropriate scoping applied (though not advanced scope specificity)
    - Defaulting channel to a blank value which looks for one with 'IsDefault' set true
    - Create release performs a simplified package version lookup to populate the 'SelectedPackages' field
12. Mar 30, 2017 - Joe Waid @joewaid
    - Pass the Environments "Guided Failure" setting
    - Check status after deployment when Chain_WaitForDeployment is true
11. Nov 21, 2016 - Henrik Andersson @alfhenrik
    - Add Wait for deployment option to chain deployment step template
10. May 2, 2016 - Damian Brady @Damovisa
    - Add Chained Deployment step template
#>
#Requires -Version 5
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
$DefaultUrl = $OctopusParameters['Octopus.Web.BaseUrl']
$Chain_BaseApiUrl = "/api"

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

function Test-String {
    param([Parameter(Position = 0)]$InputObject, [switch]$ForAbsence)

    $hasNoValue = [System.String]::IsNullOrWhiteSpace($InputObject)
    if ($ForAbsence) { $hasNoValue }
    else { -not $hasNoValue }
}

function Get-OctopusSetting {
    param([Parameter(Position = 0, Mandatory)][string]$Name, [Parameter(Position = 1, Mandatory)]$DefaultValue)
    $formattedName = 'Octopus.Action.{0}' -f $Name
    if ($OctopusParameters.ContainsKey($formattedName)) {
        $value = $OctopusParameters[$formattedName]
        if ($DefaultValue -is [int]) { return ([int]::Parse($value)) }
        if ($DefaultValue -is [bool]) { return ([System.Convert]::ToBoolean($value)) }
        if ($DefaultValue -is [array] -or $DefaultValue -is [hashtable] -or $DefaultValue -is [pscustomobject]) { return (ConvertFrom-Json -InputObject $value) }
        return $value
    }
    else { return $DefaultValue }
}

# Write functions are re-defined using octopus service messages to preserve formatting of log messages received from the chained deployment and avoid errors being twice wrapped in an ErrorRecord
function Write-Fatal($message, $exitCode = -1) {
    if (Test-Path Function:\Fail-Step) {
        Fail-Step $message
    }
    else {
        Write-Host ("##octopus[stdout-error]`n{0}" -f $message)
        Exit $exitCode
    }
}
function Write-Error($message) { Write-Host ("##octopus[stdout-error]`n{0}`n##octopus[stdout-default]" -f $message) }
function Write-Warning($message) { Write-Host ("##octopus[stdout-warning]`n{0}`n##octopus[stdout-default]" -f $message) }
function Write-Verbose($message) { Write-Host ("##octopus[stdout-verbose]`n{0}`n##octopus[stdout-default]" -f $message) }

# Use "Octopus.Web.ServerUri" if it is available
if ([string]::IsNullOrWhiteSpace($OctopusParameters['Octopus.Web.ServerUri']) -eq $False) {
    $DefaultUrl = $OctopusParameters['Octopus.Web.ServerUri']
}

$Chain_BaseUrl = (Get-OctopusSetting OctopusServerUrl $DefaultUrl).Trim('/')
if (Test-String $Chain_ApiKey -ForAbsence) {
    Write-Fatal "The step parameter 'API Key' was not found. This step requires an API Key to function, please provide one and try again."
}
$DebugLogging = Get-OctopusSetting DebugLogging $false

# Replace any "virtual directory" or route prefix e.g from the Links collection used
# with the api e.g. /api
function Format-LinksUri {
    param(
        [Parameter(Position = 0, Mandatory)]
        $Uri
    )
    $Uri = $Uri -replace '.*/api', '/api'
    Return $Uri
}
# Replace any "virtual directory" or route prefix e.g from the Links collection used
# with the web app e.g. /app
function Format-WebLinksUri {
    param(
        [Parameter(Position = 0, Mandatory)]
        $Uri
    )
    $Uri = $Uri -replace '.*/app', '/app'
    Return $Uri
}

function Invoke-OctopusApi {
    param(
        [Parameter(Position = 0, Mandatory)]$Uri,
        [ValidateSet('Get', 'Post', 'Put')]$Method = 'Get',
        $Body,
        [switch]$GetErrorResponse
    )
    # Replace query string example parameters e.g. {?skip,take,partialName} 
    # Replace any "virtual directory" or route prefix e.g from the Links collection.
    $Uri = $Uri -replace '{.*?}', '' -replace '.*/api', '/api'
    $requestParameters = @{
        Uri             = ('{0}/{1}' -f $Chain_BaseUrl, $Uri.TrimStart('/'))
        Method          = $Method
        Headers         = @{ 'X-Octopus-ApiKey' = $Chain_ApiKey }
        UseBasicParsing = $true
    }
    if ($Method -ne 'Get' -or $DebugLogging) {
        Write-Verbose ('{0} {1}' -f $Method.ToUpperInvariant(), $requestParameters.Uri)
    }
    if ($null -ne $Body) {
        $requestParameters.Add('Body', (ConvertTo-Json -InputObject $Body -Depth 10))
        Write-Verbose $requestParameters.Body
    }
    
    $wait = 0
    $webRequest = $null
    while ($null -eq $webRequest) {	
        try {
            $webRequest = Invoke-WebRequest @requestParameters
        }
        catch {
            if ($_.Exception -is [System.Net.WebException] -and $null -ne $_.Exception.Response) {
                $errorResponse = [System.IO.StreamReader]::new($_.Exception.Response.GetResponseStream()).ReadToEnd()
                Write-Verbose ("Error Response:`n{0}" -f $errorResponse)
                if ($GetErrorResponse) {
                    return ($errorResponse | ConvertFrom-Json)
                }
                if ($_.Exception.Response.StatusCode -in @([System.Net.HttpStatusCode]::NotFound, [System.Net.HttpStatusCode]::InternalServerError, [System.Net.HttpStatusCode]::BadRequest, [System.Net.HttpStatusCode]::Unauthorized)) {
                    Write-Fatal $_.Exception.Message
                }
            }
            if ($wait -eq 120) {
                Write-Fatal ("Octopus web request ({0}: {1}) failed & the maximum number of retries has been exceeded:`n{2}" -f $Method.ToUpperInvariant(), $requestParameters.Uri, $_.Exception.Message) -43
            }
            $wait = switch ($wait) {
                0 { 30 }
                30 { 60 }
                60 { 120 }
            }
            Write-Warning ("Octopus web request ({0}: {1}) failed & will be retried in $wait seconds:`n{2}" -f $Method.ToUpperInvariant(), $requestParameters.Uri, $_.Exception.Message)
            Start-Sleep -Seconds $wait
        }
    }
    $webRequest.Content | ConvertFrom-Json | Write-Output
}

function Get-FilteredOctopusItem {
    param(
        $itemList,
        $itemName
    )

    if ($itemList.Items.Count -eq 0) {
        Write-Fatal "Unable to find $itemName.  Exiting with an exit code of 1."
        Exit 1
    }  

    $item = $itemList.Items | Where-Object { $_.Name -eq $itemName }      

    if ($null -eq $item) {
        Write-Fatal "Unable to find $itemName.  Exiting with an exit code of 1."
        exit 1
    }
    
    if ($item -is [array]) {
        Write-Fatal "More than one item exists with the name $itemName.  Exiting with an exit code of 1."
        exit 1
    }

    return $item
}

function Test-SpacesApi {
    Write-Verbose "Checking API compatibility";
    $rootDocument = Invoke-OctopusApi "api/";
    if ($null -ne $rootDocument.Links -and $null -ne $rootDocument.Links.Spaces) {
        Write-Verbose "Spaces API found"
        return $true;
    }
    Write-Verbose "Pre-spaces API found"
    return $false;
}

if (Test-SpacesApi) {
    $spaceId = $OctopusParameters['Octopus.Space.Id'];
    if ([string]::IsNullOrWhiteSpace($spaceId)) {
        throw "This step needs to be run in a context that provides a value for the 'Octopus.Space.Id' system variable. In this case, we received a blank value, which isn't expected - please reach out to our support team at https://help.octopus.com if you encounter this error.";
    }
    $Chain_BaseApiUrl = "/api/$spaceId" ;
}

enum GuidedFailure {
    Default
    Enabled
    Disabled
    RetryIgnore
    RetryAbort
    Ignore
    RetryDeployment
}

class DeploymentContext {
    hidden $BaseUrl
    hidden $BaseApiUrl
    DeploymentContext($baseUrl, $baseApiUrl) {
        $this.BaseUrl = $baseUrl
        $this.BaseApiUrl = $baseApiUrl
    }

    hidden $Project
    hidden $Lifecycle
    [void] SetProject($projectName) {
        $this.Project = Invoke-OctopusApi "$($this.BaseApiUrl)/projects/all" | Where-Object Name -eq $projectName
        if ($null -eq $this.Project) {
            Write-Fatal "Project $projectName not found"
        }
        Write-Host "Project: $($this.Project.Name)"
        Write-Verbose "`t$($this.BaseUrl)$(Format-LinksUri -Uri $this.Project.Links.Self)"
        
        $this.Lifecycle = Invoke-OctopusApi ("$($this.BaseApiUrl)/lifecycles/{0}" -f $this.Project.LifecycleId)
        Write-Host "Project Lifecycle: $($this.Lifecycle.Name)"
        Write-Verbose "`t$($this.BaseUrl)$(Format-LinksUri -Uri $this.Lifecycle.Links.Self)"
    }
    
    hidden $Channel
    [void] SetChannel($channelName) {
        $useDefaultChannel = Test-String $channelName -ForAbsence
        $this.Channel = Invoke-OctopusApi (Format-LinksUri -Uri $this.Project.Links.Channels) | ForEach-Object Items | Where-Object { $useDefaultChannel -and $_.IsDefault -or $_.Name -eq $channelName }
        if ($null -eq $this.Channel) {
            Write-Fatal "$(if ($useDefaultChannel) { 'Default channel' } else { "Channel $channelName" }) not found"
        }
        Write-Host "Channel: $($this.Channel.Name)"
        Write-Verbose "`t$($this.BaseUrl)$(Format-LinksUri -Uri $this.Channel.Links.Self)"

        if ($null -ne $this.Channel.LifecycleId) {
            $this.Lifecycle = Invoke-OctopusApi ("$($this.BaseApiUrl)/lifecycles/{0}" -f $this.Channel.LifecycleId)
            Write-Host "Channel Lifecycle: $($this.Lifecycle.Name)"
            Write-Verbose "`t$($this.BaseUrl)$(Format-LinksUri -Uri $this.Lifecycle.Links.Self)"        
        }
    }

    hidden $Release
    [void] SetRelease($releaseVersion) {
        if (Test-String $releaseVersion) {
            $this.Release = Invoke-OctopusApi ("$($this.BaseApiUrl)/projects/{0}/releases/{1}" -f $this.Project.Id, $releaseVersion) -GetErrorResponse
            if ($null -ne $this.Release.ErrorMessage) {
                Write-Fatal $this.Release.ErrorMessage
            }
        }
        else {
            $this.Release = Invoke-OctopusApi (Format-LinksUri -Uri $this.Channel.Links.Releases) | ForEach-Object Items | Select-Object -First 1
            if ($null -eq $this.Release) {
                Write-Fatal "There are no releases for channel $($this.Channel.Name)"
            }
        }
        Write-Host "Release: $($this.Release.Version)"
        Write-Verbose "`t$($this.BaseUrl)$($this.BaseApiUrl)/releases/$($this.Release.Id)"
    }

    [void] CreateRelease($releaseVersion) {
        $template = Invoke-OctopusApi ('{0}/template?channel={1}' -f (Format-LinksUri -Uri $this.Project.Links.DeploymentProcess), $this.Channel.Id)
        $selectedPackages = @()
        Write-Host 'Resolving package versions...'
        $template.Packages | ForEach-Object {
            $preReleaseTag = $this.Channel.Rules | Where-Object Actions -contains $_.StepName | Where-Object { $null -ne $_ } | ForEach-Object { '&preReleaseTag={0}' -f $([System.Net.WebUtility]::UrlEncode($_.Tag)) }
            $versionRange = $this.Channel.Rules | Where-Object Actions -contains $_.StepName | Where-Object { $null -ne $_ } | ForEach-Object { '&versionRange={0}' -f $([System.Net.WebUtility]::UrlEncode($_.VersionRange)) }

            $package = Invoke-OctopusApi ("$($this.BaseApiUrl)/feeds/{0}/packages?packageId={1}&partialMatch=false&includeMultipleVersions=false&includeNotes=false&includePreRelease=true&take=1{2}{3}" -f $_.FeedId, $_.PackageId, $preReleaseTag, $versionRange)
            $packageDesc = "$($package.Title) @ $($package.Version) for step $($_.StepName)"
            if ( $_.PackageReferenceName ) {
                $packageDesc += "/$($_.PackageReferenceName)"
            }
            Write-Host "Found $packageDesc"
            
            $selectedPackages += @{
                StepName             = $_.StepName
                ActionName           = $_.ActionName
                PackageReferenceName = $_.PackageReferenceName
                Version              = $package.Version
            }

            if ( (Test-String $releaseVersion -ForAbsence) -and ($_.StepName -eq $template.VersioningPackageStepName) ) {
                Write-Host "Release will be created using the version number from package step $($template.VersioningPackageStepName): $($package.Version)"
                $releaseVersion = $package.Version
            }
        }
        if (Test-String $releaseVersion) {
            $this.Release = Invoke-OctopusApi ("$($this.BaseApiUrl)/projects/{0}/releases/{1}" -f $this.Project.Id, $releaseVersion) -GetErrorResponse
            if ( ($null -eq $this.Release.ErrorMessage) -and ($this.Release.Version -ieq $releaseVersion) -and ($this.Release.ChannelId -eq $this.Channel.Id) ) {
                Write-Host "Release version $($this.Release.Version) has already been created, selecting it for deployment"
                Write-Verbose "`t$($this.BaseUrl)$($this.BaseApiUrl)/releases/$($this.Release.Id)"
                return
            }
        }
        else {
            Write-Host "Release will be created using the incremented release version: $($template.NextVersionIncrement)"
            $releaseVersion = $template.NextVersionIncrement
        }

        $this.Release = Invoke-OctopusApi "$($this.BaseApiUrl)/releases?ignoreChannelRules=false" -Method Post -Body @{
            ProjectId        = $this.Project.Id
            ChannelId        = $this.Channel.Id 
            Version          = $releaseVersion
            SelectedPackages = $selectedPackages
        } -GetErrorResponse
        if ($null -ne $this.Release.ErrorMessage) {
            Write-Fatal "$($this.Release.ErrorMessage)`n$($this.Release.Errors -join "`n")"
        }
        Write-Host "Release $($this.Release.Version) has been successfully created"
        Write-Verbose "`t$($this.BaseUrl)$($this.BaseApiUrl)/releases/$($this.Release.Id)"
    }

    [void] UpdateVariableSnapshot() {
        $this.Release = Invoke-OctopusApi (Format-LinksUri -Uri $this.Release.Links.SnapshotVariables) -Method Post
        Write-Host 'Variables snapshot update performed. The release now references the latest variables.'
    }

    hidden $DeploymentTemplate
    [void] GetDeploymentTemplate() {
        Write-Host 'Getting deployment template for release...'
        $this.DeploymentTemplate = Invoke-OctopusApi (Format-LinksUri -Uri $this.Release.Links.DeploymentTemplate)
    }

    hidden [bool]$UseGuidedFailure
    hidden [string[]]$GuidedFailureActions
    hidden [string]$GuidedFailureMessage
    hidden [int]$DeploymentRetryCount
    [void] SetGuidedFailure([GuidedFailure]$guidedFailure, $guidedFailureMessage) {
        $this.UseGuidedFailure = switch ($guidedFailure) {
            ([GuidedFailure]::Default) { [System.Convert]::ToBoolean($global:OctopusUseGuidedFailure) }
            ([GuidedFailure]::Enabled) { $true }
            ([GuidedFailure]::Disabled) { $false }
            ([GuidedFailure]::RetryIgnore) { $true }
            ([GuidedFailure]::RetryAbort) { $true }
            ([GuidedFailure]::Ignore) { $true } 
            ([GuidedFailure]::RetryDeployment) { $false }
        }
        Write-Host "Setting Guided Failure: $($this.UseGuidedFailure)"
        
        $retryActions = @(1..(Get-OctopusSetting StepRetryCount 1) | ForEach-Object { 'Retry' })
        $this.GuidedFailureActions = switch ($guidedFailure) {
            ([GuidedFailure]::Default) { $null }
            ([GuidedFailure]::Enabled) { $null }
            ([GuidedFailure]::Disabled) { $null }
            ([GuidedFailure]::RetryIgnore) { $retryActions + @('Ignore') }
            ([GuidedFailure]::RetryAbort) { $retryActions + @('Abort') }
            ([GuidedFailure]::Ignore) { @('Ignore') }
            ([GuidedFailure]::RetryDeployment) { $null }
        }
        if ($null -ne $this.GuidedFailureActions) {
            Write-Host "Automated Failure Guidance: $($this.GuidedFailureActions -join '; ') "
        }
        $this.GuidedFailureMessage = $guidedFailureMessage
        
        $defaultRetries = if ($guidedFailure -eq [GuidedFailure]::RetryDeployment) { 1 } else { 0 }
        $this.DeploymentRetryCount = Get-OctopusSetting DeploymentRetryCount $defaultRetries
        if ($this.DeploymentRetryCount -ne 0) {
            Write-Host "Failed Deployments will be retried #$($this.DeploymentRetryCount) times"
        }
    }

    [bool]$ForcePackageDownload
    [void] SetForcePackageDownload($forcePackageDownload) {
        if ($forcePackageDownload -eq $true) {
            $this.ForcePackageDownload = $true
            Write-Host 'Deployment will Force Package Download...'
            return
        } 
        $this.ForcePackageDownload = $false
        Write-Host 'Deployment will not Force Package Download.'
        return

    }

    [bool]$WaitForDeployment
    hidden [datetime]$QueueTime
    hidden [datetime]$QueueTimeExpiry
    [void] SetSchedule($deploySchedule) {
        if (Test-String $deploySchedule -ForAbsence) {
            Write-Fatal 'The deployment schedule step parameter was not found.'
        }
        if ($deploySchedule -eq 'WaitForDeployment') {
            $this.WaitForDeployment = $true
            Write-Host 'Deployment will be queued to start immediatley...'
            return
        }
        $this.WaitForDeployment = $false
        if ($deploySchedule -eq 'NoWait') {
            Write-Host 'Deployment will be queued to start immediatley...'
            return
        }
        <#
            ^(?i) - Case-insensitive matching
            (?:
                (?<Day>MON|TUE|WED|THU|FRI|SAT|SUN)? - Capture an optional day
                \s*@\s* - '@' indicates deploying at a specific time
                (?<TimeOfDay>(?:[01]?[0-9]|2[0-3]):[0-5][0-9]) - Captures the time of day, in 24 hour format
            )? - Day & TimeOfDay are optional
            \s*
            (?:
                \+\s* - '+' indicates deploying after a length of tie
                (?<TimeSpan>
                    \d{1,3} - Match 1 to 3 digits
                    (?::[0-5][0-9])? - Optionally match a colon and 00 to 59, this denotes if the previous 1-3 digits are hours or minutes
                )
            )?$ - TimeSpan is optional
        #>
        $parsedSchedule = [regex]::Match($deploySchedule, '^(?i)(?:(?<Day>MON|TUE|WED|THU|FRI|SAT|SUN)?\s*@\s*(?<TimeOfDay>(?:[01]?[0-9]|2[0-3]):[0-5][0-9]))?\s*(?:\+\s*(?<TimeSpan>\d{1,3}(?::[0-5][0-9])?))?$')
        if (!$parsedSchedule.Success) {
            Write-Fatal "The deployment schedule step parameter contains an invalid value. Valid values are 'WaitForDeployment', 'NoWait' or a schedule in the format '[[DayOfWeek] @ HH:mm] [+ <MMM|HHH:MM>]'" 
        }
        $this.QueueTime = Get-Date
        if ($parsedSchedule.Groups['Day'].Success) {
            Write-Verbose "Parsed Day: $($parsedSchedule.Groups['Day'].Value)"
            while (!$this.QueueTime.DayOfWeek.ToString().StartsWith($parsedSchedule.Groups['Day'].Value)) {
                $this.QueueTime = $this.QueueTime.AddDays(1)
            }
        }
        if ($parsedSchedule.Groups['TimeOfDay'].Success) {
            Write-Verbose "Parsed Time Of Day: $($parsedSchedule.Groups['TimeOfDay'].Value)"
            $timeOfDay = [datetime]::ParseExact($parsedSchedule.Groups['TimeOfDay'].Value, 'HH:mm', $null)
            $this.QueueTime = $this.QueueTime.Date + $timeOfDay.TimeOfDay
        }
        if ($parsedSchedule.Groups['TimeSpan'].Success) {
            Write-Verbose "Parsed Time Span: $($parsedSchedule.Groups['TimeSpan'].Value)"
            $timeSpan = $parsedSchedule.Groups['TimeSpan'].Value.Split(':')
            $hoursToAdd = if ($timeSpan.Length -eq 2) { $timeSpan[0] } else { 0 }
            $minutesToAdd = if ($timeSpan.Length -eq 2) { $timeSpan[1] } else { $timeSpan[0] }
            $this.QueueTime = $this.QueueTime.Add((New-TimeSpan -Hours $hoursToAdd -Minutes $minutesToAdd))
        }
        Write-Host "Deployment will be queued to start at: $($this.QueueTime.ToLongDateString()) $($this.QueueTime.ToLongTimeString())"
        Write-Verbose "Local Time: $($this.QueueTime.ToLocalTime().ToString('r'))"
        Write-Verbose "Universal Time: $($this.QueueTime.ToUniversalTime().ToString('o'))"
        $this.QueueTimeExpiry = $this.QueueTime.Add([timespan]::ParseExact((Get-OctopusSetting QueueTimeout '00:30'), "hh\:mm", $null))
        Write-Verbose "Queued deployment will expire on: $($this.QueueTimeExpiry.ToUniversalTime().ToString('o'))"
    }

    hidden $Environments
    [void] SetEnvironment($environmentName) {
        $lifecyclePhaseEnvironments = $this.Lifecycle.Phases | Where-Object Name -eq $environmentName | ForEach-Object {
            $_.AutomaticDeploymentTargets
            $_.OptionalDeploymentTargets
        }
        $this.Environments = $this.DeploymentTemplate.PromoteTo | Where-Object { $_.Id -in $lifecyclePhaseEnvironments -or $_.Name -ieq $environmentName }
        if ($null -eq $this.Environments) {
            Write-Fatal "The specified environment ($environmentName) was not found or not eligible for deployment of the release ($($this.Release.Version)). Verify that the release has been deployed to all required environments before it can be promoted to this environment. Once you have corrected these problems you can try again." 
        }
        Write-Host "Environments: $(($this.Environments | ForEach-Object Name) -join ', ')"
    }
    
    [bool] $IsTenanted
    hidden $Tenants
    [void] SetTenants($tenantFilter) {
        $this.IsTenanted = Test-String $tenantFilter
        if (!$this.IsTenanted) {
            return
        }
        $tenantPromotions = $this.DeploymentTemplate.TenantPromotions | ForEach-Object Id
        $this.Tenants = $tenantFilter.Split("`n") | ForEach-Object { [uri]::EscapeUriString($_.Trim()) } | ForEach-Object {
            $criteria = if ($_ -like '*/*') { 'tags' } else { 'name' }
            
            $tenantResults = Invoke-OctopusApi ("$($this.BaseApiUrl)/tenants/all?projectId={0}&{1}={2}" -f $this.Project.Id, $criteria, $_) -GetErrorResponse
            if ($tenantResults -isnot [array] -and $tenantResults.ErrorMessage) {
                Write-Warning "Full Exception: $($tenantResults.FullException)"
                Write-Fatal $tenantResults.ErrorMessage
            }
            $tenantResults
        } | Where-Object Id -in $tenantPromotions

        if ($null -eq $this.Tenants) {
            Write-Fatal "No eligible tenants found for deployment of the release ($($this.Release.Version)). Verify that the tenants have been associated with the project."
        }
        Write-Host "Tenants: $(($this.Tenants | ForEach-Object Name) -join ', ')"
    }

    [DeploymentController[]] GetDeploymentControllers() {
        Write-Verbose 'Determining eligible environments & tenants. Retrieving deployment previews...'
        $deploymentControllers = @()
        foreach ($environment in $this.Environments) {
            $envPrefix = if ($this.Environments.Count -gt 1) { $environment.Name }
            if ($this.IsTenanted) {
                foreach ($tenant in $this.Tenants) {
                    $tenantPrefix = if ($this.Tenants.Count -gt 1) { $tenant.Name }
                    if ($this.DeploymentTemplate.TenantPromotions | Where-Object Id -eq $tenant.Id | ForEach-Object PromoteTo | Where-Object Id -eq $environment.Id) {
                        $logPrefix = ($envPrefix, $tenantPrefix | Where-Object { $null -ne $_ }) -join '::'
                        $deploymentControllers += [DeploymentController]::new($this, $logPrefix, $environment, $tenant)
                    }
                }
            }
            else {
                $deploymentControllers += [DeploymentController]::new($this, $envPrefix, $environment, $null)
            }
        }
        return $deploymentControllers
    }
}

class DeploymentController {
    hidden [string]$BaseUrl
    hidden [DeploymentContext]$DeploymentContext
    hidden [string]$LogPrefix
    hidden [object]$Environment
    hidden [object]$Tenant
    hidden [object]$DeploymentPreview
    hidden [int]$DeploymentRetryCount
    hidden [int]$DeploymentAttempt
    
    DeploymentController($deploymentContext, $logPrefix, $environment, $tenant) {
        $this.BaseUrl = $deploymentContext.BaseUrl
        $this.DeploymentContext = $deploymentContext
        if (Test-String $logPrefix) {
            $this.LogPrefix = "[${logPrefix}] "
        }
        $this.Environment = $environment
        $this.Tenant = $tenant
        if ($tenant) {
            $this.DeploymentPreview = Invoke-OctopusApi ("$($this.DeploymentContext.BaseApiUrl)/releases/{0}/deployments/preview/{1}/{2}" -f $this.DeploymentContext.Release.Id, $this.Environment.Id, $this.Tenant.Id)
        }
        else {
            $this.DeploymentPreview = Invoke-OctopusApi ("$($this.DeploymentContext.BaseApiUrl)/releases/{0}/deployments/preview/{1}" -f $this.DeploymentContext.Release.Id, $this.Environment.Id)
        }
        $this.DeploymentRetryCount = $deploymentContext.DeploymentRetryCount
        $this.DeploymentAttempt = 0
    }

    hidden [string[]]$SkipActions = @()
    [void] SetStepsToSkip($stepsToSkip) {
        $comparisonArray = $stepsToSkip.Split("`n") | ForEach-Object Trim
        $this.SkipActions = $this.DeploymentPreview.StepsToExecute | Where-Object {
            $_.CanBeSkipped -and ($_.ActionName -in $comparisonArray -or $_.ActionNumber -in $comparisonArray)
        } | ForEach-Object {
            $logMessage = "Skipping Step $($_.ActionNumber): $($_.ActionName)"
            if ($this.LogPrefix) { Write-Verbose "$($this.LogPrefix)$logMessage" }
            else { Write-Host $logMessage }
            $_.ActionId
        }
    }


    hidden [string[]]$SpecificMachineIds
    [void] SetSpecificMachineIds($specificMachineNames) {
        $this.SpecificMachineIds = @()
        $specificMachineNames.Split("`n") | ForEach-Object {
            Write-Host "Translating $_ to an Id. First checking to see if it is already an Id."
            if ($_.Trim().StartsWith("Machines-")) {
                Write-Host "$_ is already an Id, no need to look that up."
                $this.SpecificMachineIds += $_.Trim()
                continue
            }
            $itemNameToFind = $_.Trim()
            Write-Host "Attempting to find Deployment Target with the name of $itemNameToFind"
            $itemList = Invoke-OctopusApi ("$($this.DeploymentContext.BaseApiUrl)/machines/?partialName=$([uri]::EscapeDataString($itemNameToFind))&skip=0&take=100" ) -GetErrorResponse
            $machineObject = Get-FilteredOctopusItem -itemList $itemList -itemName $itemNameToFind
            Write-Host "Successfully found $itemNameToFind with id of $($machineObject.Id)"
            $this.SpecificMachineIds += $machineObject.Id
        }
    }

    hidden [hashtable]$FormValues
    [void] SetFormValues($formValuesToSet) {
        $this.FormValues = @{}
        $this.DeploymentPreview.Form.Values | Get-Member -MemberType NoteProperty | ForEach-Object {
            $this.FormValues.Add($_.Name, $this.DeploymentPreview.Form.Values.$($_.Name))
        }

        $formValuesToSet.Split("`n") | ForEach-Object {
            $entry = $_.Split('=') | ForEach-Object Trim
            $entryName, $entryValues = $entry
            $entry = @($entryName, $($entryValues -join "="))
            $this.DeploymentPreview.Form.Elements | Where-Object { $_.Control.Name -ieq $entry[0] } | ForEach-Object {
                $logMessage = "Setting Form Value '$($_.Control.Label)' to: $($entry[1])"
                if ($this.LogPrefix) { Write-Verbose "$($this.LogPrefix)$logMessage" }
                else { Write-Host $logMessage }
                $this.FormValues[$_.Name] = $entry[1]
            }
        }
    }
	
    [ServerTask]$Task
    [void] Start() {
        $request = @{
            ReleaseId        = $this.DeploymentContext.Release.Id
            EnvironmentId    = $this.Environment.Id
            SkipActions      = $this.SkipActions
            FormValues       = $this.FormValues
            SpecificMachineIds = $this.SpecificMachineIds
            ForcePackageDownload = $this.DeploymentContext.ForcePackageDownload
            UseGuidedFailure = $this.DeploymentContext.UseGuidedFailure
        }
        if ($this.DeploymentContext.QueueTime -ne [datetime]::MinValue) { $request.Add('QueueTime', $this.DeploymentContext.QueueTime.ToUniversalTime().ToString('o')) }
        if ($this.DeploymentContext.QueueTimeExpiry -ne [datetime]::MinValue) { $request.Add('QueueTimeExpiry', $this.DeploymentContext.QueueTimeExpiry.ToUniversalTime().ToString('o')) }
        if ($this.Tenant) { $request.Add('TenantId', $this.Tenant.Id) }

        $deployment = Invoke-OctopusApi "$($this.DeploymentContext.BaseApiUrl)/deployments" -Method Post -Body $request -GetErrorResponse
        if ($deployment.ErrorMessage) { Write-Fatal "$($deployment.ErrorMessage)`n$($deployment.Errors -join "`n")" }
        Write-Host "Queued $($deployment.Name)..."
        Write-Host "`t$($this.BaseUrl)$(Format-WebLinksUri -Uri $deployment.Links.Web)"
        Write-Verbose "`t$($this.BaseUrl)$(Format-LinksUri -Uri $deployment.Links.Self)"
        Write-Verbose "`t$($this.BaseUrl)$($this.DeploymentContext.BaseApiUrl)/deploymentprocesses/$($deployment.DeploymentProcessId)"
        Write-Verbose "`t$($this.BaseUrl)$(Format-LinksUri -Uri $deployment.Links.Variables)"
        Write-Verbose "`t$($this.BaseUrl)$(Format-LinksUri -Uri $deployment.Links.Task)/details"

        $this.Task = [ServerTask]::new($this.DeploymentContext, $deployment, $this.LogPrefix)
    }

    [bool] PollCheck() {
        $this.Task.Poll()
        if ($this.Task.IsCompleted -and !$this.Task.FinishedSuccessfully -and $this.DeploymentAttempt -lt $this.DeploymentRetryCount) {
            $retryWaitPeriod = New-TimeSpan -Seconds (Get-OctopusSetting RetryWaitPeriod 0)
            $waitText = if ($retryWaitPeriod.TotalSeconds -gt 0) {
                $minutesText = if ($retryWaitPeriod.Minutes -gt 1) { " $($retryWaitPeriod.Minutes) minutes" } elseif ($retryWaitPeriod.Minutes -eq 1) { " $($retryWaitPeriod.Minutes) minute" }
                $secondsText = if ($retryWaitPeriod.Seconds -gt 1) { " $($retryWaitPeriod.Seconds) seconds" } elseif ($retryWaitPeriod.Seconds -eq 1) { " $($retryWaitPeriod.Seconds) second" }
                "Waiting${minutesText}${secondsText} before "
            }
            $this.DeploymentAttempt++
            Write-Error "$($this.LogPrefix)Deployment failed. ${waitText}Queuing retry #$($this.DeploymentAttempt) of $($this.DeploymentRetryCount)..."
            if ($retryWaitPeriod.TotalSeconds -gt 0) {
                Start-Sleep -Seconds $retryWaitPeriod.TotalSeconds
            }
            $this.Start()
            return $true
        }
        return !$this.Task.IsCompleted
    }
}

class ServerTask {
    hidden [DeploymentContext]$DeploymentContext
    hidden [object]$Deployment
    hidden [string]$LogPrefix

    hidden [bool] $IsCompleted = $false
    hidden [bool] $FinishedSuccessfully
    hidden [string] $ErrorMessage
    
    hidden [int]$PollCount = 0
    hidden [bool]$HasInterruptions = $false
    hidden [hashtable]$State = @{}
    hidden [System.Collections.Generic.HashSet[string]]$Logs
 
    ServerTask($deploymentContext, $deployment, $logPrefix) {
        $this.DeploymentContext = $deploymentContext
        $this.Deployment = $deployment
        $this.LogPrefix = $logPrefix
        $this.Logs = [System.Collections.Generic.HashSet[string]]::new()
    }
    
    [void] Poll() {	
        if ($this.IsCompleted) { return }

        $details = Invoke-OctopusApi ("$($this.DeploymentContext.BaseApiUrl)/tasks/{0}/details?verbose=false&tail=30" -f $this.Deployment.TaskId)
        $this.IsCompleted = $details.Task.IsCompleted
        $this.FinishedSuccessfully = $details.Task.FinishedSuccessfully
        $this.ErrorMessage = $details.Task.ErrorMessage

        $this.PollCount++
        if ($this.PollCount % 10 -eq 0) {
            $this.Verbose("$($details.Task.State). $($details.Task.Duration), $($details.Progress.EstimatedTimeRemaining)")
        }
        
        if ($details.Task.HasPendingInterruptions) { $this.HasInterruptions = $true }
        $this.LogQueuePosition($details.Task)
        $activityLogs = $this.FlattenActivityLogs($details.ActivityLogs)    
        $this.WriteLogMessages($activityLogs)
    }

    hidden [bool] IfNewState($firstKey, $secondKey, $value) {
        $key = '{0}/{1}' -f $firstKey, $secondKey
        $containsKey = $this.State.ContainsKey($key)
        if ($containsKey) { return $false }
        $this.State[$key] = $value
        return $true
    }

    hidden [bool] HasChangedState($firstKey, $secondKey, $value) {
        $key = '{0}/{1}' -f $firstKey, $secondKey
        $hasChanged = if (!$this.State.ContainsKey($key)) { $true } else { $this.State[$key] -ne $value }
        if ($hasChanged) {
            $this.State[$key] = $value
        }
        return $hasChanged
    }

    hidden [object] GetState($firstKey, $secondKey) { return $this.State[('{0}/{1}' -f $firstKey, $secondKey)] }

    hidden [void] ResetState($firstKey, $secondKey) { $this.State.Remove(('{0}/{1}' -f $firstKey, $secondKey)) }

    hidden [void] Error($message) { Write-Error "$($this.LogPrefix)${message}" }
    hidden [void] Warn($message) { Write-Warning "$($this.LogPrefix)${message}" }
    hidden [void] Host($message) { Write-Host "$($this.LogPrefix)${message}" }   
    hidden [void] Verbose($message) { Write-Verbose "$($this.LogPrefix)${message}" }

    hidden [psobject[]] FlattenActivityLogs($ActivityLogs) {
        $flattenedActivityLogs = { @() }.Invoke()
        $this.FlattenActivityLogs($ActivityLogs, $null, $flattenedActivityLogs)
        return $flattenedActivityLogs
    }

    hidden [void] FlattenActivityLogs($ActivityLogs, $Parent, $flattenedActivityLogs) {
        foreach ($log in $ActivityLogs) {
            $log | Add-Member -MemberType NoteProperty -Name Parent -Value $Parent
            $insertBefore = $null -eq $log.Parent -and $log.Status -eq 'Running'	
            if ($insertBefore) { $flattenedActivityLogs.Add($log) }
            foreach ($childLog in $log.Children) {
                $this.FlattenActivityLogs($childLog, $log, $flattenedActivityLogs)
            }
            if (!$insertBefore) { $flattenedActivityLogs.Add($log) }
        }
    }

    hidden [void] LogQueuePosition($Task) {
        if ($Task.HasBeenPickedUpByProcessor) {
            $this.ResetState($Task.Id, 'QueuePosition')
            return
        }
		
        $queuePosition = (Invoke-OctopusApi ("$($this.DeploymentContext.BaseApiUrl)/tasks/{0}/queued-behind" -f $this.Deployment.TaskId)).Items.Count
        if ($this.HasChangedState($Task.Id, 'QueuePosition', $queuePosition) -and $queuePosition -ne 0) {
            $this.Host("Queued behind $queuePosition tasks...")
        }
    }

    hidden [void] WriteLogMessages($ActivityLogs) {
        $interrupts = if ($this.HasInterruptions) {
            Invoke-OctopusApi ("$($this.DeploymentContext.BaseApiUrl)/interruptions?regarding={0}" -f $this.Deployment.TaskId) | ForEach-Object Items
        }
        foreach ($activity in $ActivityLogs) {
            $correlatedInterrupts = $interrupts | Where-Object CorrelationId -eq $activity.Id         
            $correlatedInterrupts | Where-Object IsPending -eq $false | ForEach-Object { $this.LogInterruptMessages($activity, $_) }

            $this.LogStepTransition($activity)         
            $this.LogErrorsAndWarnings($activity)
            $correlatedInterrupts | Where-Object IsPending -eq $true | ForEach-Object { 
                $this.LogInterruptMessages($activity, $_)
                $this.HandleInterrupt($_)
            }
        }
    }

    hidden [void] LogStepTransition($ActivityLog) {
        if ($ActivityLog.ShowAtSummaryLevel -and $ActivityLog.Status -ne 'Pending') {
            $existingState = $this.GetState($ActivityLog.Id, 'Status')
            if ($this.HasChangedState($ActivityLog.Id, 'Status', $ActivityLog.Status)) {
                $existingStateText = if ($existingState) { "$existingState -> " }
                $this.Host("$($ActivityLog.Name) ($existingStateText$($ActivityLog.Status))")
            }
        }
    }

    hidden [void] LogErrorsAndWarnings($ActivityLog) {
        foreach ($logEntry in $ActivityLog.LogElements) {
            if ($logEntry.Category -eq 'Info') { continue }
            if ($this.Logs.Add(($ActivityLog.Id, $logEntry.OccurredAt, $logEntry.MessageText -join '/'))) {
                switch ($logEntry.Category) {
                    'Fatal' {
                        if ($ActivityLog.Parent) {
                            $this.Error("FATAL: During $($ActivityLog.Parent.Name)")
                            $this.Error("FATAL: $($logEntry.MessageText)")
                        }
                    }
                    'Error' { $this.Error("[$($ActivityLog.Parent.Name)] $($logEntry.MessageText)") }
                    'Warning' { $this.Warn("[$($ActivityLog.Parent.Name)] $($logEntry.MessageText)") }
                }
            }
        }
    }

    hidden [void] LogInterruptMessages($ActivityLog, $Interrupt) {
        $message = $Interrupt.Form.Elements | Where-Object Name -eq Instructions | ForEach-Object Control | ForEach-Object Text
        if ($Interrupt.IsPending -and $this.HasChangedState($Interrupt.Id, $ActivityLog.Parent.Name, $message)) {
            $this.Warn("Deployment is paused at '$($ActivityLog.Parent.Name)' for manual intervention: $message")
        }
        if ($null -ne $Interrupt.ResponsibleUserId -and $this.HasChangedState($Interrupt.Id, 'ResponsibleUserId', $Interrupt.ResponsibleUserId)) {
            $user = Invoke-OctopusApi (Format-LinksUri -Uri $Interrupt.Links.User)
            $emailText = if (Test-String $user.EmailAddress) { " ($($user.EmailAddress))" }
            $this.Warn("$($user.DisplayName)$emailText has taken responsibility for the manual intervention")
        }
        $manualAction = $Interrupt.Form.Values.Result
        if ((Test-String $manualAction) -and $this.HasChangedState($Interrupt.Id, 'Action', $manualAction)) {
            $this.Warn("Manual intervention action '$manualAction' submitted with notes: $($Interrupt.Form.Values.Notes)")
        }
        $guidanceAction = $Interrupt.Form.Values.Guidance
        if ((Test-String $guidanceAction) -and $this.HasChangedState($Interrupt.Id, 'Action', $guidanceAction)) {
            $this.Warn("Failure guidance to '$guidanceAction' submitted with notes: $($Interrupt.Form.Values.Notes)")
        }
    }

    hidden [void] HandleInterrupt($Interrupt) {
        $isGuidedFailure = $null -ne ($Interrupt.Form.Elements | Where-Object Name -eq Guidance)
        if (!$isGuidedFailure -or !$this.DeploymentContext.GuidedFailureActions -or !$Interrupt.IsPending) {
            return
        }
        $this.IfNewState($Interrupt.CorrelationId, 'ActionIndex', 0)
        if ($Interrupt.CanTakeResponsibility -and $null -eq $Interrupt.ResponsibleUserId) {
            Invoke-OctopusApi (Format-LinksUri -Uri $Interrupt.Links.Responsible) -Method Put
        }
        if ($Interrupt.HasResponsibility) {
            $guidanceIndex = $this.GetState($Interrupt.CorrelationId, 'ActionIndex')
            $guidance = $this.DeploymentContext.GuidedFailureActions[$guidanceIndex]
            $guidanceIndex++
            
            $retryWaitPeriod = New-TimeSpan -Seconds (Get-OctopusSetting RetryWaitPeriod 0)
            if ($guidance -eq 'Retry' -and $retryWaitPeriod.TotalSeconds -gt 0) {
                $minutesText = if ($retryWaitPeriod.Minutes -gt 1) { " $($retryWaitPeriod.Minutes) minutes" } elseif ($retryWaitPeriod.Minutes -eq 1) { " $($retryWaitPeriod.Minutes) minute" }
                $secondsText = if ($retryWaitPeriod.Seconds -gt 1) { " $($retryWaitPeriod.Seconds) seconds" } elseif ($retryWaitPeriod.Seconds -eq 1) { " $($retryWaitPeriod.Seconds) second" }
                $this.Warn("Waiting${minutesText}${secondsText} before submitting retry failure guidance...")
                Start-Sleep -Seconds $retryWaitPeriod.TotalSeconds
            }
            Invoke-OctopusApi (Format-LinksUri -Uri $Interrupt.Links.Submit) -Body @{
                Notes    = $this.DeploymentContext.GuidedFailureMessage.Replace('#{GuidedFailureActionIndex}', $guidanceIndex).Replace('#{GuidedFailureAction}', $guidance)
                Guidance = $guidance
            } -Method Post

            $this.HasChangedState($Interrupt.CorrelationId, 'ActionIndex', $guidanceIndex)
        }
    }
}

function Show-Heading {
    param($Text)
    $padding = ' ' * ((80 - 2 - $Text.Length) / 2)
    Write-Host " `n"
    Write-Host (@("`t", ([string][char]0x2554), (([string][char]0x2550) * 80), ([string][char]0x2557)) -join '')
    Write-Host "`t$(([string][char]0x2551))$padding $Text $padding$([string][char]0x2551)"  
    Write-Host (@("`t", ([string][char]0x255A), (([string][char]0x2550) * 80), ([string][char]0x255D)) -join '')
    Write-Host " `n"
}

if ($OctopusParameters['Octopus.Action.RunOnServer'] -ieq 'False') {
    Write-Warning "For optimal performance use 'Run On Server' for this action"
}

$deploymentContext = [DeploymentContext]::new($Chain_BaseUrl, $Chain_BaseApiUrl)

if ($Chain_CreateOption -ieq 'True') {
    Show-Heading 'Creating Release'
}
else {
    Show-Heading 'Retrieving Release'
}
$deploymentContext.SetProject($Chain_ProjectName)
$deploymentContext.SetChannel($Chain_Channel)
Write-Host "`t$Chain_BaseUrl$(Format-WebLinksUri -Uri $deploymentContext.Project.Links.Web)"

if ($Chain_CreateOption -ieq 'True') {
    $deploymentContext.CreateRelease($Chain_ReleaseNum)
}
else {
    $deploymentContext.SetRelease($Chain_ReleaseNum)
}
Write-Host "`t$Chain_BaseUrl$(Format-WebLinksUri -Uri $deploymentContext.Release.Links.Web)"
if ($Chain_SnapshotVariables -ieq 'True') {
    $deploymentContext.UpdateVariableSnapshot()
}


Show-Heading 'Configuring Deployment'
$deploymentContext.GetDeploymentTemplate()
$email = if (Test-String $OctopusParameters['Octopus.Deployment.CreatedBy.EmailAddress']) { "($($OctopusParameters['Octopus.Deployment.CreatedBy.EmailAddress']))" }
$guidedFailureMessage = Get-OctopusSetting GuidedFailureMessage @"
Automatic Failure Guidance will #{GuidedFailureAction} (Failure ###{GuidedFailureActionIndex})
Initiated by $($OctopusParameters['Octopus.Deployment.Name']) of $($OctopusParameters['Octopus.Project.Name']) release $($OctopusParameters['Octopus.Release.Number'])
Created By: $($OctopusParameters['Octopus.Deployment.CreatedBy.DisplayName']) $email
${Chain_BaseUrl}$($OctopusParameters['Octopus.Web.DeploymentLink'])
"@
$deploymentContext.SetGuidedFailure($Chain_GuidedFailure, $guidedFailureMessage)
$deploymentContext.SetSchedule($Chain_DeploySchedule)

$deploymentContext.SetEnvironment($Chain_DeployTo)
$deploymentContext.SetTenants($Chain_Tenants)
$deploymentContext.SetForcePackageDownload($Chain_ForcePackageDownload)
$deploymentControllers = $deploymentContext.GetDeploymentControllers()
if (Test-String $Chain_StepsToSkip) {
    $deploymentControllers | ForEach-Object { $_.SetStepsToSkip($Chain_StepsToSkip) }
}
if (Test-String $Chain_FormValues) {
    $deploymentControllers | ForEach-Object { $_.SetFormValues($Chain_FormValues) }
}

if (Test-String $Chain_MachineList) {
    $deploymentControllers | ForEach-Object { $_.SetSpecificMachineIds($Chain_MachineList) }
}

Show-Heading 'Queue Deployment'
if ($deploymentContext.IsTenanted) {
    Write-Host 'Queueing tenant deployments...'
}
else {
    Write-Host 'Queueing untenanted deployment...'
}
$deploymentControllers | ForEach-Object Start

if (!$deploymentContext.WaitForDeployment) {
    Write-Host 'Deployments have been queued, proceeding to the next step...'
    return
}

Show-Heading 'Waiting For Deployment'
do {
    Start-Sleep -Seconds 1
    $tasksStillRunning = $false
    foreach ($deployment in $deploymentControllers) {
        if ($deployment.PollCheck()) {
            $tasksStillRunning = $true
        }
    }
} while ($tasksStillRunning)

if ($deploymentControllers | ForEach-Object Task | Where-Object FinishedSuccessfully -eq $false) {
    Show-Heading 'Deployment Failed!'
    Write-Fatal (($deploymentControllers | ForEach-Object Task | ForEach-Object ErrorMessage) -join "`n")
}
else {
    Show-Heading 'Deployment Successful!'
}

if (Test-String $Chain_PostDeploy -ForAbsence) {
    return 
}

Show-Heading 'Post-Deploy Script'
$rawPostDeployScript = Invoke-OctopusApi ("$Chain_BaseApiUrl/releases/{0}" -f $OctopusParameters['Octopus.Release.Id']) |
ForEach-Object { Invoke-OctopusApi (Format-LinksUri -Uri $_.Links.ProjectDeploymentProcessSnapshot) } |
ForEach-Object Steps | Where-Object Id -eq $OctopusParameters['Octopus.Step.Id'] |
ForEach-Object Actions | Where-Object Id -eq $OctopusParameters['Octopus.Action.Id'] |
ForEach-Object { $_.Properties.Chain_PostDeploy }
Write-Verbose "Raw Post-Deploy Script:`n$rawPostDeployScript"

Add-Type -Path (Get-WmiObject Win32_Process | Where-Object ProcessId -eq $PID | ForEach-Object { Get-Process -Id $_.ParentProcessId } | ForEach-Object { Join-Path (Split-Path -Path $_.Path -Parent) 'Octostache.dll' })

$deploymentControllers | ForEach-Object {
    $deployment = $_.Task.Deployment
    $tenant = $_.Tenant
    $variablesDictionary = [Octostache.VariableDictionary]::new()
    Invoke-OctopusApi ("$Chain_BaseApiUrl/variables/{0}" -f $deployment.ManifestVariableSetId) | ForEach-Object Variables | Where-Object {
        ($_.IsSensitive -eq $false) -and `
        ($_.Scope.Private -ne 'True') -and `
        ($null -eq $_.Scope.Action) -and `
        ($null -eq $_.Scope.Machine) -and `
        ($null -eq $_.Scope.TargetRole) -and `
        ($null -eq $_.Scope.Role) -and `
        ($null -eq $_.Scope.Tenant -or $_.Scope.Tenant -contains $tenant.Id) -and `
        ($null -eq $_.Scope.TenantTag -or (Compare-Object $_.Scope.TenantTag $tenant.TenantTags -ExcludeDifferent -IncludeEqual)) -and `
        ($null -eq $_.Scope.Environment -or $_.Scope.Environment -contains $deployment.EnvironmentId) -and `
        ($null -eq $_.Scope.Channel -or $_.Scope.Channel -contains $deployment.ChannelId) -and `
        ($null -eq $_.Scope.Project -or $_.Scope.Project -contains $deployment.ProjectId)
    } | ForEach-Object { $variablesDictionary.Set($_.Name, $_.Value) }
    $postDeployScript = $variablesDictionary.Evaluate($rawPostDeployScript)
    Write-Host "$($_.LogPrefix)Evaluated Post-Deploy Script:"
    Write-Host $postDeployScript
    Write-Host 'Script output:'
    [scriptblock]::Create($postDeployScript).Invoke()
}

Provided under the Apache License version 2.0.

Report an issue

To use this template in Octopus Deploy, copy the JSON below and paste it into the Library → Step templates → Import dialog.

{
  "Id": "18392835-d50e-4ce9-9065-8e15a3c30954",
  "Name": "Chain Deployment",
  "Description": "Triggers a deployment of another project in Octopus",
  "Version": 26,
  "ExportedAt": "2023-02-23T21:40:33.279Z",
  "ActionType": "Octopus.Script",
  "Author": "LeoDOD",
  "Packages": [],
  "Parameters": [
    {
      "Id": "61bffab9-bb89-4107-a5e0-79d69eaf8f2a",
      "Name": "Chain_ApiKey",
      "Label": "API Key",
      "HelpText": "An Octopus API Key with appropriate permissions to perform the deployment",
      "DefaultValue": "",
      "DisplaySettings": {
        "Octopus.ControlType": "Sensitive"
      },
      "Links": {}
    },
    {
      "Id": "a37cac4d-8fd3-4d58-bfda-45a436be8dd5",
      "Name": "Chain_ProjectName",
      "Label": "Project Name",
      "HelpText": "Name of the Octopus project that should be deployed",
      "DefaultValue": "",
      "DisplaySettings": {
        "Octopus.ControlType": "SingleLineText"
      },
      "Links": {}
    },
    {
      "Id": "4fd440af-70fe-41ca-bec3-074f05155e81",
      "Name": "Chain_Channel",
      "Label": "Channel Name",
      "HelpText": "The project channel to use when finding or [creating](https://octopus.com/docs/releases/channels#Channels-CreatingReleases) the release to deploy\n\n_Leave blank to use the default channel_",
      "DefaultValue": "",
      "DisplaySettings": {
        "Octopus.ControlType": "SingleLineText"
      },
      "Links": {}
    },
    {
      "Id": "78739052-438d-4dc7-862a-d4567eafc5df",
      "Name": "Chain_ReleaseNum",
      "Label": "Release Number",
      "HelpText": "Release number to use for the deployment\n\n_Leave blank to use the latest release in the channel_",
      "DefaultValue": "",
      "DisplaySettings": {
        "Octopus.ControlType": "SingleLineText"
      },
      "Links": {}
    },
    {
      "Id": "fd2c3474-7187-4356-aaec-96f4910bb9c5",
      "Name": "Chain_CreateOption",
      "Label": "Create new release?",
      "HelpText": "If a release should be created as part of this deployment\n\n\nThe release is created using either the **Release Number** if specified, or from the project release version template / donor package step if not specified\n\nA release will not be created if it is found to already exist",
      "DefaultValue": "",
      "DisplaySettings": {
        "Octopus.ControlType": "Checkbox"
      },
      "Links": {}
    },
    {
      "Id": "f648fa0c-b271-4e4a-b4f2-7a88db6b605c",
      "Name": "Chain_SnapshotVariables",
      "Label": "Update Variable Snapshot?",
      "HelpText": "Should variables in the release be updated before deploying?\n\nBy updating the variables, the current snapshot will be discarded, and the latest variables (as seen on the Variables tab) will be imported",
      "DefaultValue": "False",
      "DisplaySettings": {
        "Octopus.ControlType": "Checkbox"
      },
      "Links": {}
    },
    {
      "Id": "80634b3b-3171-4643-b164-a5077c6d387b",
      "Name": "Chain_DeployTo",
      "Label": "Environment Name",
      "HelpText": "The name of an environment to deploy to\n\nMultiple environments can be deployed to by entering the name of a [lifecycle phase](https://octopus.com/docs/key-concepts/lifecycles#Lifecycles-LifecyclePhases)",
      "DefaultValue": "#{Octopus.Environment.Name}",
      "DisplaySettings": {
        "Octopus.ControlType": "SingleLineText"
      },
      "Links": {}
    },
    {
      "Id": "1334093d-0be4-4115-bb93-d752171a19d8",
      "Name": "Chain_Tenants",
      "Label": "Tenants",
      "HelpText": "_Leave blank to perform an untenanted deployment_\n\nA list of [tenants & tenant tags](https://octopus.com/docs/tenants) to deploy. Tenant Tags are specified in [Canonical Name](https://octopus.com/docs/tenants/tenant-tags#referencing-tenant-tags) format:\n\n    Tag Set Name/Tag Name\n\nIndividual tenants can be listed in addition to tags\n\n    Tenant Name",
      "DefaultValue": "",
      "DisplaySettings": {
        "Octopus.ControlType": "MultiLineText"
      },
      "Links": {}
    },
    {
      "Id": "83d9d973-7a72-4f71-a890-8f19d955bc37",
      "Name": "Chain_FormValues",
      "Label": "Form Values",
      "HelpText": "Provide values for [prompted variables](https://octopus.com/docs/projects/variables/prompted-variables) to use in the deployment\n\nVariables should be listed one per line using the format\n\n    VariableName = Value",
      "DefaultValue": "",
      "DisplaySettings": {
        "Octopus.ControlType": "MultiLineText"
      },
      "Links": {}
    },
    {
      "Id": "6522ca29-898a-4da6-b0c3-da52991e6812",
      "Name": "Chain_StepsToSkip",
      "Label": "Steps To Skip",
      "HelpText": "A list of steps which should be skipped in the deployment\n\nSteps should be listed one per line, and specified using either the step number (as per the deployment plan) or by the step name",
      "DefaultValue": "",
      "DisplaySettings": {
        "Octopus.ControlType": "MultiLineText"
      },
      "Links": {}
    },
    {
      "Id": "acb2cd0e-fc53-42a7-95da-089955ea1870",
      "Name": "Chain_GuidedFailure",
      "Label": "Failure Handling",
      "HelpText": "Determines how deployment failures & [guided failure mode](https://octopus.com/docs/releases/guided-failures) should be handled for the deployment\n\nAutomatic failure handling is performed by using [guided failure](https://octopus.com/docs/releases/guided-failures) and submitting an appropriate action\n\nThe number of retry attempts can be customised by [setting the following variables in indexer notion](https://octopus.com/docs/deploying-applications/variables/system-variables#Systemvariables-Action)\n- Octopus.Action.StepRetryCount\n- Octopus.Action.DeploymentRetryCount",
      "DefaultValue": "Default",
      "DisplaySettings": {
        "Octopus.ControlType": "Select",
        "Octopus.SelectOptions": "Default|Default - Guided Failure is inherited from this deployment\nEnabled|Enable - Guided Failure is enabled\nDisabled|Disable - Guided Failure is disabled\nRetryIgnore|Retry & Ignore - Automatically retry a failing step, a second failure is ignored\nRetryAbort|Retry & Abort - Automatically retry a failing step, and abort on a second failure\nIgnore|Ignore - Automatically ignore any step failures\nRetryDeployment|Retry Deployment - Automatically retry the entire deployment on failure"
      },
      "Links": {}
    },
    {
      "Id": "73a80735-4ca0-4c12-9fa3-f0123db6349f",
      "Name": "Chain_DeploySchedule",
      "Label": "Scheduling",
      "HelpText": "Defines how the deployment should be scheduled & run\n\n_Note: Automated failure handling & post-deploy script functionality is only available when the **Wait For Deployment** option is selected_\n\nA user defined schedule can be set with the **Use a custom expression** option\nFor an exact date & time use the following format, the day is optional and the time is in 24-hour format\n\n    [Mon/Tue/Wed/Thu/Fri/Sat/Sun] @ HH:MM\n\nTo schedule a deployment a relative number of hours & minutes in the future use\n\n    + MMM\n    + HHH:MM\n\n_Note: Reoccurring deployments & automatic retry of failed deployment are possible using a scheduled deployment and the [Always Run or On Failure run conditions](https://octopus.com/docs/deploying-applications#Deployingapplications-Conditions)_",
      "DefaultValue": "WaitForDeployment",
      "DisplaySettings": {
        "Octopus.ControlType": "Select",
        "Octopus.SelectOptions": "WaitForDeployment|Wait For Deployment\nNoWait|Queue Immediately\n+ 5|Deploy in 5 minutes\n+ 15|Deploy in 15 minutes\n+ 1:00|Deploy in 1 hour\n+ 24:00|Deploy in 24 hours\n@ 00:00|Deploy At Midnight\n@ 00:00 + 12:00|Deploy At Noon Tomorrow\nMon @ 08:00|Deploy At 8am On Monday\nSat @ 00:00 + 168:00|Deploy the following Saturday at Midnight"
      },
      "Links": {}
    },
    {
      "Id": "7e7f9ac5-8674-4a91-a94a-896a3ee1334d",
      "Name": "Chain_PostDeploy",
      "Label": "Post-Deploy Script",
      "HelpText": "A PowerShell script which should be run after a successful deployment\n\nVariables are replaced in the script using the resultant **Manifest VariableSet** from the deployment in the [binding syntax](https://octopus.com/docs/projects/variables/variable-substitutions#binding-variables) format\n\nVariables are not available if they are:\n- [Sensitive](https://octopus.com/docs/deploying-applications/variables/sensitive-variables)\n- Action scoped\n- Machine scoped\n- Role scoped\n\n\nWhen performing a tenanted deployment the script will be run once for each tenant using the specific variables from their deployment",
      "DefaultValue": "",
      "DisplaySettings": {
        "Octopus.ControlType": "MultiLineText"
      },
      "Links": {}
    },
    {
      "Id": "5c3fddc3-69cb-4762-ac6a-4fdb05f43c6b",
      "Name": "Chain_ForcePackageDownload",
      "Label": "Force Package Download",
      "HelpText": "Should we redownload the package for this release?",
      "DefaultValue": "False",
      "DisplaySettings": {
        "Octopus.ControlType": "Checkbox"
      }
    },
    {
      "Id": "9ee151bb-78e5-4cb0-8780-6536ea319934",
      "Name": "Chain_MachineList",
      "Label": "Machine List",
      "HelpText": "A list of Machine Names which should be Targeted in the deployment\n\nMachine Names should be listed one per line and specified using either the Machine Id or by the Machine name",
      "DefaultValue": "",
      "DisplaySettings": {
        "Octopus.ControlType": "MultiLineText"
      }
    }
  ],
  "Properties": {
    "Octopus.Action.Script.Syntax": "PowerShell",
    "Octopus.Action.Script.ScriptSource": "Inline",
    "Octopus.Action.RunOnServer": "true",
    "Octopus.Action.Script.ScriptBody": "<#\n----- Chain Deployment -----\nAuthors & Credits\n    Paul Marston @paulmarsy (paul@marston.me)\n    Joe Waid @joewaid\n    Henrik Andersson @alfhenrik\n    Damian Brady @Damovisa\n    Aaron Burke @aburke-incomm (aburke@incomm.com)\n    Bob Hindy @bstr413\n    Leo De Oliveira Dias @LeoDOD\nLinks\n    https://library.octopus.com/step-templates/18392835-d50e-4ce9-9065-8e15a3c30954\n    https://github.com/OctopusDeploy/Library/commits/master/step-templates/octopus-chain-deployment.json\n\n----- Advanced Configuration Settings -----\nVariable names can use either of the following two formats: \n    Octopus.Action.<Setting Name> - will apply to all steps in the deployment, e.g.\n        Octopus.Action.DebugLogging\n    Octopus.Action[Step Name].<Setting Name> - will apply to 'step name' alone, e.g.\n        Octopus.Action[Provision Virtual Machine].DeploymentRetryCount\n\nAvailable Settings:\n    - DebugLogging - set to 'True' or 'False' to log all GET web requests\n    - GuidedFailureMessage - will change the note used when submitting guided failure actions, the following variables will be replaced in the text:\n        #{GuidedFailureActionIndex} - The current count of interrupts for that step e.g. 1\n        #{GuidedFailureAction} - The action being submitted by the step e.g. Retry\n    - DeploymentRetryCount - will override the number of times a deployment will be retried when unsuccessful and enable retrying when the failure option is set for a different option, default is 1\n    - StepRetryCount - will override the number of times a deployment step will be retried before before submitting Ignore or Abort, default is 1\n    - RetryWaitPeriod - an additional delay in seconds wait before retrying a failed step/deployment, default is 0\n    - QueueTimeout - when scheduling a deployment for later a timeout must be provided, this allows a custom value, default is 30:00, format is hh:mm\n    - OctopusServerUrl - will override the base url used for all webrequests, making it possible to chain deployments on a different Octopus instance/server, or as a workaround for misconfigured node settings\n\n----- Changelog -----\n25. Feb 9, 2023 - Bob Hindy @bstr413\n\t- Fixed issue caused by version 23 where script would not work with on premise Octopus servers. (Reverted most of the changes 60ae653 and d614a2d made to this step template.)\n24. Sept 13, 2021 - Mark Harrison @harrisonmeister\n    - Fixed issue where the Invoke-OctopusApi function would error with 404: NotFound when running Chain deployment on an Octopus instance \n      that runs under either a \"virtual directory\" / route prefix other than the route e.g https://my.octopus.app/octo/\n23. Aug 23rd, 2021 - Ben Macpherson benjimac93\n    - Use Octopus.Web.ServerUri in place of Octopus.Web.BaseUrl if present.\n22. Dec 31, 2020 - Josh Slaughter @joshgk00\n\t- Fixed an issue where the script was unable to create a release if Chained project contained a step with multiple package references\n20. Sept 3, 2020 - Mark Harrison @harrisonmeister\n\t- Included setting to TLS 1.2. \n19. July 17, 2020 - Aaron Burke @aburke-incomm\n\t- Update script handle Regex for Channel Tags in the CreateRelease Function\n17. December 18, 2018 - Jim Burger @burgomg\n\t- Added Spaces compatibility\n16. November 22, 2018 - Patrick Kearney @patrickkearney\n    - Fixed an issue where the step was unable to pass a form variable containing an \"=\" in the value.\n15. July 17, 2017 - Robert Glickman @robertglickman\n    - Fixed an issue where the step would fail in Octopus 3.15+ due to templated URIs not being handled\n14. May 5, 2017 - Paul Marston @paulmarsy (paul@marston.me)\n    - Improved step parameter metadata & validation\n    - Added changelog, documentation of advanced settings\n    - Supports deploying to multiple environments in one step by specifying a lifecycle phase name e.g. 'Dev'\n    - Automated retry of the entire deployment as an additional failure handling option\n    - Number of step/deployment retries is configurable using a settings variable\n    - Supports Octopus scheduled deployments (can be used for reoccuring scheduled deploys, or autonomous deployment retry)\n    - Individual tenants as well as tenant tags can be deployed to\n    - Fixing a bug where Guided Failure is always evaluated to true\n    - Improved identification of valid environment&tenant promotions by using the 'deployment template' api\n    - If a release version has already been created, it will be used rather than erroring trying to recreate it\n    - Using 'Fail-Step' for better error logging\n    - Fixed a bug where log messages with an identical timestamp were repeatedly reported\n    - Added an option to wait before retrying a step/deployment\n    - A release's channel is taken into account when checking if an existing release version can be used\n13. Apr 21, 2017 - Paul Marston @paulmarsy (paul@marston.me)\n    - Complete step template rewrite\n    - Improved logging\n        * Logs only written when chained deployment changes\n        * Progress of deployment step states is reported\n        * Errors & warnings are reported without interpretation in parent deployment\n        * Manual intervention & guided failure events are reported\n        * Queue position reported before deployment starts\n        * Verbose logging of useful API urls\n    - Multi-tenancy support and handling multiple tenant deploys from one chain step\n    - Support for skipping steps\n    - Support for prompted form variables\n    - Create release functionality supports using the version from the incremented version template or donor package\n    - Ability to snapshot update variables of a release before deploying\n    - Automated handling of guided failure scenarios e.g. retry on step failure, then abort if it errors a second time\n    - Transient Octopus API request failures are handled (e.g. we saw many deployments failing because of a request timeout)\n    - Post-deploy script support with variable substitution performed using the manifest variable set of the chained deployment with appropriate scoping applied (though not advanced scope specificity)\n    - Defaulting channel to a blank value which looks for one with 'IsDefault' set true\n    - Create release performs a simplified package version lookup to populate the 'SelectedPackages' field\n12. Mar 30, 2017 - Joe Waid @joewaid\n    - Pass the Environments \"Guided Failure\" setting\n    - Check status after deployment when Chain_WaitForDeployment is true\n11. Nov 21, 2016 - Henrik Andersson @alfhenrik\n    - Add Wait for deployment option to chain deployment step template\n10. May 2, 2016 - Damian Brady @Damovisa\n    - Add Chained Deployment step template\n#>\n#Requires -Version 5\n$ErrorActionPreference = 'Stop'\n$ProgressPreference = 'SilentlyContinue'\n$DefaultUrl = $OctopusParameters['Octopus.Web.BaseUrl']\n$Chain_BaseApiUrl = \"/api\"\n\n[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12\n\nfunction Test-String {\n    param([Parameter(Position = 0)]$InputObject, [switch]$ForAbsence)\n\n    $hasNoValue = [System.String]::IsNullOrWhiteSpace($InputObject)\n    if ($ForAbsence) { $hasNoValue }\n    else { -not $hasNoValue }\n}\n\nfunction Get-OctopusSetting {\n    param([Parameter(Position = 0, Mandatory)][string]$Name, [Parameter(Position = 1, Mandatory)]$DefaultValue)\n    $formattedName = 'Octopus.Action.{0}' -f $Name\n    if ($OctopusParameters.ContainsKey($formattedName)) {\n        $value = $OctopusParameters[$formattedName]\n        if ($DefaultValue -is [int]) { return ([int]::Parse($value)) }\n        if ($DefaultValue -is [bool]) { return ([System.Convert]::ToBoolean($value)) }\n        if ($DefaultValue -is [array] -or $DefaultValue -is [hashtable] -or $DefaultValue -is [pscustomobject]) { return (ConvertFrom-Json -InputObject $value) }\n        return $value\n    }\n    else { return $DefaultValue }\n}\n\n# Write functions are re-defined using octopus service messages to preserve formatting of log messages received from the chained deployment and avoid errors being twice wrapped in an ErrorRecord\nfunction Write-Fatal($message, $exitCode = -1) {\n    if (Test-Path Function:\\Fail-Step) {\n        Fail-Step $message\n    }\n    else {\n        Write-Host (\"##octopus[stdout-error]`n{0}\" -f $message)\n        Exit $exitCode\n    }\n}\nfunction Write-Error($message) { Write-Host (\"##octopus[stdout-error]`n{0}`n##octopus[stdout-default]\" -f $message) }\nfunction Write-Warning($message) { Write-Host (\"##octopus[stdout-warning]`n{0}`n##octopus[stdout-default]\" -f $message) }\nfunction Write-Verbose($message) { Write-Host (\"##octopus[stdout-verbose]`n{0}`n##octopus[stdout-default]\" -f $message) }\n\n# Use \"Octopus.Web.ServerUri\" if it is available\nif ([string]::IsNullOrWhiteSpace($OctopusParameters['Octopus.Web.ServerUri']) -eq $False) {\n    $DefaultUrl = $OctopusParameters['Octopus.Web.ServerUri']\n}\n\n$Chain_BaseUrl = (Get-OctopusSetting OctopusServerUrl $DefaultUrl).Trim('/')\nif (Test-String $Chain_ApiKey -ForAbsence) {\n    Write-Fatal \"The step parameter 'API Key' was not found. This step requires an API Key to function, please provide one and try again.\"\n}\n$DebugLogging = Get-OctopusSetting DebugLogging $false\n\n# Replace any \"virtual directory\" or route prefix e.g from the Links collection used\n# with the api e.g. /api\nfunction Format-LinksUri {\n    param(\n        [Parameter(Position = 0, Mandatory)]\n        $Uri\n    )\n    $Uri = $Uri -replace '.*/api', '/api'\n    Return $Uri\n}\n# Replace any \"virtual directory\" or route prefix e.g from the Links collection used\n# with the web app e.g. /app\nfunction Format-WebLinksUri {\n    param(\n        [Parameter(Position = 0, Mandatory)]\n        $Uri\n    )\n    $Uri = $Uri -replace '.*/app', '/app'\n    Return $Uri\n}\n\nfunction Invoke-OctopusApi {\n    param(\n        [Parameter(Position = 0, Mandatory)]$Uri,\n        [ValidateSet('Get', 'Post', 'Put')]$Method = 'Get',\n        $Body,\n        [switch]$GetErrorResponse\n    )\n    # Replace query string example parameters e.g. {?skip,take,partialName} \n    # Replace any \"virtual directory\" or route prefix e.g from the Links collection.\n    $Uri = $Uri -replace '{.*?}', '' -replace '.*/api', '/api'\n    $requestParameters = @{\n        Uri             = ('{0}/{1}' -f $Chain_BaseUrl, $Uri.TrimStart('/'))\n        Method          = $Method\n        Headers         = @{ 'X-Octopus-ApiKey' = $Chain_ApiKey }\n        UseBasicParsing = $true\n    }\n    if ($Method -ne 'Get' -or $DebugLogging) {\n        Write-Verbose ('{0} {1}' -f $Method.ToUpperInvariant(), $requestParameters.Uri)\n    }\n    if ($null -ne $Body) {\n        $requestParameters.Add('Body', (ConvertTo-Json -InputObject $Body -Depth 10))\n        Write-Verbose $requestParameters.Body\n    }\n    \n    $wait = 0\n    $webRequest = $null\n    while ($null -eq $webRequest) {\t\n        try {\n            $webRequest = Invoke-WebRequest @requestParameters\n        }\n        catch {\n            if ($_.Exception -is [System.Net.WebException] -and $null -ne $_.Exception.Response) {\n                $errorResponse = [System.IO.StreamReader]::new($_.Exception.Response.GetResponseStream()).ReadToEnd()\n                Write-Verbose (\"Error Response:`n{0}\" -f $errorResponse)\n                if ($GetErrorResponse) {\n                    return ($errorResponse | ConvertFrom-Json)\n                }\n                if ($_.Exception.Response.StatusCode -in @([System.Net.HttpStatusCode]::NotFound, [System.Net.HttpStatusCode]::InternalServerError, [System.Net.HttpStatusCode]::BadRequest, [System.Net.HttpStatusCode]::Unauthorized)) {\n                    Write-Fatal $_.Exception.Message\n                }\n            }\n            if ($wait -eq 120) {\n                Write-Fatal (\"Octopus web request ({0}: {1}) failed & the maximum number of retries has been exceeded:`n{2}\" -f $Method.ToUpperInvariant(), $requestParameters.Uri, $_.Exception.Message) -43\n            }\n            $wait = switch ($wait) {\n                0 { 30 }\n                30 { 60 }\n                60 { 120 }\n            }\n            Write-Warning (\"Octopus web request ({0}: {1}) failed & will be retried in $wait seconds:`n{2}\" -f $Method.ToUpperInvariant(), $requestParameters.Uri, $_.Exception.Message)\n            Start-Sleep -Seconds $wait\n        }\n    }\n    $webRequest.Content | ConvertFrom-Json | Write-Output\n}\n\nfunction Get-FilteredOctopusItem {\n    param(\n        $itemList,\n        $itemName\n    )\n\n    if ($itemList.Items.Count -eq 0) {\n        Write-Fatal \"Unable to find $itemName.  Exiting with an exit code of 1.\"\n        Exit 1\n    }  \n\n    $item = $itemList.Items | Where-Object { $_.Name -eq $itemName }      \n\n    if ($null -eq $item) {\n        Write-Fatal \"Unable to find $itemName.  Exiting with an exit code of 1.\"\n        exit 1\n    }\n    \n    if ($item -is [array]) {\n        Write-Fatal \"More than one item exists with the name $itemName.  Exiting with an exit code of 1.\"\n        exit 1\n    }\n\n    return $item\n}\n\nfunction Test-SpacesApi {\n    Write-Verbose \"Checking API compatibility\";\n    $rootDocument = Invoke-OctopusApi \"api/\";\n    if ($null -ne $rootDocument.Links -and $null -ne $rootDocument.Links.Spaces) {\n        Write-Verbose \"Spaces API found\"\n        return $true;\n    }\n    Write-Verbose \"Pre-spaces API found\"\n    return $false;\n}\n\nif (Test-SpacesApi) {\n    $spaceId = $OctopusParameters['Octopus.Space.Id'];\n    if ([string]::IsNullOrWhiteSpace($spaceId)) {\n        throw \"This step needs to be run in a context that provides a value for the 'Octopus.Space.Id' system variable. In this case, we received a blank value, which isn't expected - please reach out to our support team at https://help.octopus.com if you encounter this error.\";\n    }\n    $Chain_BaseApiUrl = \"/api/$spaceId\" ;\n}\n\nenum GuidedFailure {\n    Default\n    Enabled\n    Disabled\n    RetryIgnore\n    RetryAbort\n    Ignore\n    RetryDeployment\n}\n\nclass DeploymentContext {\n    hidden $BaseUrl\n    hidden $BaseApiUrl\n    DeploymentContext($baseUrl, $baseApiUrl) {\n        $this.BaseUrl = $baseUrl\n        $this.BaseApiUrl = $baseApiUrl\n    }\n\n    hidden $Project\n    hidden $Lifecycle\n    [void] SetProject($projectName) {\n        $this.Project = Invoke-OctopusApi \"$($this.BaseApiUrl)/projects/all\" | Where-Object Name -eq $projectName\n        if ($null -eq $this.Project) {\n            Write-Fatal \"Project $projectName not found\"\n        }\n        Write-Host \"Project: $($this.Project.Name)\"\n        Write-Verbose \"`t$($this.BaseUrl)$(Format-LinksUri -Uri $this.Project.Links.Self)\"\n        \n        $this.Lifecycle = Invoke-OctopusApi (\"$($this.BaseApiUrl)/lifecycles/{0}\" -f $this.Project.LifecycleId)\n        Write-Host \"Project Lifecycle: $($this.Lifecycle.Name)\"\n        Write-Verbose \"`t$($this.BaseUrl)$(Format-LinksUri -Uri $this.Lifecycle.Links.Self)\"\n    }\n    \n    hidden $Channel\n    [void] SetChannel($channelName) {\n        $useDefaultChannel = Test-String $channelName -ForAbsence\n        $this.Channel = Invoke-OctopusApi (Format-LinksUri -Uri $this.Project.Links.Channels) | ForEach-Object Items | Where-Object { $useDefaultChannel -and $_.IsDefault -or $_.Name -eq $channelName }\n        if ($null -eq $this.Channel) {\n            Write-Fatal \"$(if ($useDefaultChannel) { 'Default channel' } else { \"Channel $channelName\" }) not found\"\n        }\n        Write-Host \"Channel: $($this.Channel.Name)\"\n        Write-Verbose \"`t$($this.BaseUrl)$(Format-LinksUri -Uri $this.Channel.Links.Self)\"\n\n        if ($null -ne $this.Channel.LifecycleId) {\n            $this.Lifecycle = Invoke-OctopusApi (\"$($this.BaseApiUrl)/lifecycles/{0}\" -f $this.Channel.LifecycleId)\n            Write-Host \"Channel Lifecycle: $($this.Lifecycle.Name)\"\n            Write-Verbose \"`t$($this.BaseUrl)$(Format-LinksUri -Uri $this.Lifecycle.Links.Self)\"        \n        }\n    }\n\n    hidden $Release\n    [void] SetRelease($releaseVersion) {\n        if (Test-String $releaseVersion) {\n            $this.Release = Invoke-OctopusApi (\"$($this.BaseApiUrl)/projects/{0}/releases/{1}\" -f $this.Project.Id, $releaseVersion) -GetErrorResponse\n            if ($null -ne $this.Release.ErrorMessage) {\n                Write-Fatal $this.Release.ErrorMessage\n            }\n        }\n        else {\n            $this.Release = Invoke-OctopusApi (Format-LinksUri -Uri $this.Channel.Links.Releases) | ForEach-Object Items | Select-Object -First 1\n            if ($null -eq $this.Release) {\n                Write-Fatal \"There are no releases for channel $($this.Channel.Name)\"\n            }\n        }\n        Write-Host \"Release: $($this.Release.Version)\"\n        Write-Verbose \"`t$($this.BaseUrl)$($this.BaseApiUrl)/releases/$($this.Release.Id)\"\n    }\n\n    [void] CreateRelease($releaseVersion) {\n        $template = Invoke-OctopusApi ('{0}/template?channel={1}' -f (Format-LinksUri -Uri $this.Project.Links.DeploymentProcess), $this.Channel.Id)\n        $selectedPackages = @()\n        Write-Host 'Resolving package versions...'\n        $template.Packages | ForEach-Object {\n            $preReleaseTag = $this.Channel.Rules | Where-Object Actions -contains $_.StepName | Where-Object { $null -ne $_ } | ForEach-Object { '&preReleaseTag={0}' -f $([System.Net.WebUtility]::UrlEncode($_.Tag)) }\n            $versionRange = $this.Channel.Rules | Where-Object Actions -contains $_.StepName | Where-Object { $null -ne $_ } | ForEach-Object { '&versionRange={0}' -f $([System.Net.WebUtility]::UrlEncode($_.VersionRange)) }\n\n            $package = Invoke-OctopusApi (\"$($this.BaseApiUrl)/feeds/{0}/packages?packageId={1}&partialMatch=false&includeMultipleVersions=false&includeNotes=false&includePreRelease=true&take=1{2}{3}\" -f $_.FeedId, $_.PackageId, $preReleaseTag, $versionRange)\n            $packageDesc = \"$($package.Title) @ $($package.Version) for step $($_.StepName)\"\n            if ( $_.PackageReferenceName ) {\n                $packageDesc += \"/$($_.PackageReferenceName)\"\n            }\n            Write-Host \"Found $packageDesc\"\n            \n            $selectedPackages += @{\n                StepName             = $_.StepName\n                ActionName           = $_.ActionName\n                PackageReferenceName = $_.PackageReferenceName\n                Version              = $package.Version\n            }\n\n            if ( (Test-String $releaseVersion -ForAbsence) -and ($_.StepName -eq $template.VersioningPackageStepName) ) {\n                Write-Host \"Release will be created using the version number from package step $($template.VersioningPackageStepName): $($package.Version)\"\n                $releaseVersion = $package.Version\n            }\n        }\n        if (Test-String $releaseVersion) {\n            $this.Release = Invoke-OctopusApi (\"$($this.BaseApiUrl)/projects/{0}/releases/{1}\" -f $this.Project.Id, $releaseVersion) -GetErrorResponse\n            if ( ($null -eq $this.Release.ErrorMessage) -and ($this.Release.Version -ieq $releaseVersion) -and ($this.Release.ChannelId -eq $this.Channel.Id) ) {\n                Write-Host \"Release version $($this.Release.Version) has already been created, selecting it for deployment\"\n                Write-Verbose \"`t$($this.BaseUrl)$($this.BaseApiUrl)/releases/$($this.Release.Id)\"\n                return\n            }\n        }\n        else {\n            Write-Host \"Release will be created using the incremented release version: $($template.NextVersionIncrement)\"\n            $releaseVersion = $template.NextVersionIncrement\n        }\n\n        $this.Release = Invoke-OctopusApi \"$($this.BaseApiUrl)/releases?ignoreChannelRules=false\" -Method Post -Body @{\n            ProjectId        = $this.Project.Id\n            ChannelId        = $this.Channel.Id \n            Version          = $releaseVersion\n            SelectedPackages = $selectedPackages\n        } -GetErrorResponse\n        if ($null -ne $this.Release.ErrorMessage) {\n            Write-Fatal \"$($this.Release.ErrorMessage)`n$($this.Release.Errors -join \"`n\")\"\n        }\n        Write-Host \"Release $($this.Release.Version) has been successfully created\"\n        Write-Verbose \"`t$($this.BaseUrl)$($this.BaseApiUrl)/releases/$($this.Release.Id)\"\n    }\n\n    [void] UpdateVariableSnapshot() {\n        $this.Release = Invoke-OctopusApi (Format-LinksUri -Uri $this.Release.Links.SnapshotVariables) -Method Post\n        Write-Host 'Variables snapshot update performed. The release now references the latest variables.'\n    }\n\n    hidden $DeploymentTemplate\n    [void] GetDeploymentTemplate() {\n        Write-Host 'Getting deployment template for release...'\n        $this.DeploymentTemplate = Invoke-OctopusApi (Format-LinksUri -Uri $this.Release.Links.DeploymentTemplate)\n    }\n\n    hidden [bool]$UseGuidedFailure\n    hidden [string[]]$GuidedFailureActions\n    hidden [string]$GuidedFailureMessage\n    hidden [int]$DeploymentRetryCount\n    [void] SetGuidedFailure([GuidedFailure]$guidedFailure, $guidedFailureMessage) {\n        $this.UseGuidedFailure = switch ($guidedFailure) {\n            ([GuidedFailure]::Default) { [System.Convert]::ToBoolean($global:OctopusUseGuidedFailure) }\n            ([GuidedFailure]::Enabled) { $true }\n            ([GuidedFailure]::Disabled) { $false }\n            ([GuidedFailure]::RetryIgnore) { $true }\n            ([GuidedFailure]::RetryAbort) { $true }\n            ([GuidedFailure]::Ignore) { $true } \n            ([GuidedFailure]::RetryDeployment) { $false }\n        }\n        Write-Host \"Setting Guided Failure: $($this.UseGuidedFailure)\"\n        \n        $retryActions = @(1..(Get-OctopusSetting StepRetryCount 1) | ForEach-Object { 'Retry' })\n        $this.GuidedFailureActions = switch ($guidedFailure) {\n            ([GuidedFailure]::Default) { $null }\n            ([GuidedFailure]::Enabled) { $null }\n            ([GuidedFailure]::Disabled) { $null }\n            ([GuidedFailure]::RetryIgnore) { $retryActions + @('Ignore') }\n            ([GuidedFailure]::RetryAbort) { $retryActions + @('Abort') }\n            ([GuidedFailure]::Ignore) { @('Ignore') }\n            ([GuidedFailure]::RetryDeployment) { $null }\n        }\n        if ($null -ne $this.GuidedFailureActions) {\n            Write-Host \"Automated Failure Guidance: $($this.GuidedFailureActions -join '; ') \"\n        }\n        $this.GuidedFailureMessage = $guidedFailureMessage\n        \n        $defaultRetries = if ($guidedFailure -eq [GuidedFailure]::RetryDeployment) { 1 } else { 0 }\n        $this.DeploymentRetryCount = Get-OctopusSetting DeploymentRetryCount $defaultRetries\n        if ($this.DeploymentRetryCount -ne 0) {\n            Write-Host \"Failed Deployments will be retried #$($this.DeploymentRetryCount) times\"\n        }\n    }\n\n    [bool]$ForcePackageDownload\n    [void] SetForcePackageDownload($forcePackageDownload) {\n        if ($forcePackageDownload -eq $true) {\n            $this.ForcePackageDownload = $true\n            Write-Host 'Deployment will Force Package Download...'\n            return\n        } \n        $this.ForcePackageDownload = $false\n        Write-Host 'Deployment will not Force Package Download.'\n        return\n\n    }\n\n    [bool]$WaitForDeployment\n    hidden [datetime]$QueueTime\n    hidden [datetime]$QueueTimeExpiry\n    [void] SetSchedule($deploySchedule) {\n        if (Test-String $deploySchedule -ForAbsence) {\n            Write-Fatal 'The deployment schedule step parameter was not found.'\n        }\n        if ($deploySchedule -eq 'WaitForDeployment') {\n            $this.WaitForDeployment = $true\n            Write-Host 'Deployment will be queued to start immediatley...'\n            return\n        }\n        $this.WaitForDeployment = $false\n        if ($deploySchedule -eq 'NoWait') {\n            Write-Host 'Deployment will be queued to start immediatley...'\n            return\n        }\n        <#\n            ^(?i) - Case-insensitive matching\n            (?:\n                (?<Day>MON|TUE|WED|THU|FRI|SAT|SUN)? - Capture an optional day\n                \\s*@\\s* - '@' indicates deploying at a specific time\n                (?<TimeOfDay>(?:[01]?[0-9]|2[0-3]):[0-5][0-9]) - Captures the time of day, in 24 hour format\n            )? - Day & TimeOfDay are optional\n            \\s*\n            (?:\n                \\+\\s* - '+' indicates deploying after a length of tie\n                (?<TimeSpan>\n                    \\d{1,3} - Match 1 to 3 digits\n                    (?::[0-5][0-9])? - Optionally match a colon and 00 to 59, this denotes if the previous 1-3 digits are hours or minutes\n                )\n            )?$ - TimeSpan is optional\n        #>\n        $parsedSchedule = [regex]::Match($deploySchedule, '^(?i)(?:(?<Day>MON|TUE|WED|THU|FRI|SAT|SUN)?\\s*@\\s*(?<TimeOfDay>(?:[01]?[0-9]|2[0-3]):[0-5][0-9]))?\\s*(?:\\+\\s*(?<TimeSpan>\\d{1,3}(?::[0-5][0-9])?))?$')\n        if (!$parsedSchedule.Success) {\n            Write-Fatal \"The deployment schedule step parameter contains an invalid value. Valid values are 'WaitForDeployment', 'NoWait' or a schedule in the format '[[DayOfWeek] @ HH:mm] [+ <MMM|HHH:MM>]'\" \n        }\n        $this.QueueTime = Get-Date\n        if ($parsedSchedule.Groups['Day'].Success) {\n            Write-Verbose \"Parsed Day: $($parsedSchedule.Groups['Day'].Value)\"\n            while (!$this.QueueTime.DayOfWeek.ToString().StartsWith($parsedSchedule.Groups['Day'].Value)) {\n                $this.QueueTime = $this.QueueTime.AddDays(1)\n            }\n        }\n        if ($parsedSchedule.Groups['TimeOfDay'].Success) {\n            Write-Verbose \"Parsed Time Of Day: $($parsedSchedule.Groups['TimeOfDay'].Value)\"\n            $timeOfDay = [datetime]::ParseExact($parsedSchedule.Groups['TimeOfDay'].Value, 'HH:mm', $null)\n            $this.QueueTime = $this.QueueTime.Date + $timeOfDay.TimeOfDay\n        }\n        if ($parsedSchedule.Groups['TimeSpan'].Success) {\n            Write-Verbose \"Parsed Time Span: $($parsedSchedule.Groups['TimeSpan'].Value)\"\n            $timeSpan = $parsedSchedule.Groups['TimeSpan'].Value.Split(':')\n            $hoursToAdd = if ($timeSpan.Length -eq 2) { $timeSpan[0] } else { 0 }\n            $minutesToAdd = if ($timeSpan.Length -eq 2) { $timeSpan[1] } else { $timeSpan[0] }\n            $this.QueueTime = $this.QueueTime.Add((New-TimeSpan -Hours $hoursToAdd -Minutes $minutesToAdd))\n        }\n        Write-Host \"Deployment will be queued to start at: $($this.QueueTime.ToLongDateString()) $($this.QueueTime.ToLongTimeString())\"\n        Write-Verbose \"Local Time: $($this.QueueTime.ToLocalTime().ToString('r'))\"\n        Write-Verbose \"Universal Time: $($this.QueueTime.ToUniversalTime().ToString('o'))\"\n        $this.QueueTimeExpiry = $this.QueueTime.Add([timespan]::ParseExact((Get-OctopusSetting QueueTimeout '00:30'), \"hh\\:mm\", $null))\n        Write-Verbose \"Queued deployment will expire on: $($this.QueueTimeExpiry.ToUniversalTime().ToString('o'))\"\n    }\n\n    hidden $Environments\n    [void] SetEnvironment($environmentName) {\n        $lifecyclePhaseEnvironments = $this.Lifecycle.Phases | Where-Object Name -eq $environmentName | ForEach-Object {\n            $_.AutomaticDeploymentTargets\n            $_.OptionalDeploymentTargets\n        }\n        $this.Environments = $this.DeploymentTemplate.PromoteTo | Where-Object { $_.Id -in $lifecyclePhaseEnvironments -or $_.Name -ieq $environmentName }\n        if ($null -eq $this.Environments) {\n            Write-Fatal \"The specified environment ($environmentName) was not found or not eligible for deployment of the release ($($this.Release.Version)). Verify that the release has been deployed to all required environments before it can be promoted to this environment. Once you have corrected these problems you can try again.\" \n        }\n        Write-Host \"Environments: $(($this.Environments | ForEach-Object Name) -join ', ')\"\n    }\n    \n    [bool] $IsTenanted\n    hidden $Tenants\n    [void] SetTenants($tenantFilter) {\n        $this.IsTenanted = Test-String $tenantFilter\n        if (!$this.IsTenanted) {\n            return\n        }\n        $tenantPromotions = $this.DeploymentTemplate.TenantPromotions | ForEach-Object Id\n        $this.Tenants = $tenantFilter.Split(\"`n\") | ForEach-Object { [uri]::EscapeUriString($_.Trim()) } | ForEach-Object {\n            $criteria = if ($_ -like '*/*') { 'tags' } else { 'name' }\n            \n            $tenantResults = Invoke-OctopusApi (\"$($this.BaseApiUrl)/tenants/all?projectId={0}&{1}={2}\" -f $this.Project.Id, $criteria, $_) -GetErrorResponse\n            if ($tenantResults -isnot [array] -and $tenantResults.ErrorMessage) {\n                Write-Warning \"Full Exception: $($tenantResults.FullException)\"\n                Write-Fatal $tenantResults.ErrorMessage\n            }\n            $tenantResults\n        } | Where-Object Id -in $tenantPromotions\n\n        if ($null -eq $this.Tenants) {\n            Write-Fatal \"No eligible tenants found for deployment of the release ($($this.Release.Version)). Verify that the tenants have been associated with the project.\"\n        }\n        Write-Host \"Tenants: $(($this.Tenants | ForEach-Object Name) -join ', ')\"\n    }\n\n    [DeploymentController[]] GetDeploymentControllers() {\n        Write-Verbose 'Determining eligible environments & tenants. Retrieving deployment previews...'\n        $deploymentControllers = @()\n        foreach ($environment in $this.Environments) {\n            $envPrefix = if ($this.Environments.Count -gt 1) { $environment.Name }\n            if ($this.IsTenanted) {\n                foreach ($tenant in $this.Tenants) {\n                    $tenantPrefix = if ($this.Tenants.Count -gt 1) { $tenant.Name }\n                    if ($this.DeploymentTemplate.TenantPromotions | Where-Object Id -eq $tenant.Id | ForEach-Object PromoteTo | Where-Object Id -eq $environment.Id) {\n                        $logPrefix = ($envPrefix, $tenantPrefix | Where-Object { $null -ne $_ }) -join '::'\n                        $deploymentControllers += [DeploymentController]::new($this, $logPrefix, $environment, $tenant)\n                    }\n                }\n            }\n            else {\n                $deploymentControllers += [DeploymentController]::new($this, $envPrefix, $environment, $null)\n            }\n        }\n        return $deploymentControllers\n    }\n}\n\nclass DeploymentController {\n    hidden [string]$BaseUrl\n    hidden [DeploymentContext]$DeploymentContext\n    hidden [string]$LogPrefix\n    hidden [object]$Environment\n    hidden [object]$Tenant\n    hidden [object]$DeploymentPreview\n    hidden [int]$DeploymentRetryCount\n    hidden [int]$DeploymentAttempt\n    \n    DeploymentController($deploymentContext, $logPrefix, $environment, $tenant) {\n        $this.BaseUrl = $deploymentContext.BaseUrl\n        $this.DeploymentContext = $deploymentContext\n        if (Test-String $logPrefix) {\n            $this.LogPrefix = \"[${logPrefix}] \"\n        }\n        $this.Environment = $environment\n        $this.Tenant = $tenant\n        if ($tenant) {\n            $this.DeploymentPreview = Invoke-OctopusApi (\"$($this.DeploymentContext.BaseApiUrl)/releases/{0}/deployments/preview/{1}/{2}\" -f $this.DeploymentContext.Release.Id, $this.Environment.Id, $this.Tenant.Id)\n        }\n        else {\n            $this.DeploymentPreview = Invoke-OctopusApi (\"$($this.DeploymentContext.BaseApiUrl)/releases/{0}/deployments/preview/{1}\" -f $this.DeploymentContext.Release.Id, $this.Environment.Id)\n        }\n        $this.DeploymentRetryCount = $deploymentContext.DeploymentRetryCount\n        $this.DeploymentAttempt = 0\n    }\n\n    hidden [string[]]$SkipActions = @()\n    [void] SetStepsToSkip($stepsToSkip) {\n        $comparisonArray = $stepsToSkip.Split(\"`n\") | ForEach-Object Trim\n        $this.SkipActions = $this.DeploymentPreview.StepsToExecute | Where-Object {\n            $_.CanBeSkipped -and ($_.ActionName -in $comparisonArray -or $_.ActionNumber -in $comparisonArray)\n        } | ForEach-Object {\n            $logMessage = \"Skipping Step $($_.ActionNumber): $($_.ActionName)\"\n            if ($this.LogPrefix) { Write-Verbose \"$($this.LogPrefix)$logMessage\" }\n            else { Write-Host $logMessage }\n            $_.ActionId\n        }\n    }\n\n\n    hidden [string[]]$SpecificMachineIds\n    [void] SetSpecificMachineIds($specificMachineNames) {\n        $this.SpecificMachineIds = @()\n        $specificMachineNames.Split(\"`n\") | ForEach-Object {\n            Write-Host \"Translating $_ to an Id. First checking to see if it is already an Id.\"\n            if ($_.Trim().StartsWith(\"Machines-\")) {\n                Write-Host \"$_ is already an Id, no need to look that up.\"\n                $this.SpecificMachineIds += $_.Trim()\n                continue\n            }\n            $itemNameToFind = $_.Trim()\n            Write-Host \"Attempting to find Deployment Target with the name of $itemNameToFind\"\n            $itemList = Invoke-OctopusApi (\"$($this.DeploymentContext.BaseApiUrl)/machines/?partialName=$([uri]::EscapeDataString($itemNameToFind))&skip=0&take=100\" ) -GetErrorResponse\n            $machineObject = Get-FilteredOctopusItem -itemList $itemList -itemName $itemNameToFind\n            Write-Host \"Successfully found $itemNameToFind with id of $($machineObject.Id)\"\n            $this.SpecificMachineIds += $machineObject.Id\n        }\n    }\n\n    hidden [hashtable]$FormValues\n    [void] SetFormValues($formValuesToSet) {\n        $this.FormValues = @{}\n        $this.DeploymentPreview.Form.Values | Get-Member -MemberType NoteProperty | ForEach-Object {\n            $this.FormValues.Add($_.Name, $this.DeploymentPreview.Form.Values.$($_.Name))\n        }\n\n        $formValuesToSet.Split(\"`n\") | ForEach-Object {\n            $entry = $_.Split('=') | ForEach-Object Trim\n            $entryName, $entryValues = $entry\n            $entry = @($entryName, $($entryValues -join \"=\"))\n            $this.DeploymentPreview.Form.Elements | Where-Object { $_.Control.Name -ieq $entry[0] } | ForEach-Object {\n                $logMessage = \"Setting Form Value '$($_.Control.Label)' to: $($entry[1])\"\n                if ($this.LogPrefix) { Write-Verbose \"$($this.LogPrefix)$logMessage\" }\n                else { Write-Host $logMessage }\n                $this.FormValues[$_.Name] = $entry[1]\n            }\n        }\n    }\n\t\n    [ServerTask]$Task\n    [void] Start() {\n        $request = @{\n            ReleaseId        = $this.DeploymentContext.Release.Id\n            EnvironmentId    = $this.Environment.Id\n            SkipActions      = $this.SkipActions\n            FormValues       = $this.FormValues\n            SpecificMachineIds = $this.SpecificMachineIds\n            ForcePackageDownload = $this.DeploymentContext.ForcePackageDownload\n            UseGuidedFailure = $this.DeploymentContext.UseGuidedFailure\n        }\n        if ($this.DeploymentContext.QueueTime -ne [datetime]::MinValue) { $request.Add('QueueTime', $this.DeploymentContext.QueueTime.ToUniversalTime().ToString('o')) }\n        if ($this.DeploymentContext.QueueTimeExpiry -ne [datetime]::MinValue) { $request.Add('QueueTimeExpiry', $this.DeploymentContext.QueueTimeExpiry.ToUniversalTime().ToString('o')) }\n        if ($this.Tenant) { $request.Add('TenantId', $this.Tenant.Id) }\n\n        $deployment = Invoke-OctopusApi \"$($this.DeploymentContext.BaseApiUrl)/deployments\" -Method Post -Body $request -GetErrorResponse\n        if ($deployment.ErrorMessage) { Write-Fatal \"$($deployment.ErrorMessage)`n$($deployment.Errors -join \"`n\")\" }\n        Write-Host \"Queued $($deployment.Name)...\"\n        Write-Host \"`t$($this.BaseUrl)$(Format-WebLinksUri -Uri $deployment.Links.Web)\"\n        Write-Verbose \"`t$($this.BaseUrl)$(Format-LinksUri -Uri $deployment.Links.Self)\"\n        Write-Verbose \"`t$($this.BaseUrl)$($this.DeploymentContext.BaseApiUrl)/deploymentprocesses/$($deployment.DeploymentProcessId)\"\n        Write-Verbose \"`t$($this.BaseUrl)$(Format-LinksUri -Uri $deployment.Links.Variables)\"\n        Write-Verbose \"`t$($this.BaseUrl)$(Format-LinksUri -Uri $deployment.Links.Task)/details\"\n\n        $this.Task = [ServerTask]::new($this.DeploymentContext, $deployment, $this.LogPrefix)\n    }\n\n    [bool] PollCheck() {\n        $this.Task.Poll()\n        if ($this.Task.IsCompleted -and !$this.Task.FinishedSuccessfully -and $this.DeploymentAttempt -lt $this.DeploymentRetryCount) {\n            $retryWaitPeriod = New-TimeSpan -Seconds (Get-OctopusSetting RetryWaitPeriod 0)\n            $waitText = if ($retryWaitPeriod.TotalSeconds -gt 0) {\n                $minutesText = if ($retryWaitPeriod.Minutes -gt 1) { \" $($retryWaitPeriod.Minutes) minutes\" } elseif ($retryWaitPeriod.Minutes -eq 1) { \" $($retryWaitPeriod.Minutes) minute\" }\n                $secondsText = if ($retryWaitPeriod.Seconds -gt 1) { \" $($retryWaitPeriod.Seconds) seconds\" } elseif ($retryWaitPeriod.Seconds -eq 1) { \" $($retryWaitPeriod.Seconds) second\" }\n                \"Waiting${minutesText}${secondsText} before \"\n            }\n            $this.DeploymentAttempt++\n            Write-Error \"$($this.LogPrefix)Deployment failed. ${waitText}Queuing retry #$($this.DeploymentAttempt) of $($this.DeploymentRetryCount)...\"\n            if ($retryWaitPeriod.TotalSeconds -gt 0) {\n                Start-Sleep -Seconds $retryWaitPeriod.TotalSeconds\n            }\n            $this.Start()\n            return $true\n        }\n        return !$this.Task.IsCompleted\n    }\n}\n\nclass ServerTask {\n    hidden [DeploymentContext]$DeploymentContext\n    hidden [object]$Deployment\n    hidden [string]$LogPrefix\n\n    hidden [bool] $IsCompleted = $false\n    hidden [bool] $FinishedSuccessfully\n    hidden [string] $ErrorMessage\n    \n    hidden [int]$PollCount = 0\n    hidden [bool]$HasInterruptions = $false\n    hidden [hashtable]$State = @{}\n    hidden [System.Collections.Generic.HashSet[string]]$Logs\n \n    ServerTask($deploymentContext, $deployment, $logPrefix) {\n        $this.DeploymentContext = $deploymentContext\n        $this.Deployment = $deployment\n        $this.LogPrefix = $logPrefix\n        $this.Logs = [System.Collections.Generic.HashSet[string]]::new()\n    }\n    \n    [void] Poll() {\t\n        if ($this.IsCompleted) { return }\n\n        $details = Invoke-OctopusApi (\"$($this.DeploymentContext.BaseApiUrl)/tasks/{0}/details?verbose=false&tail=30\" -f $this.Deployment.TaskId)\n        $this.IsCompleted = $details.Task.IsCompleted\n        $this.FinishedSuccessfully = $details.Task.FinishedSuccessfully\n        $this.ErrorMessage = $details.Task.ErrorMessage\n\n        $this.PollCount++\n        if ($this.PollCount % 10 -eq 0) {\n            $this.Verbose(\"$($details.Task.State). $($details.Task.Duration), $($details.Progress.EstimatedTimeRemaining)\")\n        }\n        \n        if ($details.Task.HasPendingInterruptions) { $this.HasInterruptions = $true }\n        $this.LogQueuePosition($details.Task)\n        $activityLogs = $this.FlattenActivityLogs($details.ActivityLogs)    \n        $this.WriteLogMessages($activityLogs)\n    }\n\n    hidden [bool] IfNewState($firstKey, $secondKey, $value) {\n        $key = '{0}/{1}' -f $firstKey, $secondKey\n        $containsKey = $this.State.ContainsKey($key)\n        if ($containsKey) { return $false }\n        $this.State[$key] = $value\n        return $true\n    }\n\n    hidden [bool] HasChangedState($firstKey, $secondKey, $value) {\n        $key = '{0}/{1}' -f $firstKey, $secondKey\n        $hasChanged = if (!$this.State.ContainsKey($key)) { $true } else { $this.State[$key] -ne $value }\n        if ($hasChanged) {\n            $this.State[$key] = $value\n        }\n        return $hasChanged\n    }\n\n    hidden [object] GetState($firstKey, $secondKey) { return $this.State[('{0}/{1}' -f $firstKey, $secondKey)] }\n\n    hidden [void] ResetState($firstKey, $secondKey) { $this.State.Remove(('{0}/{1}' -f $firstKey, $secondKey)) }\n\n    hidden [void] Error($message) { Write-Error \"$($this.LogPrefix)${message}\" }\n    hidden [void] Warn($message) { Write-Warning \"$($this.LogPrefix)${message}\" }\n    hidden [void] Host($message) { Write-Host \"$($this.LogPrefix)${message}\" }   \n    hidden [void] Verbose($message) { Write-Verbose \"$($this.LogPrefix)${message}\" }\n\n    hidden [psobject[]] FlattenActivityLogs($ActivityLogs) {\n        $flattenedActivityLogs = { @() }.Invoke()\n        $this.FlattenActivityLogs($ActivityLogs, $null, $flattenedActivityLogs)\n        return $flattenedActivityLogs\n    }\n\n    hidden [void] FlattenActivityLogs($ActivityLogs, $Parent, $flattenedActivityLogs) {\n        foreach ($log in $ActivityLogs) {\n            $log | Add-Member -MemberType NoteProperty -Name Parent -Value $Parent\n            $insertBefore = $null -eq $log.Parent -and $log.Status -eq 'Running'\t\n            if ($insertBefore) { $flattenedActivityLogs.Add($log) }\n            foreach ($childLog in $log.Children) {\n                $this.FlattenActivityLogs($childLog, $log, $flattenedActivityLogs)\n            }\n            if (!$insertBefore) { $flattenedActivityLogs.Add($log) }\n        }\n    }\n\n    hidden [void] LogQueuePosition($Task) {\n        if ($Task.HasBeenPickedUpByProcessor) {\n            $this.ResetState($Task.Id, 'QueuePosition')\n            return\n        }\n\t\t\n        $queuePosition = (Invoke-OctopusApi (\"$($this.DeploymentContext.BaseApiUrl)/tasks/{0}/queued-behind\" -f $this.Deployment.TaskId)).Items.Count\n        if ($this.HasChangedState($Task.Id, 'QueuePosition', $queuePosition) -and $queuePosition -ne 0) {\n            $this.Host(\"Queued behind $queuePosition tasks...\")\n        }\n    }\n\n    hidden [void] WriteLogMessages($ActivityLogs) {\n        $interrupts = if ($this.HasInterruptions) {\n            Invoke-OctopusApi (\"$($this.DeploymentContext.BaseApiUrl)/interruptions?regarding={0}\" -f $this.Deployment.TaskId) | ForEach-Object Items\n        }\n        foreach ($activity in $ActivityLogs) {\n            $correlatedInterrupts = $interrupts | Where-Object CorrelationId -eq $activity.Id         \n            $correlatedInterrupts | Where-Object IsPending -eq $false | ForEach-Object { $this.LogInterruptMessages($activity, $_) }\n\n            $this.LogStepTransition($activity)         \n            $this.LogErrorsAndWarnings($activity)\n            $correlatedInterrupts | Where-Object IsPending -eq $true | ForEach-Object { \n                $this.LogInterruptMessages($activity, $_)\n                $this.HandleInterrupt($_)\n            }\n        }\n    }\n\n    hidden [void] LogStepTransition($ActivityLog) {\n        if ($ActivityLog.ShowAtSummaryLevel -and $ActivityLog.Status -ne 'Pending') {\n            $existingState = $this.GetState($ActivityLog.Id, 'Status')\n            if ($this.HasChangedState($ActivityLog.Id, 'Status', $ActivityLog.Status)) {\n                $existingStateText = if ($existingState) { \"$existingState -> \" }\n                $this.Host(\"$($ActivityLog.Name) ($existingStateText$($ActivityLog.Status))\")\n            }\n        }\n    }\n\n    hidden [void] LogErrorsAndWarnings($ActivityLog) {\n        foreach ($logEntry in $ActivityLog.LogElements) {\n            if ($logEntry.Category -eq 'Info') { continue }\n            if ($this.Logs.Add(($ActivityLog.Id, $logEntry.OccurredAt, $logEntry.MessageText -join '/'))) {\n                switch ($logEntry.Category) {\n                    'Fatal' {\n                        if ($ActivityLog.Parent) {\n                            $this.Error(\"FATAL: During $($ActivityLog.Parent.Name)\")\n                            $this.Error(\"FATAL: $($logEntry.MessageText)\")\n                        }\n                    }\n                    'Error' { $this.Error(\"[$($ActivityLog.Parent.Name)] $($logEntry.MessageText)\") }\n                    'Warning' { $this.Warn(\"[$($ActivityLog.Parent.Name)] $($logEntry.MessageText)\") }\n                }\n            }\n        }\n    }\n\n    hidden [void] LogInterruptMessages($ActivityLog, $Interrupt) {\n        $message = $Interrupt.Form.Elements | Where-Object Name -eq Instructions | ForEach-Object Control | ForEach-Object Text\n        if ($Interrupt.IsPending -and $this.HasChangedState($Interrupt.Id, $ActivityLog.Parent.Name, $message)) {\n            $this.Warn(\"Deployment is paused at '$($ActivityLog.Parent.Name)' for manual intervention: $message\")\n        }\n        if ($null -ne $Interrupt.ResponsibleUserId -and $this.HasChangedState($Interrupt.Id, 'ResponsibleUserId', $Interrupt.ResponsibleUserId)) {\n            $user = Invoke-OctopusApi (Format-LinksUri -Uri $Interrupt.Links.User)\n            $emailText = if (Test-String $user.EmailAddress) { \" ($($user.EmailAddress))\" }\n            $this.Warn(\"$($user.DisplayName)$emailText has taken responsibility for the manual intervention\")\n        }\n        $manualAction = $Interrupt.Form.Values.Result\n        if ((Test-String $manualAction) -and $this.HasChangedState($Interrupt.Id, 'Action', $manualAction)) {\n            $this.Warn(\"Manual intervention action '$manualAction' submitted with notes: $($Interrupt.Form.Values.Notes)\")\n        }\n        $guidanceAction = $Interrupt.Form.Values.Guidance\n        if ((Test-String $guidanceAction) -and $this.HasChangedState($Interrupt.Id, 'Action', $guidanceAction)) {\n            $this.Warn(\"Failure guidance to '$guidanceAction' submitted with notes: $($Interrupt.Form.Values.Notes)\")\n        }\n    }\n\n    hidden [void] HandleInterrupt($Interrupt) {\n        $isGuidedFailure = $null -ne ($Interrupt.Form.Elements | Where-Object Name -eq Guidance)\n        if (!$isGuidedFailure -or !$this.DeploymentContext.GuidedFailureActions -or !$Interrupt.IsPending) {\n            return\n        }\n        $this.IfNewState($Interrupt.CorrelationId, 'ActionIndex', 0)\n        if ($Interrupt.CanTakeResponsibility -and $null -eq $Interrupt.ResponsibleUserId) {\n            Invoke-OctopusApi (Format-LinksUri -Uri $Interrupt.Links.Responsible) -Method Put\n        }\n        if ($Interrupt.HasResponsibility) {\n            $guidanceIndex = $this.GetState($Interrupt.CorrelationId, 'ActionIndex')\n            $guidance = $this.DeploymentContext.GuidedFailureActions[$guidanceIndex]\n            $guidanceIndex++\n            \n            $retryWaitPeriod = New-TimeSpan -Seconds (Get-OctopusSetting RetryWaitPeriod 0)\n            if ($guidance -eq 'Retry' -and $retryWaitPeriod.TotalSeconds -gt 0) {\n                $minutesText = if ($retryWaitPeriod.Minutes -gt 1) { \" $($retryWaitPeriod.Minutes) minutes\" } elseif ($retryWaitPeriod.Minutes -eq 1) { \" $($retryWaitPeriod.Minutes) minute\" }\n                $secondsText = if ($retryWaitPeriod.Seconds -gt 1) { \" $($retryWaitPeriod.Seconds) seconds\" } elseif ($retryWaitPeriod.Seconds -eq 1) { \" $($retryWaitPeriod.Seconds) second\" }\n                $this.Warn(\"Waiting${minutesText}${secondsText} before submitting retry failure guidance...\")\n                Start-Sleep -Seconds $retryWaitPeriod.TotalSeconds\n            }\n            Invoke-OctopusApi (Format-LinksUri -Uri $Interrupt.Links.Submit) -Body @{\n                Notes    = $this.DeploymentContext.GuidedFailureMessage.Replace('#{GuidedFailureActionIndex}', $guidanceIndex).Replace('#{GuidedFailureAction}', $guidance)\n                Guidance = $guidance\n            } -Method Post\n\n            $this.HasChangedState($Interrupt.CorrelationId, 'ActionIndex', $guidanceIndex)\n        }\n    }\n}\n\nfunction Show-Heading {\n    param($Text)\n    $padding = ' ' * ((80 - 2 - $Text.Length) / 2)\n    Write-Host \" `n\"\n    Write-Host (@(\"`t\", ([string][char]0x2554), (([string][char]0x2550) * 80), ([string][char]0x2557)) -join '')\n    Write-Host \"`t$(([string][char]0x2551))$padding $Text $padding$([string][char]0x2551)\"  \n    Write-Host (@(\"`t\", ([string][char]0x255A), (([string][char]0x2550) * 80), ([string][char]0x255D)) -join '')\n    Write-Host \" `n\"\n}\n\nif ($OctopusParameters['Octopus.Action.RunOnServer'] -ieq 'False') {\n    Write-Warning \"For optimal performance use 'Run On Server' for this action\"\n}\n\n$deploymentContext = [DeploymentContext]::new($Chain_BaseUrl, $Chain_BaseApiUrl)\n\nif ($Chain_CreateOption -ieq 'True') {\n    Show-Heading 'Creating Release'\n}\nelse {\n    Show-Heading 'Retrieving Release'\n}\n$deploymentContext.SetProject($Chain_ProjectName)\n$deploymentContext.SetChannel($Chain_Channel)\nWrite-Host \"`t$Chain_BaseUrl$(Format-WebLinksUri -Uri $deploymentContext.Project.Links.Web)\"\n\nif ($Chain_CreateOption -ieq 'True') {\n    $deploymentContext.CreateRelease($Chain_ReleaseNum)\n}\nelse {\n    $deploymentContext.SetRelease($Chain_ReleaseNum)\n}\nWrite-Host \"`t$Chain_BaseUrl$(Format-WebLinksUri -Uri $deploymentContext.Release.Links.Web)\"\nif ($Chain_SnapshotVariables -ieq 'True') {\n    $deploymentContext.UpdateVariableSnapshot()\n}\n\n\nShow-Heading 'Configuring Deployment'\n$deploymentContext.GetDeploymentTemplate()\n$email = if (Test-String $OctopusParameters['Octopus.Deployment.CreatedBy.EmailAddress']) { \"($($OctopusParameters['Octopus.Deployment.CreatedBy.EmailAddress']))\" }\n$guidedFailureMessage = Get-OctopusSetting GuidedFailureMessage @\"\nAutomatic Failure Guidance will #{GuidedFailureAction} (Failure ###{GuidedFailureActionIndex})\nInitiated by $($OctopusParameters['Octopus.Deployment.Name']) of $($OctopusParameters['Octopus.Project.Name']) release $($OctopusParameters['Octopus.Release.Number'])\nCreated By: $($OctopusParameters['Octopus.Deployment.CreatedBy.DisplayName']) $email\n${Chain_BaseUrl}$($OctopusParameters['Octopus.Web.DeploymentLink'])\n\"@\n$deploymentContext.SetGuidedFailure($Chain_GuidedFailure, $guidedFailureMessage)\n$deploymentContext.SetSchedule($Chain_DeploySchedule)\n\n$deploymentContext.SetEnvironment($Chain_DeployTo)\n$deploymentContext.SetTenants($Chain_Tenants)\n$deploymentContext.SetForcePackageDownload($Chain_ForcePackageDownload)\n$deploymentControllers = $deploymentContext.GetDeploymentControllers()\nif (Test-String $Chain_StepsToSkip) {\n    $deploymentControllers | ForEach-Object { $_.SetStepsToSkip($Chain_StepsToSkip) }\n}\nif (Test-String $Chain_FormValues) {\n    $deploymentControllers | ForEach-Object { $_.SetFormValues($Chain_FormValues) }\n}\n\nif (Test-String $Chain_MachineList) {\n    $deploymentControllers | ForEach-Object { $_.SetSpecificMachineIds($Chain_MachineList) }\n}\n\nShow-Heading 'Queue Deployment'\nif ($deploymentContext.IsTenanted) {\n    Write-Host 'Queueing tenant deployments...'\n}\nelse {\n    Write-Host 'Queueing untenanted deployment...'\n}\n$deploymentControllers | ForEach-Object Start\n\nif (!$deploymentContext.WaitForDeployment) {\n    Write-Host 'Deployments have been queued, proceeding to the next step...'\n    return\n}\n\nShow-Heading 'Waiting For Deployment'\ndo {\n    Start-Sleep -Seconds 1\n    $tasksStillRunning = $false\n    foreach ($deployment in $deploymentControllers) {\n        if ($deployment.PollCheck()) {\n            $tasksStillRunning = $true\n        }\n    }\n} while ($tasksStillRunning)\n\nif ($deploymentControllers | ForEach-Object Task | Where-Object FinishedSuccessfully -eq $false) {\n    Show-Heading 'Deployment Failed!'\n    Write-Fatal (($deploymentControllers | ForEach-Object Task | ForEach-Object ErrorMessage) -join \"`n\")\n}\nelse {\n    Show-Heading 'Deployment Successful!'\n}\n\nif (Test-String $Chain_PostDeploy -ForAbsence) {\n    return \n}\n\nShow-Heading 'Post-Deploy Script'\n$rawPostDeployScript = Invoke-OctopusApi (\"$Chain_BaseApiUrl/releases/{0}\" -f $OctopusParameters['Octopus.Release.Id']) |\nForEach-Object { Invoke-OctopusApi (Format-LinksUri -Uri $_.Links.ProjectDeploymentProcessSnapshot) } |\nForEach-Object Steps | Where-Object Id -eq $OctopusParameters['Octopus.Step.Id'] |\nForEach-Object Actions | Where-Object Id -eq $OctopusParameters['Octopus.Action.Id'] |\nForEach-Object { $_.Properties.Chain_PostDeploy }\nWrite-Verbose \"Raw Post-Deploy Script:`n$rawPostDeployScript\"\n\nAdd-Type -Path (Get-WmiObject Win32_Process | Where-Object ProcessId -eq $PID | ForEach-Object { Get-Process -Id $_.ParentProcessId } | ForEach-Object { Join-Path (Split-Path -Path $_.Path -Parent) 'Octostache.dll' })\n\n$deploymentControllers | ForEach-Object {\n    $deployment = $_.Task.Deployment\n    $tenant = $_.Tenant\n    $variablesDictionary = [Octostache.VariableDictionary]::new()\n    Invoke-OctopusApi (\"$Chain_BaseApiUrl/variables/{0}\" -f $deployment.ManifestVariableSetId) | ForEach-Object Variables | Where-Object {\n        ($_.IsSensitive -eq $false) -and `\n        ($_.Scope.Private -ne 'True') -and `\n        ($null -eq $_.Scope.Action) -and `\n        ($null -eq $_.Scope.Machine) -and `\n        ($null -eq $_.Scope.TargetRole) -and `\n        ($null -eq $_.Scope.Role) -and `\n        ($null -eq $_.Scope.Tenant -or $_.Scope.Tenant -contains $tenant.Id) -and `\n        ($null -eq $_.Scope.TenantTag -or (Compare-Object $_.Scope.TenantTag $tenant.TenantTags -ExcludeDifferent -IncludeEqual)) -and `\n        ($null -eq $_.Scope.Environment -or $_.Scope.Environment -contains $deployment.EnvironmentId) -and `\n        ($null -eq $_.Scope.Channel -or $_.Scope.Channel -contains $deployment.ChannelId) -and `\n        ($null -eq $_.Scope.Project -or $_.Scope.Project -contains $deployment.ProjectId)\n    } | ForEach-Object { $variablesDictionary.Set($_.Name, $_.Value) }\n    $postDeployScript = $variablesDictionary.Evaluate($rawPostDeployScript)\n    Write-Host \"$($_.LogPrefix)Evaluated Post-Deploy Script:\"\n    Write-Host $postDeployScript\n    Write-Host 'Script output:'\n    [scriptblock]::Create($postDeployScript).Invoke()\n}"
  },
  "Category": "Octopus",
  "HistoryUrl": "https://github.com/OctopusDeploy/Library/commits/master/step-templates//opt/buildagent/work/75443764cd38076d/step-templates/octopus-chain-deployment.json",
  "Website": "/step-templates/18392835-d50e-4ce9-9065-8e15a3c30954",
  "Logo": "iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAMAAACahl6sAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAC1QTFRFT6Tl////L5Pg8vj9Y67omsvwPJrisdfzfbzs5fL7y+T32Ov5isLucLXqvt31CJPHWwAABMJJREFUeNrs3deW4jAMAFDF3U75/89dlp0ZhiU4blJEjvQ8hYubLJsA00UCBCIQgQhEIAIRiEAEIhCBCEQgAhGIQAQiEIEIhD8kJm+t+QprfdKfB9HbYpx6CWfspj8HMi+gMgHL/AmQA8W3JTKH+ALFvzCeL0RbpyoCPE9IJeNOSQwh5Z3qd6yRGWQ2qi2cZQWxqj1WzQYSjeoJmJlAklOd4VlArOqPhQEkqBERToeMcfRJBkC0Uep8CfBpjz4JsHJ0zF3dkEWNje0kiB/sUC6eApndaIiCMyAa1PiwJ0AWhRGJHJJQHG2dC7h1rNbO1QOxSA7lNCkkKrQIpJCAB1GREILYIC1NAiwbpKFJgGWDNExcwGstfExcZBCHC6nOglshHtmhViLIig1RNBCN7qjtW8C0Z1UvJcC1Z9XmwMBzzvobmgAyEzgq91dtEEsBsQSQQAFZCSBAATEEEApHZbrVBIkkEIUPSVeB+KtALA0kXQUSrwKZBCIQBnk8Y4i5CsReBeKvkqLM+BCSDWJlrZFvGk9SRTHshkgjZCGAaArIxm3H3grhVzFlW2msfl1ca79UJ1bofYvsDHHlNdTZnlh5MghuPd5NdBDUNZHyCkfktIh03XzALGRPlBDPac7qgWjHZzWcmF5zmmkhidMQ6boKiDXcDTUEaylZqCGJ0Vjvu/fLJtHqhSANEvqb2OYqkOUqEHuVMbJcZdZCGiPhKhC4yjqiIjEE7XThMp8fAWII3mY3kUIQD+AMKQTzPiBhgQ63HlT/KSvgtoi0dq5mCPah1UIE0eh3sT0NhOByvKeAkFzi8PgQomumFhsyOxpIzZN4gLOj5plVwNpR0b2AuePWKBEHQu24pSsJA+LVCeHHQxZ1SiyDIdqok8IOhSSnTottHEQTdyt4ettAj4KkzA4dMikk2Dht2S5ptm1vswnPDxn0YyDZ5oDM3iToo2T5voWaYe+Q+vdjH80QyAzZhCgcDtLMI1Tmtz9w++XHgziHQHJJu/OZ3bs9Xn8gQ72NcP3dKqEfkp10F51xhoIi2I91R+LurXV/5q7pH+wx061CzO16oSQleMyr8fXvwMA0Pro8432DPD/ySx8XrHfSuDAM8n6UhnjQabaiXf5Bq/lREHvEeNtn1rJ08+C/uXkQZHeguxAPC3UvtcJYUogLzZX5hhZZvS6onG5lxXtzWGaygwb79vT/IXhdlNibwlKYOR6T8xjI7W8n+xV7T+GH4tMzWwR+lZhRkJYSsC0thpmCYqyngOz3rN2FLBZ2wZflBCggUHF0Vnp88JKienzIXLSEZCZqU7IKr/gQW9yx3pzV7Y9kvWZWTRRIqDmTtRUnU7b2lLcTYmoqHqnmiO1poER0SPkAeZMAZxaJx0Y3TCdAclsIqDz03ALcyxfTCZBsthoGXWmigGyVhWPLFJJfuuKQWycoEFdXbH4dJJoJxNR1eD/kshz6yn48cF8yW8sFoitflB1w6Q8n+/15Za7oA17/pYNmYgP5fmWm8L1NOHPWgK8kuFew1/JXtOA0yJCv7ah7X8ObUuT5kObU30+fDZm8+zqP+HTIpK0xQ796b5Kv2hSIQAQiEIEIRCACEYhABCIQgQhEIAIRiEAEIpBf8UeAAQAEjtYmlDTcCgAAAABJRU5ErkJggg==",
  "$Meta": {
    "Type": "ActionTemplate"
  }
}

History

Page updated on Thursday, February 23, 2023