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.
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"
}
}
Page updated on Thursday, February 23, 2023