From e685744fa3bfb1a1617efbe03b546096c4e54f98 Mon Sep 17 00:00:00 2001 From: Jim Burger Date: Tue, 18 Dec 2018 16:28:19 +1030 Subject: [PATCH 01/16] Updated 'clone a tenant' step to be space aware --- step-templates/clone-tenant.json | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/step-templates/clone-tenant.json b/step-templates/clone-tenant.json index 02f43e3fc..02f2e25f7 100644 --- a/step-templates/clone-tenant.json +++ b/step-templates/clone-tenant.json @@ -3,12 +3,12 @@ "Name": "Clone Tenant", "Description": "Clone an Octopus [tenant](https://octopus.com/docs/deployment-patterns/multi-tenant-deployments). The project connections and tenant tags will be cloned and the non-sensitive variables can optionally be cloned.", "ActionType": "Octopus.Script", - "Version": 1, + "Version": 2, "Packages": [], "Properties": { "Octopus.Action.Script.ScriptSource": "Inline", "Octopus.Action.Script.Syntax": "PowerShell", - "Octopus.Action.Script.ScriptBody": "$apiKey = $CloneTenantStep_ApiKey\n$tenantToClone = $CloneTenantStep_TenantIdToClone\n$tenantName = $CloneTenantStep_TenantName\n$octopusBaseUrl = $OctopusParameters['Octopus.Web.BaseUrl'].Trim('/')\n$cloneVariables = $CloneTenantStep_CloneVariables\n\n$ErrorActionPreference = 'Stop'\n\nif ([string]::IsNullOrWhiteSpace($apiKey)) {\n throw \"The step parameter 'Octopus API Key' was not found. This step requires an API Key to function, please provide one and try again.\"\n}\n\nfunction Invoke-OctopusApi {\n param(\n [Parameter(Position = 0, Mandatory)]$Uri,\n [ValidateSet(\"Get\", \"Post\", \"Put\", \"Delete\")]$Method = 'Get',\n $Body\n )\n $requestParameters = @{\n Uri = ('{0}/{1}' -f $octopusBaseUrl, $Uri.TrimStart('/'))\n Method = $Method\n Headers = @{ \"X-Octopus-ApiKey\" = $apiKey }\n UseBasicParsing = $true\n }\n if ($null -ne $Body) { $requestParameters.Add('Body', ($Body | ConvertTo-Json -Depth 10)) }\n Write-Verbose \"$($Method.ToUpperInvariant()) $($requestParameters.Uri)\" \n \n\n return Invoke-WebRequest @requestParameters | % Content | ConvertFrom-Json\n}\n\n$tenant = Invoke-OctopusApi \"api/tenants/$tenantToClone\"\n\n$tenant.Id = $null\n$tenant.Name = $tenantName\n\n$newTenant = Invoke-OctopusApi \"/api/tenants\" -Method Post -Body $tenant\n\nif ($cloneVariables -eq $true) {\n $variables = Invoke-OctopusApi $tenant.Links.Variables\n $variables.TenantId = $newTenant.Id\n $variables.TenantName = $tenantName\n\n $newVars = Invoke-OctopusApi $newTenant.Links.Variables -Method Put -Body $variables\n}" + "Octopus.Action.Script.ScriptBody": "$apiKey = $CloneTenantStep_ApiKey\n$tenantToClone = $CloneTenantStep_TenantIdToClone\n$tenantName = $CloneTenantStep_TenantName\n$octopusBaseUrl = $OctopusParameters['Octopus.Web.BaseUrl'].Trim('/')\n$cloneVariables = $CloneTenantStep_CloneVariables\n$spaceId = $CloneTenantStep_SpaceId\n\n$ErrorActionPreference = 'Stop'\n\nif ([string]::IsNullOrWhiteSpace($apiKey)) {\n throw \"The step parameter 'Octopus API Key' was not found. This step requires an API Key to function, please provide one and try again.\"\n}\n\nfunction Invoke-OctopusApi {\n param(\n [Parameter(Position = 0, Mandatory)]$Uri,\n [ValidateSet(\"Get\", \"Post\", \"Put\", \"Delete\")]$Method = 'Get',\n $Body\n )\n $requestParameters = @{\n Uri = ('{0}/{1}' -f $octopusBaseUrl, $Uri.TrimStart('/'))\n Method = $Method\n Headers = @{ \"X-Octopus-ApiKey\" = $apiKey }\n UseBasicParsing = $true\n }\n if ($null -ne $Body) { $requestParameters.Add('Body', ($Body | ConvertTo-Json -Depth 10)) }\n Write-Verbose \"$($Method.ToUpperInvariant()) $($requestParameters.Uri)\" \n \n\n return Invoke-WebRequest @requestParameters | % Content | ConvertFrom-Json\n}\n\nfunction Test-SpacesApi {\n\t$rootDocument = Invoke-OctopusApi 'api/';\n if($rootDocument.Links -ne $null -and $rootDocument.Links.Spaces -ne $null) {\n \treturn $true;\n }\n return $false;\n}\n\nif(Test-SpacesApi) {\n\tif([string]::IsNullOrWhiteSpace($spaceId)) {\n throw \"The step parameter 'Space ID' was not found. This step requires an explicit space ID when used against an Octopus Server that supports Spaces\";\n }\n\t$tenantApiUrl = \"api/$spaceId/tenants\" ;\n} else {\n\t$tenantApiUrl = \"api/tenants\" ;\n}\n\n$tenant = Invoke-OctopusApi \"$tenantApiUrl/$tenantToClone\"\n\n$tenant.Id = $null\n$tenant.Name = $tenantName\n\n$newTenant = Invoke-OctopusApi $tenantApiUrl -Method Post -Body $tenant\n\nif ($cloneVariables -eq $true) {\n $variables = Invoke-OctopusApi $tenant.Links.Variables\n $variables.TenantId = $newTenant.Id\n $variables.TenantName = $tenantName\n\n $newVars = Invoke-OctopusApi $newTenant.Links.Variables -Method Put -Body $variables\n}" }, "Parameters": [ { @@ -50,6 +50,14 @@ "DisplaySettings": { "Octopus.ControlType": "Checkbox" } + }, + { + "DisplaySettings": { + "Octopus.ControlType": "SingleLineText" + }, + "Name": "CloneTenantStep_SpaceId", + "Label": "Space Id of Tenant to Clone", + "HelpText": "The Id of the Space that the Tenant to clone resides in. Leave blank if your version of Octopus server doesn't support spaces yet." } ], "$Meta": { From 5d06d57341fbe44eff095c918d9eeab4b7ba48a0 Mon Sep 17 00:00:00 2001 From: Jim Burger Date: Tue, 18 Dec 2018 17:26:08 +1030 Subject: [PATCH 02/16] Updates 'save output variable to project' to be space aware --- step-templates/save-octopus-output-variable.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/step-templates/save-octopus-output-variable.json b/step-templates/save-octopus-output-variable.json index a64002769..31818712d 100644 --- a/step-templates/save-octopus-output-variable.json +++ b/step-templates/save-octopus-output-variable.json @@ -3,12 +3,12 @@ "Name": "Save Octopus Output Variable", "Description": "Saves an [output variable](https://octopus.com/docs/deploying-applications/variables/output-variables) to the given project / library variable set", "ActionType": "Octopus.Script", - "Version": 2, + "Version": 3, "Properties": { "Octopus.Action.Script.Syntax": "PowerShell", "Octopus.Action.Script.ScriptSource": "Inline", "Octopus.Action.RunOnServer": "true", - "Octopus.Action.Script.ScriptBody": "$ErrorActionPreference = 'Stop'\n\n$StepTemplate_BaseUrl = $OctopusParameters['Octopus.Web.BaseUrl'].Trim('/')\nif ([string]::IsNullOrWhiteSpace($StepTemplate_ApiKey)) {\n throw \"The step parameter 'API Key' was not found. This step requires an API Key to function, please provide one and try again.\"\n}\n\nfunction Invoke-OctopusApi {\n param(\n [Parameter(Position = 0, Mandatory)]$Uri,\n [ValidateSet(\"Get\", \"Put\")]$Method = 'Get',\n $Body\n )\n $requestParameters = @{\n Uri = ('{0}/{1}' -f $StepTemplate_BaseUrl, $Uri.TrimStart('/'))\n Method = $Method\n Headers = @{ \"X-Octopus-ApiKey\" = $StepTemplate_ApiKey }\n UseBasicParsing = $true\n }\n if ($null -ne $Body) { $requestParameters.Add('Body', ($Body | ConvertTo-Json -Depth 10)) }\n Write-Verbose \"$($Method.ToUpperInvariant()) $($requestParameters.Uri)\" \n Invoke-WebRequest @requestParameters | % Content | ConvertFrom-Json | Write-Output\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 [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$outputVariableKey = \"Octopus.Action[${StepTemplate_DeploymentStep}].Output.${StepTemplate_VariableName}\"\nif (!$OctopusParameters.ContainsKey($outputVariableKey)) {\n throw \"Variable '$StepTemplate_VariableName' has not been output from '$StepTemplate_DeploymentStep'\"\n}\n$isSensitive = [System.Convert]::ToBoolean($StepTemplate_IsSensitive)\n$variableType = if ($isSensitive) { \"Sensitive\" } else { \"String\" }\n\n$variableValue = $OctopusParameters[$outputVariableKey]\nWrite-Host \"Name: $StepTemplate_VariableName\"\nWrite-Host \"Type: $variableType\"\nWrite-Host \"Value: $(if ($isSensitive) { \"********\" } else { $variableValue })\"\nWrite-Host ' '\n\nWrite-Host \"Retrieving $StepTemplate_VariableSetType variable set...\"\nif ($StepTemplate_VariableSetType -eq 'project') {\n $variableSet = Invoke-OctopusApi '/api/projects/all' | ? Name -eq $StepTemplate_VariableSetName | % { Invoke-OctopusApi $_.Links.Variables }\n}\nif ($StepTemplate_VariableSetType -eq 'library') {\n $variableSet = Invoke-OctopusApi '/api/libraryvariablesets/all?ContentType=Variables' | ? Name -eq $StepTemplate_VariableSetName | % { Invoke-OctopusApi $_.Links.Variables }\n}\nif ($null -eq $variableSet) {\n throw \"Unable to find $StepTemplate_VariableSetType variable set '$StepTemplate_VariableSetName'\"\n}\n\n$variableExists = $false\n$variableSet.Variables | ? Name -eq $StepTemplate_VariableName | % {\n Write-Host \"Updating existing variable...\"\n Write-Verbose \"Existing value: $(if ($isSensitive) { \"********\" } else { $_.Value })\"\n $_.Value = $variableValue\n $_.Type = $variableType\n $_.IsSensitive = $isSensitive\n $_.Scope = Get-OctopusSetting Scope $_.Scope\n $variableExists = $true\n}\nif (!$variableExists) {\n Write-Host \"Creating new variable...\"\n $variableSet.Variables += @{\n Name = $StepTemplate_VariableName\n Value = $variableValue\n Type = $variableType\n IsSensitive = $isSensitive\n Scope = (Get-OctopusSetting Scope @{})\n }\n}\n\nWrite-Host \"Saving updated variable set...\"\nInvoke-OctopusApi $variableSet.Links.Self -Method Put -Body $variableSet | Out-Null", + "Octopus.Action.Script.ScriptBody": "$ErrorActionPreference = 'Stop'\n\n$StepTemplate_BaseUrl = $OctopusParameters['Octopus.Web.BaseUrl'].Trim('/')\nif ([string]::IsNullOrWhiteSpace($StepTemplate_ApiKey)) {\n throw \"The step parameter 'API Key' was not found. This step requires an API Key to function, please provide one and try again.\"\n}\n\nfunction Invoke-OctopusApi {\n param(\n [Parameter(Position = 0, Mandatory)]$Uri,\n [ValidateSet(\"Get\", \"Put\")]$Method = 'Get',\n $Body\n )\n $requestParameters = @{\n Uri = ('{0}/{1}' -f $StepTemplate_BaseUrl, $Uri.TrimStart('/'))\n Method = $Method\n Headers = @{ \"X-Octopus-ApiKey\" = $StepTemplate_ApiKey }\n UseBasicParsing = $true\n }\n if ($null -ne $Body) { $requestParameters.Add('Body', ($Body | ConvertTo-Json -Depth 10)) }\n Write-Verbose \"$($Method.ToUpperInvariant()) $($requestParameters.Uri)\" \n Invoke-WebRequest @requestParameters | % Content | ConvertFrom-Json | Write-Output\n}\n\nfunction Test-SpacesApi {\n\tWrite-Verbose \"Checking API compatibility\";\n\t$rootDocument = Invoke-OctopusApi 'api/';\n if($rootDocument.Links -ne $null -and $rootDocument.Links.Spaces -ne $null) {\n \tWrite-Verbose \"Spaces API found\"\n \treturn $true;\n }\n Write-Verbose \"Pre-spaces API found\"\n return $false;\n}\n\nif(Test-SpacesApi) {\n\t$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 recieved a blank value, which could indicate you do not have the correct permissions.\";\n }\n\t$baseApiUrl = \"api/$spaceId\" ;\n} else {\n\t$baseApiUrl = \"api\" ;\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 [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$outputVariableKey = \"Octopus.Action[${StepTemplate_DeploymentStep}].Output.${StepTemplate_VariableName}\"\nif (!$OctopusParameters.ContainsKey($outputVariableKey)) {\n throw \"Variable '$StepTemplate_VariableName' has not been output from '$StepTemplate_DeploymentStep'\"\n}\n$isSensitive = [System.Convert]::ToBoolean($StepTemplate_IsSensitive)\n$variableType = if ($isSensitive) { \"Sensitive\" } else { \"String\" }\n\n$variableValue = $OctopusParameters[$outputVariableKey]\nWrite-Host \"Name: $StepTemplate_VariableName\"\nWrite-Host \"Type: $variableType\"\nWrite-Host \"Value: $(if ($isSensitive) { \"********\" } else { $variableValue })\"\nWrite-Host ' '\n\nWrite-Host \"Retrieving $StepTemplate_VariableSetType variable set...\"\nif ($StepTemplate_VariableSetType -eq 'project') {\n $variableSet = Invoke-OctopusApi \"$baseApiUrl/projects/all\" | ? Name -eq $StepTemplate_VariableSetName | % { Invoke-OctopusApi $_.Links.Variables }\n}\nif ($StepTemplate_VariableSetType -eq 'library') {\n $variableSet = Invoke-OctopusApi \"$baseApiUrl/libraryvariablesets/all?ContentType=Variables\" | ? Name -eq $StepTemplate_VariableSetName | % { Invoke-OctopusApi $_.Links.Variables }\n}\nif ($null -eq $variableSet) {\n throw \"Unable to find $StepTemplate_VariableSetType variable set '$StepTemplate_VariableSetName'\"\n}\n\n$variableExists = $false\n$variableSet.Variables | ? Name -eq $StepTemplate_VariableName | % {\n Write-Host \"Updating existing variable...\"\n Write-Verbose \"Existing value: $(if ($isSensitive) { \"********\" } else { $_.Value })\"\n $_.Value = $variableValue\n $_.Type = $variableType\n $_.IsSensitive = $isSensitive\n $_.Scope = Get-OctopusSetting Scope $_.Scope\n $variableExists = $true\n}\nif (!$variableExists) {\n Write-Host \"Creating new variable...\"\n $variableSet.Variables += @{\n Name = $StepTemplate_VariableName\n Value = $variableValue\n Type = $variableType\n IsSensitive = $isSensitive\n Scope = (Get-OctopusSetting Scope @{})\n }\n}\n\nWrite-Host \"Saving updated variable set...\"\nInvoke-OctopusApi $variableSet.Links.Self -Method Put -Body $variableSet | Out-Null", "Octopus.Action.Script.ScriptFileName": null, "Octopus.Action.Package.FeedId": null, "Octopus.Action.Package.PackageId": null From 146e5d927fd17d4ac0314afacd50cf99d20f3e25 Mon Sep 17 00:00:00 2001 From: Jim Burger Date: Tue, 18 Dec 2018 17:42:11 +1030 Subject: [PATCH 03/16] Making clone tenant a little less space aware --- step-templates/clone-tenant.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/step-templates/clone-tenant.json b/step-templates/clone-tenant.json index 02f2e25f7..ed5bdcd35 100644 --- a/step-templates/clone-tenant.json +++ b/step-templates/clone-tenant.json @@ -8,7 +8,7 @@ "Properties": { "Octopus.Action.Script.ScriptSource": "Inline", "Octopus.Action.Script.Syntax": "PowerShell", - "Octopus.Action.Script.ScriptBody": "$apiKey = $CloneTenantStep_ApiKey\n$tenantToClone = $CloneTenantStep_TenantIdToClone\n$tenantName = $CloneTenantStep_TenantName\n$octopusBaseUrl = $OctopusParameters['Octopus.Web.BaseUrl'].Trim('/')\n$cloneVariables = $CloneTenantStep_CloneVariables\n$spaceId = $CloneTenantStep_SpaceId\n\n$ErrorActionPreference = 'Stop'\n\nif ([string]::IsNullOrWhiteSpace($apiKey)) {\n throw \"The step parameter 'Octopus API Key' was not found. This step requires an API Key to function, please provide one and try again.\"\n}\n\nfunction Invoke-OctopusApi {\n param(\n [Parameter(Position = 0, Mandatory)]$Uri,\n [ValidateSet(\"Get\", \"Post\", \"Put\", \"Delete\")]$Method = 'Get',\n $Body\n )\n $requestParameters = @{\n Uri = ('{0}/{1}' -f $octopusBaseUrl, $Uri.TrimStart('/'))\n Method = $Method\n Headers = @{ \"X-Octopus-ApiKey\" = $apiKey }\n UseBasicParsing = $true\n }\n if ($null -ne $Body) { $requestParameters.Add('Body', ($Body | ConvertTo-Json -Depth 10)) }\n Write-Verbose \"$($Method.ToUpperInvariant()) $($requestParameters.Uri)\" \n \n\n return Invoke-WebRequest @requestParameters | % Content | ConvertFrom-Json\n}\n\nfunction Test-SpacesApi {\n\t$rootDocument = Invoke-OctopusApi 'api/';\n if($rootDocument.Links -ne $null -and $rootDocument.Links.Spaces -ne $null) {\n \treturn $true;\n }\n return $false;\n}\n\nif(Test-SpacesApi) {\n\tif([string]::IsNullOrWhiteSpace($spaceId)) {\n throw \"The step parameter 'Space ID' was not found. This step requires an explicit space ID when used against an Octopus Server that supports Spaces\";\n }\n\t$tenantApiUrl = \"api/$spaceId/tenants\" ;\n} else {\n\t$tenantApiUrl = \"api/tenants\" ;\n}\n\n$tenant = Invoke-OctopusApi \"$tenantApiUrl/$tenantToClone\"\n\n$tenant.Id = $null\n$tenant.Name = $tenantName\n\n$newTenant = Invoke-OctopusApi $tenantApiUrl -Method Post -Body $tenant\n\nif ($cloneVariables -eq $true) {\n $variables = Invoke-OctopusApi $tenant.Links.Variables\n $variables.TenantId = $newTenant.Id\n $variables.TenantName = $tenantName\n\n $newVars = Invoke-OctopusApi $newTenant.Links.Variables -Method Put -Body $variables\n}" + "Octopus.Action.Script.ScriptBody": "$apiKey = $CloneTenantStep_ApiKey\n$tenantToClone = $CloneTenantStep_TenantIdToClone\n$tenantName = $CloneTenantStep_TenantName\n$octopusBaseUrl = $OctopusParameters['Octopus.Web.BaseUrl'].Trim('/')\n$cloneVariables = $CloneTenantStep_CloneVariables\n\n$ErrorActionPreference = 'Stop'\n\nif ([string]::IsNullOrWhiteSpace($apiKey)) {\n throw \"The step parameter 'Octopus API Key' was not found. This step requires an API Key to function, please provide one and try again.\"\n}\n\nfunction Invoke-OctopusApi {\n param(\n [Parameter(Position = 0, Mandatory)]$Uri,\n [ValidateSet(\"Get\", \"Post\", \"Put\", \"Delete\")]$Method = 'Get',\n $Body\n )\n $requestParameters = @{\n Uri = ('{0}/{1}' -f $octopusBaseUrl, $Uri.TrimStart('/'))\n Method = $Method\n Headers = @{ \"X-Octopus-ApiKey\" = $apiKey }\n UseBasicParsing = $true\n }\n if ($null -ne $Body) { $requestParameters.Add('Body', ($Body | ConvertTo-Json -Depth 10)) }\n Write-Verbose \"$($Method.ToUpperInvariant()) $($requestParameters.Uri)\" \n \n\n return Invoke-WebRequest @requestParameters | % Content | ConvertFrom-Json\n}\n\nfunction Test-SpacesApi {\n\tWrite-Verbose \"Checking API compatibility\";\n\t$rootDocument = Invoke-OctopusApi 'api/';\n if($rootDocument.Links -ne $null -and $rootDocument.Links.Spaces -ne $null) {\n \tWrite-Verbose \"Spaces API found\"\n \treturn $true;\n }\n Write-Verbose \"Pre-spaces API found\"\n return $false;\n}\n\nif(Test-SpacesApi) {\n\t$spaceId = $OctopusParameters['Octopus.Space.Id'];\n\tif([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 recieved a blank value, which could indicate you do not have the correct permissions.\";\n }\n\t$tenantApiUrl = \"api/$spaceId/tenants\" ;\n} else {\n\t$tenantApiUrl = \"api/tenants\" ;\n}\n\n$tenant = Invoke-OctopusApi \"$tenantApiUrl/$tenantToClone\"\n\n$tenant.Id = $null\n$tenant.Name = $tenantName\n\n$newTenant = Invoke-OctopusApi $tenantApiUrl -Method Post -Body $tenant\n\nif ($cloneVariables -eq $true) {\n $variables = Invoke-OctopusApi $tenant.Links.Variables\n $variables.TenantId = $newTenant.Id\n $variables.TenantName = $tenantName\n\n $newVars = Invoke-OctopusApi $newTenant.Links.Variables -Method Put -Body $variables\n}" }, "Parameters": [ { From 0df53249088d7d0289fd9db44b3861940c768cb9 Mon Sep 17 00:00:00 2001 From: Jim Burger Date: Tue, 18 Dec 2018 17:53:57 +1030 Subject: [PATCH 04/16] modified 'save output var with scoping' to be space aware --- step-templates/save-octopus-output-variable-with-scoping.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/step-templates/save-octopus-output-variable-with-scoping.json b/step-templates/save-octopus-output-variable-with-scoping.json index db12eac4f..fcb29d44b 100644 --- a/step-templates/save-octopus-output-variable-with-scoping.json +++ b/step-templates/save-octopus-output-variable-with-scoping.json @@ -3,14 +3,14 @@ "Name": "Save Octopus Output Variable With Scoping", "Description": "Saves an [output variable](https://octopus.com/docs/deploying-applications/variables/output-variables) to the given project / library variable set with scoping", "ActionType": "Octopus.Script", - "Version": 1, + "Version": 2, "CommunityActionTemplateId": null, "Packages": [], "Properties": { "Octopus.Action.Script.Syntax": "PowerShell", "Octopus.Action.Script.ScriptSource": "Inline", "Octopus.Action.RunOnServer": "true", - "Octopus.Action.Script.ScriptBody": "$ErrorActionPreference = 'Stop'\n\n$StepTemplate_BaseUrl = $OctopusParameters['Octopus.Web.BaseUrl'].Trim('/')\nif ([string]::IsNullOrWhiteSpace($StepTemplate_ApiKey)) {\n throw \"The step parameter 'API Key' was not found. This step requires an API Key to function, please provide one and try again.\"\n}\n\nfunction Invoke-OctopusApi {\n param(\n [Parameter(Position = 0, Mandatory)]$Uri,\n [ValidateSet(\"Get\", \"Put\")]$Method = 'Get',\n $Body\n )\n $requestParameters = @{\n Uri = ('{0}/{1}' -f $StepTemplate_BaseUrl, $Uri.TrimStart('/'))\n Method = $Method\n Headers = @{ \"X-Octopus-ApiKey\" = $StepTemplate_ApiKey }\n UseBasicParsing = $true\n }\n if ($null -ne $Body) { $requestParameters.Add('Body', ($Body | ConvertTo-Json -Depth 10)) }\n Write-Verbose \"$($Method.ToUpperInvariant()) $($requestParameters.Uri)\" \n Invoke-WebRequest @requestParameters | % Content | ConvertFrom-Json | Write-Output\n}\n\nfunction Check-Scope {\n\tparam(\n \t[Parameter(Position = 0, Mandatory)][string]$ScopeName,\n [Parameter(Position = 1, Mandatory)][array]$ScopeValues,\n [Parameter(Position = 2)][array]$ExistingScopeValue,\n [Parameter(Position = 3)][string]$LookingForScopeValue\n )\n \n if ($LookingForScopeValue) {\n \t\n \tWrite-Host \"Checking $ScopeName Scope\"\n \n $scopes = Create-Scope $ScopeName $ScopeValues $LookingForScopeValue\n \n if (-not ($ExistingScopeValue -and (Compare-Object $ExistingScopeValue $scopes) -eq $null)) {\n \tWrite-Host \"$ScopeName scope does not match\"\n \treturn $false\n }\n Write-Host \"$ScopeName scope matches\"\n } else {\n \tif ($ExistingScopeValue) {\n \tWrite-Host \"$ScopeName scope does not match\"\n \treturn $false\n }\n }\n \n return $true\n}\n\nfunction Create-Scope {\n\tparam(\n \t[Parameter(Position = 0, Mandatory)][string]$ScopeName,\n [Parameter(Position = 1, Mandatory)][array]$ScopeValues,\n [Parameter(Position = 2)][string]$ScopeValue\n )\n \n $scopes = @()\n \n foreach ($scope in $ScopeValue.Split($StepTemplate_ScopeDelimiter)) {\n \tif ($ScopeName -eq \"TenantTag\") {\n \t\t$value = $ScopeValues | Where { $_.Id -eq $scope } | Select -First 1\n \t}\n else {\n \t\t$value = $ScopeValues | Where { $_.Name -eq $scope } | Select -First 1\n \t}\n \t$scopes += $value.Id\n }\n \n return $scopes\n}\n\n$outputVariableKey = \"Octopus.Action[${StepTemplate_DeploymentStep}].Output.${StepTemplate_VariableName}\"\nif (!$OctopusParameters.ContainsKey($outputVariableKey)) {\n throw \"Variable '$StepTemplate_VariableName' has not been output from '$StepTemplate_DeploymentStep'\"\n}\n$isSensitive = [System.Convert]::ToBoolean($StepTemplate_IsSensitive)\n$variableType = if ($isSensitive) { \"Sensitive\" } else { \"String\" }\n\n$variableValue = $OctopusParameters[$outputVariableKey]\nWrite-Host \"Name: $StepTemplate_VariableName\"\nWrite-Host \"Type: $variableType\"\nWrite-Host \"Value: $(if ($isSensitive) { \"********\" } else { $variableValue })\"\nWrite-Host ' '\n\nWrite-Host \"Retrieving $StepTemplate_VariableSetType variable set...\"\nif ($StepTemplate_VariableSetType -eq 'project') {\n $variableSet = Invoke-OctopusApi '/api/projects/all' | ? Name -eq $StepTemplate_VariableSetName | % { Invoke-OctopusApi $_.Links.Variables }\n}\nif ($StepTemplate_VariableSetType -eq 'library') {\n $variableSet = Invoke-OctopusApi '/api/libraryvariablesets/all?ContentType=Variables' | ? Name -eq $StepTemplate_VariableSetName | % { Invoke-OctopusApi $_.Links.Variables }\n}\nif ($null -eq $variableSet) {\n throw \"Unable to find $StepTemplate_VariableSetType variable set '$StepTemplate_VariableSetName'\"\n}\n\n$variableExists = $false\n\n$variableSet.Variables | ? Name -eq $StepTemplate_TargetName | % {\n\tif (-not (Check-Scope 'Environment' $variableSet.ScopeValues.Environments $_.Scope.Environment $StepTemplate_EnvironmentScope)) {\n \treturn\n }\n\n\tif (-not (Check-Scope 'Machine' $variableSet.ScopeValues.Machines $_.Scope.Machine $StepTemplate_MachineScope)) {\n \treturn\n }\n \n if (-not (Check-Scope 'Role' $variableSet.ScopeValues.Roles $_.Scope.Role $StepTemplate_RoleScope)) {\n \treturn\n }\n \n if (-not (Check-Scope 'Action' $variableSet.ScopeValues.Actions $_.Scope.Action $StepTemplate_ActionScope)) {\n \treturn\n }\n \n if (-not (Check-Scope 'Channel' $variableSet.ScopeValues.Channels $_.Scope.Channel $StepTemplate_ChannelScope)) {\n \treturn\n }\n \n if (-not (Check-Scope 'TenantTag' $variableSet.ScopeValues.TenantTags $_.Scope.TenantTag $StepTemplate_TenantTagScope)) {\n \treturn\n }\n\n Write-Host \"Updating existing variable...\"\n Write-Host \"Existing value:\"\n\tWrite-Host \"$(if ($isSensitive) { \"********\" } else { $_.Value })\"\n $_.Value = $variableValue\n $_.Type = $variableType\n $_.IsSensitive = $isSensitive\n $variableExists = $true\n}\n\nif (!$variableExists) {\n Write-Host \"Creating new variable...\"\n \n $variable = @{\n Name = $StepTemplate_TargetName\n Value = $variableValue\n Type = $variableType\n IsSensitive = $isSensitive\n Scope = @{}\n }\n \n if ($StepTemplate_EnvironmentScope) {\n \t$variable.Scope['Environment'] = (Create-Scope 'Environment' $variableSet.ScopeValues.Environments $StepTemplate_EnvironmentScope)\n }\n if ($StepTemplate_RoleScope) {\n \t$variable.Scope['Role'] = (Create-Scope 'Role' $variableSet.ScopeValues.Roles $StepTemplate_RoleScope)\n }\n if ($StepTemplate_MachineScope) {\n \t$variable.Scope['Machine'] = (Create-Scope 'Machine' $variableSet.ScopeValues.Machines $StepTemplate_MachineScope)\n }\n if ($StepTemplate_ActionScope) {\n \t$variable.Scope['Action'] = (Create-Scope 'Action' $variableSet.ScopeValues.Actions $StepTemplate_ActionScope)\n }\n if ($StepTemplate_ChannelScope) {\n \t$variable.Scope['Channel'] = (Create-Scope 'Channel' $variableSet.ScopeValues.Channels $StepTemplate_ChannelScope)\n }\n if ($StepTemplate_TenantTagScope) {\n $variable.Scope['TenantTag'] = (Create-Scope 'TenantTag' $variableSet.ScopeValues.TenantTags $StepTemplate_TenantTagScope)\n }\n \n $variableSet.Variables += $variable\n}\n\nWrite-Host \"Saving updated variable set...\"\nInvoke-OctopusApi $variableSet.Links.Self -Method Put -Body $variableSet | Out-Null\n" + "Octopus.Action.Script.ScriptBody": "$ErrorActionPreference = 'Stop'\n\n$StepTemplate_BaseUrl = $OctopusParameters['Octopus.Web.BaseUrl'].Trim('/')\nif ([string]::IsNullOrWhiteSpace($StepTemplate_ApiKey)) {\n throw \"The step parameter 'API Key' was not found. This step requires an API Key to function, please provide one and try again.\"\n}\n\nfunction Invoke-OctopusApi {\n param(\n [Parameter(Position = 0, Mandatory)]$Uri,\n [ValidateSet(\"Get\", \"Put\")]$Method = 'Get',\n $Body\n )\n $requestParameters = @{\n Uri = ('{0}/{1}' -f $StepTemplate_BaseUrl, $Uri.TrimStart('/'))\n Method = $Method\n Headers = @{ \"X-Octopus-ApiKey\" = $StepTemplate_ApiKey }\n UseBasicParsing = $true\n }\n if ($null -ne $Body) { $requestParameters.Add('Body', ($Body | ConvertTo-Json -Depth 10)) }\n Write-Verbose \"$($Method.ToUpperInvariant()) $($requestParameters.Uri)\" \n Invoke-WebRequest @requestParameters | % Content | ConvertFrom-Json | Write-Output\n}\n\nfunction Test-SpacesApi {\n\tWrite-Verbose \"Checking API compatibility\";\n\t$rootDocument = Invoke-OctopusApi 'api/';\n if($rootDocument.Links -ne $null -and $rootDocument.Links.Spaces -ne $null) {\n \tWrite-Verbose \"Spaces API found\"\n \treturn $true;\n }\n Write-Verbose \"Pre-spaces API found\"\n return $false;\n}\n\nif(Test-SpacesApi) {\n\t$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 recieved a blank value, which could indicate you do not have the correct permissions.\";\n }\n\t$baseApiUrl = \"api/$spaceId\" ;\n} else {\n\t$baseApiUrl = \"api\" ;\n}\n\nfunction Check-Scope {\n\tparam(\n \t[Parameter(Position = 0, Mandatory)][string]$ScopeName,\n [Parameter(Position = 1, Mandatory)][array]$ScopeValues,\n [Parameter(Position = 2)][array]$ExistingScopeValue,\n [Parameter(Position = 3)][string]$LookingForScopeValue\n )\n \n if ($LookingForScopeValue) {\n \t\n \tWrite-Host \"Checking $ScopeName Scope\"\n \n $scopes = Create-Scope $ScopeName $ScopeValues $LookingForScopeValue\n \n if (-not ($ExistingScopeValue -and (Compare-Object $ExistingScopeValue $scopes) -eq $null)) {\n \tWrite-Host \"$ScopeName scope does not match\"\n \treturn $false\n }\n Write-Host \"$ScopeName scope matches\"\n } else {\n \tif ($ExistingScopeValue) {\n \tWrite-Host \"$ScopeName scope does not match\"\n \treturn $false\n }\n }\n \n return $true\n}\n\nfunction Create-Scope {\n\tparam(\n \t[Parameter(Position = 0, Mandatory)][string]$ScopeName,\n [Parameter(Position = 1, Mandatory)][array]$ScopeValues,\n [Parameter(Position = 2)][string]$ScopeValue\n )\n \n $scopes = @()\n \n foreach ($scope in $ScopeValue.Split($StepTemplate_ScopeDelimiter)) {\n \tif ($ScopeName -eq \"TenantTag\") {\n \t\t$value = $ScopeValues | Where { $_.Id -eq $scope } | Select -First 1\n \t}\n else {\n \t\t$value = $ScopeValues | Where { $_.Name -eq $scope } | Select -First 1\n \t}\n \t$scopes += $value.Id\n }\n \n return $scopes\n}\n\n$outputVariableKey = \"Octopus.Action[${StepTemplate_DeploymentStep}].Output.${StepTemplate_VariableName}\"\nif (!$OctopusParameters.ContainsKey($outputVariableKey)) {\n throw \"Variable '$StepTemplate_VariableName' has not been output from '$StepTemplate_DeploymentStep'\"\n}\n$isSensitive = [System.Convert]::ToBoolean($StepTemplate_IsSensitive)\n$variableType = if ($isSensitive) { \"Sensitive\" } else { \"String\" }\n\n$variableValue = $OctopusParameters[$outputVariableKey]\nWrite-Host \"Name: $StepTemplate_VariableName\"\nWrite-Host \"Type: $variableType\"\nWrite-Host \"Value: $(if ($isSensitive) { \"********\" } else { $variableValue })\"\nWrite-Host ' '\n\nWrite-Host \"Retrieving $StepTemplate_VariableSetType variable set...\"\nif ($StepTemplate_VariableSetType -eq 'project') {\n $variableSet = Invoke-OctopusApi \"$baseApiUrl/projects/all\" | ? Name -eq $StepTemplate_VariableSetName | % { Invoke-OctopusApi $_.Links.Variables }\n}\nif ($StepTemplate_VariableSetType -eq 'library') {\n $variableSet = Invoke-OctopusApi \"$baseApiUrl/libraryvariablesets/all?ContentType=Variables\" | ? Name -eq $StepTemplate_VariableSetName | % { Invoke-OctopusApi $_.Links.Variables }\n}\nif ($null -eq $variableSet) {\n throw \"Unable to find $StepTemplate_VariableSetType variable set '$StepTemplate_VariableSetName'\"\n}\n\n$variableExists = $false\n\n$variableSet.Variables | ? Name -eq $StepTemplate_TargetName | % {\n\tif (-not (Check-Scope 'Environment' $variableSet.ScopeValues.Environments $_.Scope.Environment $StepTemplate_EnvironmentScope)) {\n \treturn\n }\n\n\tif (-not (Check-Scope 'Machine' $variableSet.ScopeValues.Machines $_.Scope.Machine $StepTemplate_MachineScope)) {\n \treturn\n }\n \n if (-not (Check-Scope 'Role' $variableSet.ScopeValues.Roles $_.Scope.Role $StepTemplate_RoleScope)) {\n \treturn\n }\n \n if (-not (Check-Scope 'Action' $variableSet.ScopeValues.Actions $_.Scope.Action $StepTemplate_ActionScope)) {\n \treturn\n }\n \n if (-not (Check-Scope 'Channel' $variableSet.ScopeValues.Channels $_.Scope.Channel $StepTemplate_ChannelScope)) {\n \treturn\n }\n \n if (-not (Check-Scope 'TenantTag' $variableSet.ScopeValues.TenantTags $_.Scope.TenantTag $StepTemplate_TenantTagScope)) {\n \treturn\n }\n\n Write-Host \"Updating existing variable...\"\n Write-Host \"Existing value:\"\n\tWrite-Host \"$(if ($isSensitive) { \"********\" } else { $_.Value })\"\n $_.Value = $variableValue\n $_.Type = $variableType\n $_.IsSensitive = $isSensitive\n $variableExists = $true\n}\n\nif (!$variableExists) {\n Write-Host \"Creating new variable...\"\n \n $variable = @{\n Name = $StepTemplate_TargetName\n Value = $variableValue\n Type = $variableType\n IsSensitive = $isSensitive\n Scope = @{}\n }\n \n if ($StepTemplate_EnvironmentScope) {\n \t$variable.Scope['Environment'] = (Create-Scope 'Environment' $variableSet.ScopeValues.Environments $StepTemplate_EnvironmentScope)\n }\n if ($StepTemplate_RoleScope) {\n \t$variable.Scope['Role'] = (Create-Scope 'Role' $variableSet.ScopeValues.Roles $StepTemplate_RoleScope)\n }\n if ($StepTemplate_MachineScope) {\n \t$variable.Scope['Machine'] = (Create-Scope 'Machine' $variableSet.ScopeValues.Machines $StepTemplate_MachineScope)\n }\n if ($StepTemplate_ActionScope) {\n \t$variable.Scope['Action'] = (Create-Scope 'Action' $variableSet.ScopeValues.Actions $StepTemplate_ActionScope)\n }\n if ($StepTemplate_ChannelScope) {\n \t$variable.Scope['Channel'] = (Create-Scope 'Channel' $variableSet.ScopeValues.Channels $StepTemplate_ChannelScope)\n }\n if ($StepTemplate_TenantTagScope) {\n $variable.Scope['TenantTag'] = (Create-Scope 'TenantTag' $variableSet.ScopeValues.TenantTags $StepTemplate_TenantTagScope)\n }\n \n $variableSet.Variables += $variable\n}\n\nWrite-Host \"Saving updated variable set...\"\nInvoke-OctopusApi $variableSet.Links.Self -Method Put -Body $variableSet | Out-Null\n" }, "Parameters": [ { From 0833a7d8e9c21af7e82b60108eda6b6dc0482271 Mon Sep 17 00:00:00 2001 From: Jim Burger Date: Tue, 18 Dec 2018 20:00:11 +1030 Subject: [PATCH 05/16] modified 'chain deploy' step to be space aware --- step-templates/octopus-chain-deployment.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/step-templates/octopus-chain-deployment.json b/step-templates/octopus-chain-deployment.json index 02b7365d3..863906d7c 100644 --- a/step-templates/octopus-chain-deployment.json +++ b/step-templates/octopus-chain-deployment.json @@ -3,12 +3,12 @@ "Name": "Chain Deployment", "Description": "Triggers a deployment of another project in Octopus", "ActionType": "Octopus.Script", - "Version": 16, + "Version": 17, "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\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. - will apply to all steps in the deployment, e.g.\n Octopus.Action.DebugLogging\n Octopus.Action[Step 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 -----\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\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$Chain_BaseUrl = (Get-OctopusSetting OctopusServerUrl $OctopusParameters['Octopus.Web.BaseUrl']).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\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 $Uri = $Uri -replace '{.*?}',''\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 } 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\nenum GuidedFailure {\n Default\n Enabled\n Disabled\n RetryIgnore\n RetryAbort\n Ignore\n RetryDeployment\n}\n\nclass DeploymentContext {\n hidden $BaseUrl\n\n DeploymentContext($baseUrl) {\n $this.BaseUrl = $baseUrl\n }\n\n hidden $Project\n hidden $Lifecycle\n [void] SetProject($projectName) {\n $this.Project = Invoke-OctopusApi '/api/projects/all' | ? 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)$($this.Project.Links.Self)\"\n \n $this.Lifecycle = Invoke-OctopusApi ('/api/lifecycles/{0}' -f $this.Project.LifecycleId)\n Write-Host \"Project Lifecycle: $($this.Lifecycle.Name)\"\n Write-Verbose \"`t$($this.BaseUrl)$($this.Lifecycle.Links.Self)\"\n }\n \n hidden $Channel\n [void] SetChannel($channelName) {\n $useDefaultChannel = Test-String $channelName -ForAbsence\n $this.Channel = Invoke-OctopusApi $this.Project.Links.Channels | % Items | ? { $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)$($this.Channel.Links.Self)\"\n\n if ($null -ne $this.Channel.LifecycleId) {\n $this.Lifecycle = Invoke-OctopusApi ('/api/lifecycles/{0}' -f $this.Channel.LifecycleId)\n Write-Host \"Channel Lifecycle: $($this.Lifecycle.Name)\"\n Write-Verbose \"`t$($this.BaseUrl)$($this.Lifecycle.Links.Self)\" \n }\n }\n\n hidden $Release\n [void] SetRelease($releaseVersion) {\n if (Test-String $releaseVersion) {\n $this.Release = Invoke-OctopusApi ('/api/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 $this.Channel.Links.Releases | % 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)/api/releases/$($this.Release.Id)\"\n }\n [void] CreateRelease($releaseVersion) {\n $template = Invoke-OctopusApi ('{0}/template?channel={1}' -f $this.Project.Links.DeploymentProcess, $this.Channel.Id)\n $selectedPackages = @()\n Write-Host 'Resolving package versions...'\n $template.Packages | % {\n $preReleaseTag = $this.Channel.Rules | ? Actions -contains $_.StepName | ? { $null -ne $_ } | % { '&preReleaseTag={0}' -f $_.Tag }\n\n $package = Invoke-OctopusApi ('/api/feeds/{0}/packages?packageId={1}&partialMatch=false&includeMultipleVersions=false&includeNotes=false&includePreRelease=true&take=1{2}' -f $_.FeedId, $_.PackageId, $preReleaseTag)\n\n Write-Host \"Found $($package.Title) @ $($package.Version) for step $($_.StepName)\"\n $selectedPackages += @{\n StepName = $_.StepName\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 ('/api/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)/api/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 '/api/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)/api/releases/$($this.Release.Id)\"\n }\n\n [void] UpdateVariableSnapshot() {\n $this.Release = Invoke-OctopusApi $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 $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) | % {'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]$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 (?MON|TUE|WED|THU|FRI|SAT|SUN)? - Capture an optional day\n \\s*@\\s* - '@' indicates deploying at a specific time\n (?(?:[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 (?\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)(?:(?MON|TUE|WED|THU|FRI|SAT|SUN)?\\s*@\\s*(?(?:[01]?[0-9]|2[0-3]):[0-5][0-9]))?\\s*(?:\\+\\s*(?\\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] [+ ]'\" \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 | ? Name -eq $environmentName | % {\n $_.AutomaticDeploymentTargets\n $_.OptionalDeploymentTargets\n }\n $this.Environments = $this.DeploymentTemplate.PromoteTo | ? { $_.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 | % 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 | % Id\n $this.Tenants = $tenantFilter.Split(\"`n\") | % { [uri]::EscapeUriString($_.Trim()) } | % {\n $criteria = if ($_ -like '*/*') { 'tags' } else { 'name' }\n \n $tenantResults = Invoke-OctopusApi ('/api/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 } | ? 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 | % 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 | ? Id -eq $tenant.Id | % PromoteTo | ? Id -eq $environment.Id) {\n $logPrefix = ($envPrefix,$tenantPrefix | ? { $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 ('/api/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 ('/api/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\") | % Trim\n $this.SkipActions = $this.DeploymentPreview.StepsToExecute | ? {\n $_.CanBeSkipped -and ($_.ActionName -in $comparisonArray -or $_.ActionNumber -in $comparisonArray)\n } | % {\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 hidden [hashtable]$FormValues\n [void] SetFormValues($formValuesToSet) {\n $this.FormValues = @{}\n $this.DeploymentPreview.Form.Values | Get-Member -MemberType NoteProperty | % {\n $this.FormValues.Add($_.Name, $this.DeploymentPreview.Form.Values.$($_.Name))\n }\n\n $formValuesToSet.Split(\"`n\") | % {\n $entry = $_.Split('=') | % Trim\n $entryName, $entryValues = $entry\n $entry = @($entryName, $($entryValues -join \"=\"))\n $this.DeploymentPreview.Form.Elements | ? { $_.Control.Name -ieq $entry[0] } | % {\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 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 '/api/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)$($deployment.Links.Web)\"\n Write-Verbose \"`t$($this.BaseUrl)$($deployment.Links.Self)\"\n Write-Verbose \"`t$($this.BaseUrl)/api/deploymentprocesses/$($deployment.DeploymentProcessId)\"\n Write-Verbose \"`t$($this.BaseUrl)$($deployment.Links.Variables)\"\n Write-Verbose \"`t$($this.BaseUrl)$($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 ('/api/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 ('/api/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 ('/api/interruptions?regarding={0}' -f $this.Deployment.TaskId) | % Items\n }\n foreach ($activity in $ActivityLogs) {\n $correlatedInterrupts = $interrupts | ? CorrelationId -eq $activity.Id \n $correlatedInterrupts | ? IsPending -eq $false | % { $this.LogInterruptMessages($activity, $_) }\n\n $this.LogStepTransition($activity) \n $this.LogErrorsAndWarnings($activity)\n $correlatedInterrupts | ? IsPending -eq $true | % { \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 | ? Name -eq Instructions | % Control | % 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 $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 | ? 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 $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 $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)\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$($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$($deploymentContext.Release.Links.Web)\"\nif ($Chain_SnapshotVariables -ieq 'True') {\n $deploymentContext.UpdateVariableSnapshot()\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\n$deploymentControllers = $deploymentContext.GetDeploymentControllers()\nif (Test-String $Chain_StepsToSkip) {\n $deploymentControllers | % { $_.SetStepsToSkip($Chain_StepsToSkip) }\n}\nif (Test-String $Chain_FormValues) {\n $deploymentControllers | % { $_.SetFormValues($Chain_FormValues) }\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 | % 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 | % Task | ? FinishedSuccessfully -eq $false) {\n Show-Heading 'Deployment Failed!'\n Write-Fatal (($deploymentControllers | % Task | % 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 ('/api/releases/{0}' -f $OctopusParameters['Octopus.Release.Id']) |\n % { Invoke-OctopusApi $_.Links.ProjectDeploymentProcessSnapshot } |\n % Steps | ? Id -eq $OctopusParameters['Octopus.Step.Id'] |\n % Actions | ? Id -eq $OctopusParameters['Octopus.Action.Id'] |\n % { $_.Properties.Chain_PostDeploy }\nWrite-Verbose \"Raw Post-Deploy Script:`n$rawPostDeployScript\"\n\nAdd-Type -Path (Get-WmiObject Win32_Process | ? ProcessId -eq $PID | % { Get-Process -Id $_.ParentProcessId } | % { Join-Path (Split-Path -Path $_.Path -Parent) 'Octostache.dll' })\n\n$deploymentControllers | % {\n $deployment = $_.Task.Deployment\n $tenant = $_.Tenant\n $variablesDictionary = [Octostache.VariableDictionary]::new()\n Invoke-OctopusApi ('/api/variables/{0}' -f $deployment.ManifestVariableSetId) | % Variables | ? {\n ($_.IsSensitive -eq $false) -and `\n ($_.Scope.Private -ne 'True') -and `\n\t\t($null -eq $_.Scope.Action) -and `\n\t\t($null -eq $_.Scope.Machine) -and `\n ($null -eq $_.Scope.TargetRole) -and `\n\t\t($null -eq $_.Scope.Role) -and `\n ($null -eq $_.Scope.Tenant -or $_.Scope.Tenant -contains $tenant.Id) -and `\n\t\t($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\t\t($null -eq $_.Scope.Channel -or $_.Scope.Channel -contains $deployment.ChannelId) -and `\n\t\t($null -eq $_.Scope.Project -or $_.Scope.Project -contains $deployment.ProjectId)\n } | % { $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}\n", + "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\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. - will apply to all steps in the deployment, e.g.\n Octopus.Action.DebugLogging\n Octopus.Action[Step 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 -----\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\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$Chain_BaseUrl = (Get-OctopusSetting OctopusServerUrl $OctopusParameters['Octopus.Web.BaseUrl']).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\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 $Uri = $Uri -replace '{.*?}',''\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 } 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 Test-SpacesApi {\n\tWrite-Verbose \"Checking API compatibility\";\n\t$rootDocument = Invoke-OctopusApi 'api/';\n if($rootDocument.Links -ne $null -and $rootDocument.Links.Spaces -ne $null) {\n \tWrite-Verbose \"Spaces API found\"\n \treturn $true;\n }\n Write-Verbose \"Pre-spaces API found\"\n return $false;\n}\n\n$Chain_BaseApiUrl = \"/api\"\nif(Test-SpacesApi) {\n\t$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 recieved a blank value, which could indicate you do not have the correct permissions.\";\n }\n\t$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\thidden $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\" | ? 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)$($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)$($this.Lifecycle.Links.Self)\"\n }\n \n hidden $Channel\n [void] SetChannel($channelName) {\n $useDefaultChannel = Test-String $channelName -ForAbsence\n $this.Channel = Invoke-OctopusApi $this.Project.Links.Channels | % Items | ? { $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)$($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)$($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 $this.Channel.Links.Releases | % 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 [void] CreateRelease($releaseVersion) {\n $template = Invoke-OctopusApi ('{0}/template?channel={1}' -f $this.Project.Links.DeploymentProcess, $this.Channel.Id)\n $selectedPackages = @()\n Write-Host 'Resolving package versions...'\n $template.Packages | % {\n $preReleaseTag = $this.Channel.Rules | ? Actions -contains $_.StepName | ? { $null -ne $_ } | % { '&preReleaseTag={0}' -f $_.Tag }\n\n $package = Invoke-OctopusApi (\"$($this.BaseApiUrl)/feeds/{0}/packages?packageId={1}&partialMatch=false&includeMultipleVersions=false&includeNotes=false&includePreRelease=true&take=1{2}\" -f $_.FeedId, $_.PackageId, $preReleaseTag)\n\n Write-Host \"Found $($package.Title) @ $($package.Version) for step $($_.StepName)\"\n $selectedPackages += @{\n StepName = $_.StepName\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 $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 $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) | % {'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]$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 (?MON|TUE|WED|THU|FRI|SAT|SUN)? - Capture an optional day\n \\s*@\\s* - '@' indicates deploying at a specific time\n (?(?:[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 (?\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)(?:(?MON|TUE|WED|THU|FRI|SAT|SUN)?\\s*@\\s*(?(?:[01]?[0-9]|2[0-3]):[0-5][0-9]))?\\s*(?:\\+\\s*(?\\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] [+ ]'\" \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 | ? Name -eq $environmentName | % {\n $_.AutomaticDeploymentTargets\n $_.OptionalDeploymentTargets\n }\n $this.Environments = $this.DeploymentTemplate.PromoteTo | ? { $_.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 | % 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 | % Id\n $this.Tenants = $tenantFilter.Split(\"`n\") | % { [uri]::EscapeUriString($_.Trim()) } | % {\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 } | ? 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 | % 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 | ? Id -eq $tenant.Id | % PromoteTo | ? Id -eq $environment.Id) {\n $logPrefix = ($envPrefix,$tenantPrefix | ? { $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\") | % Trim\n $this.SkipActions = $this.DeploymentPreview.StepsToExecute | ? {\n $_.CanBeSkipped -and ($_.ActionName -in $comparisonArray -or $_.ActionNumber -in $comparisonArray)\n } | % {\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 hidden [hashtable]$FormValues\n [void] SetFormValues($formValuesToSet) {\n $this.FormValues = @{}\n $this.DeploymentPreview.Form.Values | Get-Member -MemberType NoteProperty | % {\n $this.FormValues.Add($_.Name, $this.DeploymentPreview.Form.Values.$($_.Name))\n }\n\n $formValuesToSet.Split(\"`n\") | % {\n $entry = $_.Split('=') | % Trim\n $entryName, $entryValues = $entry\n $entry = @($entryName, $($entryValues -join \"=\"))\n $this.DeploymentPreview.Form.Elements | ? { $_.Control.Name -ieq $entry[0] } | % {\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 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)$($deployment.Links.Web)\"\n Write-Verbose \"`t$($this.BaseUrl)$($deployment.Links.Self)\"\n Write-Verbose \"`t$($this.BaseUrl)$($this.DeploymentContext.BaseApiUrl)/deploymentprocesses/$($deployment.DeploymentProcessId)\"\n Write-Verbose \"`t$($this.BaseUrl)$($deployment.Links.Variables)\"\n Write-Verbose \"`t$($this.BaseUrl)$($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) | % Items\n }\n foreach ($activity in $ActivityLogs) {\n $correlatedInterrupts = $interrupts | ? CorrelationId -eq $activity.Id \n $correlatedInterrupts | ? IsPending -eq $false | % { $this.LogInterruptMessages($activity, $_) }\n\n $this.LogStepTransition($activity) \n $this.LogErrorsAndWarnings($activity)\n $correlatedInterrupts | ? IsPending -eq $true | % { \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 | ? Name -eq Instructions | % Control | % 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 $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 | ? 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 $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 $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$($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$($deploymentContext.Release.Links.Web)\"\nif ($Chain_SnapshotVariables -ieq 'True') {\n $deploymentContext.UpdateVariableSnapshot()\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\n$deploymentControllers = $deploymentContext.GetDeploymentControllers()\nif (Test-String $Chain_StepsToSkip) {\n $deploymentControllers | % { $_.SetStepsToSkip($Chain_StepsToSkip) }\n}\nif (Test-String $Chain_FormValues) {\n $deploymentControllers | % { $_.SetFormValues($Chain_FormValues) }\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 | % 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 | % Task | ? FinishedSuccessfully -eq $false) {\n Show-Heading 'Deployment Failed!'\n Write-Fatal (($deploymentControllers | % Task | % 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']) |\n % { Invoke-OctopusApi $_.Links.ProjectDeploymentProcessSnapshot } |\n % Steps | ? Id -eq $OctopusParameters['Octopus.Step.Id'] |\n % Actions | ? Id -eq $OctopusParameters['Octopus.Action.Id'] |\n % { $_.Properties.Chain_PostDeploy }\nWrite-Verbose \"Raw Post-Deploy Script:`n$rawPostDeployScript\"\n\nAdd-Type -Path (Get-WmiObject Win32_Process | ? ProcessId -eq $PID | % { Get-Process -Id $_.ParentProcessId } | % { Join-Path (Split-Path -Path $_.Path -Parent) 'Octostache.dll' })\n\n$deploymentControllers | % {\n $deployment = $_.Task.Deployment\n $tenant = $_.Tenant\n $variablesDictionary = [Octostache.VariableDictionary]::new()\n Invoke-OctopusApi (\"$Chain_BaseApiUrl/variables/{0}\" -f $deployment.ManifestVariableSetId) | % Variables | ? {\n ($_.IsSensitive -eq $false) -and `\n ($_.Scope.Private -ne 'True') -and `\n\t\t($null -eq $_.Scope.Action) -and `\n\t\t($null -eq $_.Scope.Machine) -and `\n ($null -eq $_.Scope.TargetRole) -and `\n\t\t($null -eq $_.Scope.Role) -and `\n ($null -eq $_.Scope.Tenant -or $_.Scope.Tenant -contains $tenant.Id) -and `\n\t\t($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\t\t($null -eq $_.Scope.Channel -or $_.Scope.Channel -contains $deployment.ChannelId) -and `\n\t\t($null -eq $_.Scope.Project -or $_.Scope.Project -contains $deployment.ProjectId)\n } | % { $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}", "Octopus.Action.Script.ScriptFileName": null, "Octopus.Action.Package.FeedId": null, "Octopus.Action.Package.PackageId": null From c7762e3b5d0d4b9ad1f58f8ace3f2db0e4afc34f Mon Sep 17 00:00:00 2001 From: Jim Burger Date: Tue, 18 Dec 2018 20:10:51 +1030 Subject: [PATCH 06/16] modified 'import certificate' step to be space aware --- step-templates/octopus-import-certificate.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/step-templates/octopus-import-certificate.json b/step-templates/octopus-import-certificate.json index 46b187006..1841793e3 100644 --- a/step-templates/octopus-import-certificate.json +++ b/step-templates/octopus-import-certificate.json @@ -3,12 +3,12 @@ "Name": "Octopus - Import Certificate", "Description": "Create or replace an [Octopus Certificate](https://octopus.com/docs/deploying-applications/certificates) from a certificate file", "ActionType": "Octopus.Script", - "Version": 1, + "Version": 2, "Properties": { "Octopus.Action.Script.Syntax": "PowerShell", "Octopus.Action.Script.ScriptSource": "Inline", "Octopus.Action.RunOnServer": "false", - "Octopus.Action.Script.ScriptBody": "<#\n ----- Octopus - Import Certificate ----- \n Paul Marston @paulmarsy (paul@marston.me)\nLinks\n https://github.com/OctopusDeploy/Library/commits/master/step-templates/octopus-import-certificate.json\n#> \n \n$ErrorActionPreference = 'Stop'\n\n$StepTemplate_BaseUrl = $OctopusParameters['Octopus.Web.BaseUrl'].Trim('/')\nif ([string]::IsNullOrWhiteSpace($StepTemplate_ApiKey)) {\n throw \"The step parameter 'API Key' was not found. This step requires an API Key to function, please provide one and try again.\"\n}\nfilter Out-Verbose {\n Write-Verbose ($_ | Out-String)\n}\nfilter Out-Indented {\n $_ | Out-String | % Trim | % Split \"`n\" | % { \"`t$_\" } \n}\nfunction Invoke-OctopusApi {\n param(\n [Parameter(Position = 0, Mandatory)]$Uri,\n [ValidateSet(\"Get\", \"Post\")]$Method = 'Get',\n $Body\n )\n $requestParameters = @{\n Uri = ('{0}/{1}' -f $StepTemplate_BaseUrl, $Uri.TrimStart('/'))\n Method = $Method\n Headers = @{ \"X-Octopus-ApiKey\" = $StepTemplate_ApiKey }\n UseBasicParsing = $true\n }\n Write-Verbose \"$($Method.ToUpperInvariant()) $($requestParameters.Uri)\" \n if ($null -ne $Body) { $requestParameters.Add('Body', ($Body | ConvertTo-Json -Depth 10)) }\n try {\n Invoke-WebRequest @requestParameters | % Content | ConvertFrom-Json | Write-Output\n }\n catch [System.Net.WebException] {\n if ($_.Exception.Response) {\n $errorResponse = [System.IO.StreamReader]::new($_.Exception.Response.GetResponseStream()).ReadToEnd()\n throw (\"$($_.Exception.Message)`n{0}\" -f $errorResponse)\n }\n }\n}\n\n$certificate = switch ($StepTemplate_CertEncoding) {\n 'file' { \n if (!(Test-Path $StepTemplate_Certificate)) {\n throw \"Certificate file $StepTemplate_Certificate does not exist\"\n }\n $certificateBytes = Get-Content -Path $StepTemplate_Certificate -Encoding Byte\n [System.Convert]::ToBase64String($certificateBytes)\n }\n 'base64' {\n $StepTemplate_Certificate\n }\n}\n\n$existingCert = Invoke-OctopusApi '/api/certificates' | % Items | ? Name -eq $StepTemplate_CertificateName\nif ($existingCert) {\n Write-Host 'Existing certificate will be archived & replaced...'\n Invoke-OctopusApi ('/api/certificates/{0}/replace' -f $existingCert.Id) -Method Post -Body @{\n certificateData = $certificate\n password = $StepTemplate_Password\n } | % {\n $_.CertificateData = $null\n $_.Password = $null\n $_\n } | Out-Verbose\n} else {\n Write-Host 'Creating & importing new certificate...'\n Invoke-OctopusApi '/api/certificates' -Method Post -Body @{\n Name = $StepTemplate_CertificateName\n CertificateData = @{\n HasValue = $true\n NewValue = $certificate\n }\n Password = @{\n HasValue = $true\n NewValue = $StepTemplate_Password\n }\n } | Out-Verbose\n}\nWrite-Host 'Certificate has been imported:'\nInvoke-OctopusApi '/api/certificates' | % Items | ? Name -eq $StepTemplate_CertificateName | Out-Indented", + "Octopus.Action.Script.ScriptBody": "<#\n ----- Octopus - Import Certificate ----- \n Paul Marston @paulmarsy (paul@marston.me)\nLinks\n https://github.com/OctopusDeploy/Library/commits/master/step-templates/octopus-import-certificate.json\n#> \n \n$ErrorActionPreference = 'Stop'\n\n$StepTemplate_BaseUrl = $OctopusParameters['Octopus.Web.BaseUrl'].Trim('/')\nif ([string]::IsNullOrWhiteSpace($StepTemplate_ApiKey)) {\n throw \"The step parameter 'API Key' was not found. This step requires an API Key to function, please provide one and try again.\"\n}\nfilter Out-Verbose {\n Write-Verbose ($_ | Out-String)\n}\nfilter Out-Indented {\n $_ | Out-String | % Trim | % Split \"`n\" | % { \"`t$_\" } \n}\nfunction Invoke-OctopusApi {\n param(\n [Parameter(Position = 0, Mandatory)]$Uri,\n [ValidateSet(\"Get\", \"Post\")]$Method = 'Get',\n $Body\n )\n $requestParameters = @{\n Uri = ('{0}/{1}' -f $StepTemplate_BaseUrl, $Uri.TrimStart('/'))\n Method = $Method\n Headers = @{ \"X-Octopus-ApiKey\" = $StepTemplate_ApiKey }\n UseBasicParsing = $true\n }\n Write-Verbose \"$($Method.ToUpperInvariant()) $($requestParameters.Uri)\" \n if ($null -ne $Body) { $requestParameters.Add('Body', ($Body | ConvertTo-Json -Depth 10)) }\n try {\n Invoke-WebRequest @requestParameters | % Content | ConvertFrom-Json | Write-Output\n }\n catch [System.Net.WebException] {\n if ($_.Exception.Response) {\n $errorResponse = [System.IO.StreamReader]::new($_.Exception.Response.GetResponseStream()).ReadToEnd()\n throw (\"$($_.Exception.Message)`n{0}\" -f $errorResponse)\n }\n }\n}\n\nfunction Test-SpacesApi {\n\tWrite-Verbose \"Checking API compatibility\";\n\t$rootDocument = Invoke-OctopusApi 'api/';\n if($rootDocument.Links -ne $null -and $rootDocument.Links.Spaces -ne $null) {\n \tWrite-Verbose \"Spaces API found\"\n \treturn $true;\n }\n Write-Verbose \"Pre-spaces API found\"\n return $false;\n}\n\nif(Test-SpacesApi) {\n\t$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 recieved a blank value, which could indicate you do not have the correct permissions.\";\n }\n\t$baseApiUrl = \"/api/$spaceId\" ;\n} else {\n\t$baseApiUrl = \"/api\" ;\n}\n\n$certificate = switch ($StepTemplate_CertEncoding) {\n 'file' { \n if (!(Test-Path $StepTemplate_Certificate)) {\n throw \"Certificate file $StepTemplate_Certificate does not exist\"\n }\n $certificateBytes = Get-Content -Path $StepTemplate_Certificate -Encoding Byte\n [System.Convert]::ToBase64String($certificateBytes)\n }\n 'base64' {\n $StepTemplate_Certificate\n }\n}\n\n$existingCert = Invoke-OctopusApi \"$baseApiUrl/certificates\" | % Items | ? Name -eq $StepTemplate_CertificateName\nif ($existingCert) {\n Write-Host 'Existing certificate will be archived & replaced...'\n Invoke-OctopusApi (\"$baseApiUrl/certificates/{0}/replace\" -f $existingCert.Id) -Method Post -Body @{\n certificateData = $certificate\n password = $StepTemplate_Password\n } | % {\n $_.CertificateData = $null\n $_.Password = $null\n $_\n } | Out-Verbose\n} else {\n Write-Host 'Creating & importing new certificate...'\n Invoke-OctopusApi \"$baseApiUrl/certificates\" -Method Post -Body @{\n Name = $StepTemplate_CertificateName\n CertificateData = @{\n HasValue = $true\n NewValue = $certificate\n }\n Password = @{\n HasValue = $true\n NewValue = $StepTemplate_Password\n }\n } | Out-Verbose\n}\nWrite-Host 'Certificate has been imported:'\nInvoke-OctopusApi \"$baseApiUrl/certificates\" | % Items | ? Name -eq $StepTemplate_CertificateName | Out-Indented", "Octopus.Action.Script.ScriptFileName": null, "Octopus.Action.Package.FeedId": null, "Octopus.Action.Package.PackageId": null From 2c5a33218329ef0c67f765cfafca63e507f4d8d6 Mon Sep 17 00:00:00 2001 From: Jim Burger Date: Tue, 18 Dec 2018 20:16:36 +1030 Subject: [PATCH 07/16] modified 'Create tenant' step to be space aware --- step-templates/create-tenant.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/step-templates/create-tenant.json b/step-templates/create-tenant.json index 9b82e9735..8c8fc14c7 100644 --- a/step-templates/create-tenant.json +++ b/step-templates/create-tenant.json @@ -3,12 +3,12 @@ "Name": "Create Tenant", "Description": "Create an Octopus [tenant](https://octopus.com/docs/deployment-patterns/multi-tenant-deployments) with optional tenant tags and project connections.", "ActionType": "Octopus.Script", - "Version": 1, + "Version": 2, "Packages": [], "Properties": { "Octopus.Action.Script.ScriptSource": "Inline", "Octopus.Action.Script.Syntax": "PowerShell", - "Octopus.Action.Script.ScriptBody": "$ErrorActionPreference = 'Stop'\n\n$apiKey = $CreateTenantStep_ApiKey\n$tenantName = $CreateTenantStep_TenantName\n$tenantTags = if ($CreateTenantStep_TenantTags -eq $null) { @() } else { $CreateTenantStep_TenantTags | ConvertFrom-Json }\n$projectEnvironments = if ($CreateTenantStep_ProjectEnvironments -eq $null) { @{} } else { $CreateTenantStep_ProjectEnvironments | ConvertFrom-Json }\n$octopusBaseUrl = $OctopusParameters['Octopus.Web.BaseUrl'].Trim('/')\n\nif ([string]::IsNullOrWhiteSpace($apiKey)) {\n throw \"The step parameter 'Octopus API Key' was not found. This step requires an API Key to function, please provide one and try again.\"\n}\n\nfunction Invoke-OctopusApi {\n param(\n [Parameter(Position = 0, Mandatory)]$Uri,\n [ValidateSet(\"Get\", \"Post\", \"Put\", \"Delete\")]$Method = 'Get',\n $Body\n )\n $requestParameters = @{\n Uri = ('{0}/{1}' -f $octopusBaseUrl, $Uri.TrimStart('/'))\n Method = $Method\n Headers = @{ \"X-Octopus-ApiKey\" = $apiKey }\n UseBasicParsing = $true\n }\n if ($null -ne $Body) { $requestParameters.Add('Body', ($Body | ConvertTo-Json -Depth 10)) }\n Write-Verbose \"$($Method.ToUpperInvariant()) $($requestParameters.Uri)\" \n \n return Invoke-WebRequest @requestParameters | % Content | ConvertFrom-Json\n}\n\n$body = @{\n\tId = $null\n Name = $tenantName\n TenantTags = @($tenantTags)\n ProjectEnvironments = $projectEnvironments #@{ \"Projects-63\" = @(\"Environments-1\",\"Environments-2\") }\n}\n\nInvoke-OctopusApi \"/api/tenants\" -Method Post -Body $body | Out-Null" + "Octopus.Action.Script.ScriptBody": "$ErrorActionPreference = 'Stop'\n\n$apiKey = $CreateTenantStep_ApiKey\n$tenantName = $CreateTenantStep_TenantName\n$tenantTags = if ($CreateTenantStep_TenantTags -eq $null) { @() } else { $CreateTenantStep_TenantTags | ConvertFrom-Json }\n$projectEnvironments = if ($CreateTenantStep_ProjectEnvironments -eq $null) { @{} } else { $CreateTenantStep_ProjectEnvironments | ConvertFrom-Json }\n$octopusBaseUrl = $OctopusParameters['Octopus.Web.BaseUrl'].Trim('/')\n\nif ([string]::IsNullOrWhiteSpace($apiKey)) {\n throw \"The step parameter 'Octopus API Key' was not found. This step requires an API Key to function, please provide one and try again.\"\n}\n\nfunction Invoke-OctopusApi {\n param(\n [Parameter(Position = 0, Mandatory)]$Uri,\n [ValidateSet(\"Get\", \"Post\", \"Put\", \"Delete\")]$Method = 'Get',\n $Body\n )\n $requestParameters = @{\n Uri = ('{0}/{1}' -f $octopusBaseUrl, $Uri.TrimStart('/'))\n Method = $Method\n Headers = @{ \"X-Octopus-ApiKey\" = $apiKey }\n UseBasicParsing = $true\n }\n if ($null -ne $Body) { $requestParameters.Add('Body', ($Body | ConvertTo-Json -Depth 10)) }\n Write-Verbose \"$($Method.ToUpperInvariant()) $($requestParameters.Uri)\" \n \n return Invoke-WebRequest @requestParameters | % Content | ConvertFrom-Json\n}\n\nfunction Test-SpacesApi {\n\tWrite-Verbose \"Checking API compatibility\";\n\t$rootDocument = Invoke-OctopusApi 'api/';\n if($rootDocument.Links -ne $null -and $rootDocument.Links.Spaces -ne $null) {\n \tWrite-Verbose \"Spaces API found\"\n \treturn $true;\n }\n Write-Verbose \"Pre-spaces API found\"\n return $false;\n}\n\nif(Test-SpacesApi) {\n\t$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 recieved a blank value, which could indicate you do not have the correct permissions.\";\n }\n\t$baseApiUrl = \"/api/$spaceId\" ;\n} else {\n\t$baseApiUrl = \"/api\" ;\n}\n\n$body = @{\n\tId = $null\n Name = $tenantName\n TenantTags = @($tenantTags)\n ProjectEnvironments = $projectEnvironments #@{ \"Projects-63\" = @(\"Environments-1\",\"Environments-2\") }\n}\n\nInvoke-OctopusApi \"$baseApiUrl/tenants\" -Method Post -Body $body | Out-Null" }, "Parameters": [ { From e81c9570e147ccb6429d90b5adda4ca8fb785733 Mon Sep 17 00:00:00 2001 From: Jim Burger Date: Tue, 18 Dec 2018 20:29:29 +1030 Subject: [PATCH 08/16] Modified 'consolidate release notes' step to be space aware --- step-templates/octopus-consolidate-releasenotes.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/step-templates/octopus-consolidate-releasenotes.json b/step-templates/octopus-consolidate-releasenotes.json index ac134ce42..8850e7ab7 100644 --- a/step-templates/octopus-consolidate-releasenotes.json +++ b/step-templates/octopus-consolidate-releasenotes.json @@ -3,12 +3,12 @@ "Name": "Consolidate Release Notes", "Description": "Consolidates all Release Notes between the last successful release in the current Environment and this one by merging or concatenating them.", "ActionType": "Octopus.Script", - "Version": 9, + "Version": 10, "Properties": { "Octopus.Action.Script.Syntax": "PowerShell", "Octopus.Action.Script.ScriptSource": "Inline", "Octopus.Action.RunOnServer": "true", - "Octopus.Action.Script.ScriptBody": "$baseUri = $OctopusParameters['Octopus.Web.BaseUrl']\r\n$reqheaders = @{\"X-Octopus-ApiKey\" = $Consolidate_ApiKey }\r\n$putReqHeaders = @{\"X-HTTP-Method-Override\" = \"PUT\"; \"X-Octopus-ApiKey\" = $Consolidate_ApiKey }\r\n\r\n$remWhiteSpace = [bool]::Parse($Consolidate_RemoveWhitespace)\r\n$deDupe = [bool]::Parse($Consolidate_Dedupe)\r\n$reverse = ($Consolidate_Order -eq \"Oldest\")\r\n\r\n# Get details we'll need\r\n$projectId = $OctopusParameters['Octopus.Project.Id']\r\n$thisReleaseNumber = $OctopusParameters['Octopus.Release.Number']\r\n$lastSuccessfulReleaseId = $OctopusParameters['Octopus.Release.CurrentForEnvironment.Id']\r\n$lastSuccessfulReleaseNumber = $OctopusParameters['Octopus.Release.CurrentForEnvironment.Number']\r\n\r\n# Get all previous releases to this environment\r\n$releaseUri = \"$baseUri/api/projects/$projectId/releases\"\r\ntry {\r\n $allReleases = Invoke-WebRequest $releaseUri -Headers $reqheaders -UseBasicParsing | ConvertFrom-Json\r\n} catch {\r\n if ($_.Exception.Response.StatusCode.Value__ -ne 404) {\r\n $result = $_.Exception.Response.GetResponseStream()\r\n $reader = New-Object System.Io.StreamReader($result);\r\n $responseBody = $reader.ReadToEnd();\r\n throw \"Error occurred: $responseBody\"\r\n }\r\n}\r\n\r\n# Find and aggregate release notes\r\n$aggregateNotes = @()\r\n\r\nWrite-Host \"Finding all release notes between the last successful release: $lastSuccessfulReleaseNumber and this release: $thisReleaseNumber\"\r\nforeach ($rel in $allReleases.Items) {\r\n if ($rel.Id -ne $lastSuccessfulReleaseId) {\r\n Write-Host \"Found release notes for $($rel.Version)\"\r\n $theseNotes = @()\r\n #split into lines\r\n $lines = $rel.ReleaseNotes -split \"`n\"\r\n foreach ($line in $lines) {\r\n if (-not $remWhitespace -or -not [string]::IsNullOrWhiteSpace($line)) {\r\n if (-not $deDupe -or -not $aggregateNotes.Contains($line)) {\r\n $theseNotes = $theseNotes + $line\r\n }\r\n }\r\n }\r\n if ($reverse) {\r\n $aggregateNotes = $theseNotes + $aggregateNotes\r\n } else {\r\n $aggregateNotes = $aggregateNotes + $theseNotes\r\n }\r\n } else {\r\n break\r\n }\r\n}\r\n$aggregateNotesText = $aggregateNotes -join \"`n`n\"\r\n\r\n# Get the current release\r\n$releaseUri = \"$baseUri/api/projects/$projectId/releases/$thisReleaseNumber\"\r\ntry {\r\n $currentRelease = Invoke-WebRequest $releaseUri -Headers $reqheaders -UseBasicParsing | ConvertFrom-Json\r\n} catch {\r\n if ($_.Exception.Response.StatusCode.Value__ -ne 404) {\r\n $result = $_.Exception.Response.GetResponseStream()\r\n $reader = New-Object System.Io.StreamReader($result);\r\n $responseBody = $reader.ReadToEnd();\r\n throw \"Error occurred: $responseBody\"\r\n }\r\n}\r\n\r\n# Update the release notes for the current release\r\n$currentRelease.ReleaseNotes = $aggregateNotesText\r\nWrite-Host \"Updating release notes for $thisReleaseNumber`:`n`n\"\r\nWrite-Host $aggregateNotesText\r\ntry {\r\n $releaseUri = \"$baseUri/api/releases/$($currentRelease.Id)\"\r\n $currentReleaseBody = $currentRelease | ConvertTo-Json\r\n $result = Invoke-WebRequest $releaseUri -Method Post -Headers $putReqHeaders -Body $currentReleaseBody -UseBasicParsing | ConvertFrom-Json\r\n} catch {\r\n $result = $_.Exception.Response.GetResponseStream()\r\n $reader = New-Object System.Io.StreamReader($result);\r\n $responseBody = $reader.ReadToEnd();\r\n Write-Host $responseBody\r\n throw \"Error occurred: $responseBody\"\r\n}", + "Octopus.Action.Script.ScriptBody": "$baseUri = $OctopusParameters['Octopus.Web.BaseUrl']\n$reqheaders = @{\"X-Octopus-ApiKey\" = $Consolidate_ApiKey }\n$putReqHeaders = @{\"X-HTTP-Method-Override\" = \"PUT\"; \"X-Octopus-ApiKey\" = $Consolidate_ApiKey }\n\n$remWhiteSpace = [bool]::Parse($Consolidate_RemoveWhitespace)\n$deDupe = [bool]::Parse($Consolidate_Dedupe)\n$reverse = ($Consolidate_Order -eq \"Oldest\")\n\n# Get details we'll need\n$projectId = $OctopusParameters['Octopus.Project.Id']\n$thisReleaseNumber = $OctopusParameters['Octopus.Release.Number']\n$lastSuccessfulReleaseId = $OctopusParameters['Octopus.Release.CurrentForEnvironment.Id']\n$lastSuccessfulReleaseNumber = $OctopusParameters['Octopus.Release.CurrentForEnvironment.Number']\n\nfunction Test-SpacesApi {\n\tWrite-Verbose \"Checking API compatibility\";\n\t$rootDocument = Invoke-WebRequest \"$baseUri/api\" -Headers $reqHeaders -UseBasicParsing | ConvertFrom-Json;\n if($rootDocument.Links -ne $null -and $rootDocument.Links.Spaces -ne $null) {\n \tWrite-Verbose \"Spaces API found\"\n \treturn $true;\n }\n Write-Verbose \"Pre-spaces API found\"\n return $false;\n}\n\nif(Test-SpacesApi) {\n\t$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 recieved a blank value, which could indicate you do not have the correct permissions.\";\n }\n\t$baseApiUrl = \"/api/$spaceId\" ;\n} else {\n\t$baseApiUrl = \"/api\" ;\n}\n\n# Get all previous releases to this environment\n$releaseUri = \"$baseUri$baseApiUrl/projects/$projectId/releases\"\ntry {\n $allReleases = Invoke-WebRequest $releaseUri -Headers $reqheaders -UseBasicParsing | ConvertFrom-Json\n} catch {\n if ($_.Exception.Response.StatusCode.Value__ -ne 404) {\n $result = $_.Exception.Response.GetResponseStream()\n $reader = New-Object System.Io.StreamReader($result);\n $responseBody = $reader.ReadToEnd();\n throw \"Error occurred: $responseBody\"\n }\n}\n\n# Find and aggregate release notes\n$aggregateNotes = @()\n\nWrite-Host \"Finding all release notes between the last successful release: $lastSuccessfulReleaseNumber and this release: $thisReleaseNumber\"\nforeach ($rel in $allReleases.Items) {\n if ($rel.Id -ne $lastSuccessfulReleaseId) {\n Write-Host \"Found release notes for $($rel.Version)\"\n $theseNotes = @()\n #split into lines\n $lines = $rel.ReleaseNotes -split \"`n\"\n foreach ($line in $lines) {\n if (-not $remWhitespace -or -not [string]::IsNullOrWhiteSpace($line)) {\n if (-not $deDupe -or -not $aggregateNotes.Contains($line)) {\n $theseNotes = $theseNotes + $line\n }\n }\n }\n if ($reverse) {\n $aggregateNotes = $theseNotes + $aggregateNotes\n } else {\n $aggregateNotes = $aggregateNotes + $theseNotes\n }\n } else {\n break\n }\n}\n$aggregateNotesText = $aggregateNotes -join \"`n`n\"\n\n# Get the current release\n$releaseUri = \"$baseUri$baseApiUrl/projects/$projectId/releases/$thisReleaseNumber\"\ntry {\n $currentRelease = Invoke-WebRequest $releaseUri -Headers $reqheaders -UseBasicParsing | ConvertFrom-Json\n} catch {\n if ($_.Exception.Response.StatusCode.Value__ -ne 404) {\n $result = $_.Exception.Response.GetResponseStream()\n $reader = New-Object System.Io.StreamReader($result);\n $responseBody = $reader.ReadToEnd();\n throw \"Error occurred: $responseBody\"\n }\n}\n\n# Update the release notes for the current release\n$currentRelease.ReleaseNotes = $aggregateNotesText\nWrite-Host \"Updating release notes for $thisReleaseNumber`:`n`n\"\nWrite-Host $aggregateNotesText\ntry {\n $releaseUri = \"$baseUri$baseApiUrl/releases/$($currentRelease.Id)\"\n $currentReleaseBody = $currentRelease | ConvertTo-Json\n $result = Invoke-WebRequest $releaseUri -Method Post -Headers $putReqHeaders -Body $currentReleaseBody -UseBasicParsing | ConvertFrom-Json\n} catch {\n $result = $_.Exception.Response.GetResponseStream()\n $reader = New-Object System.Io.StreamReader($result);\n $responseBody = $reader.ReadToEnd();\n Write-Host $responseBody\n throw \"Error occurred: $responseBody\"\n}", "Octopus.Action.Script.ScriptFileName": null, "Octopus.Action.Package.FeedId": null, "Octopus.Action.Package.PackageId": null From aa3fc9ba293eef409d6fb3d8ca91260491bb3834 Mon Sep 17 00:00:00 2001 From: Jim Burger Date: Tue, 18 Dec 2018 20:39:37 +1030 Subject: [PATCH 09/16] modified 'set octo release notes from TFS Query' step to be space aware --- .../octopus-set-Octopus-releaese-notes-from-TFS-query.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/step-templates/octopus-set-Octopus-releaese-notes-from-TFS-query.json b/step-templates/octopus-set-Octopus-releaese-notes-from-TFS-query.json index 84e01b63b..8dce2a209 100644 --- a/step-templates/octopus-set-Octopus-releaese-notes-from-TFS-query.json +++ b/step-templates/octopus-set-Octopus-releaese-notes-from-TFS-query.json @@ -3,12 +3,12 @@ "Name": "Set Octopus release notes from TFS query", "Description": "Sets Octopus release notes from TFS query", "ActionType": "Octopus.Script", - "Version": 3, + "Version": 4, "CommunityActionTemplateId": null, "Properties": { "Octopus.Action.Script.ScriptSource": "Inline", "Octopus.Action.Script.Syntax": "PowerShell", - "Octopus.Action.Script.ScriptBody": "#TFS\n$instance = $OctopusParameters[\"tfsInstance\"]\n$collection = $OctopusParameters[\"tfsCollection\"]\n$project = $OctopusParameters[\"tfsProject\"]\n$PAT = $OctopusParameters[\"tfsPat\"]\n$pathquery = $OctopusParameters[\"tfsPathQuery\"]\n\n#Octopus\n$octopusAPIKey = $OctopusParameters['octopusAPIKey']\n$baseUri = $OctopusParameters['Octopus.Web.BaseUrl']\n$octopusProjectId = $OctopusParameters['Octopus.Project.Id']\n$thisReleaseNumber = $OctopusParameters['Octopus.Release.Number']\n\nwrite-host \"Instance: $($instance)\"\nwrite-host \"collection: $($collection)\"\nwrite-host \"project: $($project)\"\nwrite-host \"baseUri: $($baseUri)\"\nwrite-host \"projectId: $($projectId)\"\nwrite-host \"thisReleaseNumber: $($thisReleaseNumber)\"\nwrite-host \"TFS path: $($pathquery)\"\n\n#Create HEADERS\n$bytes = [System.Text.Encoding]::ASCII.GetBytes($PAT)\n$base64 = [System.Convert]::ToBase64String($bytes)\n$basicAuthValue = \"Basic $base64\"\n$headers = @{ }\n$headers.Add(\"Authorization\", $basicAuthValue)\n$headers.Add(\"Accept\",\"application/json\")\n$headers.Add(\"Content-Type\",\"application/json\")\n\n$reqheaders = @{\"X-Octopus-ApiKey\" = $octopusAPIKey }\n$putReqHeaders = @{\"X-HTTP-Method-Override\" = \"PUT\"; \"X-Octopus-ApiKey\" = $octopusAPIKey }\n\n# Get the current release\n$releaseUri = \"$baseUri/api/projects/$octopusProjectId/releases/$thisReleaseNumber\"\nwrite-host \"Release uri $($releaseUri)\"\ntry {\n $currentRelease = Invoke-RestMethod $releaseUri -Headers $reqheaders -UseBasicParsing \n} catch {\n if ($_.Exception.Response.StatusCode.Value__ -ne 404) {\n $result = $_.Exception.Response.GetResponseStream()\n $reader = New-Object System.Io.StreamReader($result);\n $responseBody = $reader.ReadToEnd();\n throw \"Error occurred: $responseBody\"\n }\n}\n\nif(![string]::IsNullOrWhiteSpace($currentRelease.ReleaseNotes)){\n\twrite-host \"Release notes already filled in. $($currentRelease.ReleaseNotes)\"\n Set-OctopusVariable -name \"ReleaseNotes\" -value $releaseNotes\n\texit;\n}\n\n\n#Get projectid\n$url = \"http://$($instance)/tfs/$($collection)/$($projectId)/_apis/projects/$($project)?&includeCapabilities=false&includeHistory=false&api-version=2.2\"\nwrite-host \"Invoking url= $($url)\"\n$projectresponse = Invoke-RestMethod $url -Method GET -Headers $headers\n\n$projectid = $projectresponse.id\nwrite-host \"projectid $($projectid)\"\n\n#Get the ID of the query to execute\n$queryResult = Invoke-RestMethod \"http://$($instance)/tfs/$($collection)/$($projectId)/_apis/$($pathquery)?$depth=1&api-version=2.2\" -Method GET -Headers $headers\nwrite-host \"queryResult $($queryResult)\"\n\n#https://{instance}/DefaultCollection/[{project}/]_apis/wit/wiql/{id}?api-version={version}\n$queryResult = Invoke-RestMethod \"http://$($instance)/tfs/$($collection)/$($projectId)/_apis/wit/wiql/$($queryResult.Id)?api-version=2.2\" -Method GET -Headers $headers\n\nWrite-Host \"Found $($queryResult.workItems.Count) number of workitems for query: ReleaseNotes$($releaseTag)\"\n\n$releaseNotes = \"**Work Items:**\"\n\n\nif($queryResult.workItems.Count -eq 0)\n{\n\tWrite-Host \"No work items for release\"\n\t$releaseNotes = \"`n no new work items\"\n}\nelse\n{\n\t#Create a list of ids\n\t$ids = [string]::Join(\"%2C\", ($queryResult.workItems.id))\n\n\t#Get all the work items\n\t$workItems = Invoke-RestMethod \"http://$($instance)/tfs/$($collection)/_apis/wit/workItems?ids=$($ids)&fields=System.Title\" -Method GET -Headers $headers\n\n\tforeach($workItem in $workItems.value)\n\t{\n\t\t#Add line for each work item\n\t\t$releaseNotes = $releaseNotes + \"`n* [$($workItem.id)] (http://$($instance)/tfs/$($collection)/9981e67f-b27c-4628-b5cf-fba1d327aa07/_workitems/edit/$($workItem.id)) : $($workItem.fields.'System.Title')\"\n\t}\n\n}\n\n\n\n# Update the release notes for the current release\n$currentRelease.ReleaseNotes = $releaseNotes \nwrite-host \"Release notes $($currentRelease.ReleaseNotes)\"\nWrite-Host \"Updating release notes for $thisReleaseNumber`:`n`n\"\ntry {\n $releaseUri = \"$baseUri/api/releases/$($currentRelease.Id)\"\n write-host \"Release uri $($releaseUri)\"\n $currentReleaseBody = $currentRelease | ConvertTo-Json\n write-host \"Current release body $($currentReleaseBody)\"\n $result = Invoke-RestMethod $releaseUri -Method Post -Headers $putReqHeaders -Body $currentReleaseBody -UseBasicParsing\n\twrite-host \"result $($result)\"\n} catch {\n $result = $_.Exception.Response.GetResponseStream()\n $reader = New-Object System.Io.StreamReader($result);\n $responseBody = $reader.ReadToEnd();\n Write-Host \"error $($responseBody)\"\n throw \"Error occurred: $responseBody\"\n}\n\nSet-OctopusVariable -name \"ReleaseNotes\" -value $releaseNotes\n" + "Octopus.Action.Script.ScriptBody": "#TFS\n$instance = $OctopusParameters[\"tfsInstance\"]\n$collection = $OctopusParameters[\"tfsCollection\"]\n$project = $OctopusParameters[\"tfsProject\"]\n$PAT = $OctopusParameters[\"tfsPat\"]\n$pathquery = $OctopusParameters[\"tfsPathQuery\"]\n\n#Octopus\n$octopusAPIKey = $OctopusParameters['octopusAPIKey']\n$baseUri = $OctopusParameters['Octopus.Web.BaseUrl']\n$octopusProjectId = $OctopusParameters['Octopus.Project.Id']\n$thisReleaseNumber = $OctopusParameters['Octopus.Release.Number']\n\nwrite-host \"Instance: $($instance)\"\nwrite-host \"collection: $($collection)\"\nwrite-host \"project: $($project)\"\nwrite-host \"baseUri: $($baseUri)\"\nwrite-host \"projectId: $($projectId)\"\nwrite-host \"thisReleaseNumber: $($thisReleaseNumber)\"\nwrite-host \"TFS path: $($pathquery)\"\n\n#Create HEADERS\n$bytes = [System.Text.Encoding]::ASCII.GetBytes($PAT)\n$base64 = [System.Convert]::ToBase64String($bytes)\n$basicAuthValue = \"Basic $base64\"\n$headers = @{ }\n$headers.Add(\"Authorization\", $basicAuthValue)\n$headers.Add(\"Accept\",\"application/json\")\n$headers.Add(\"Content-Type\",\"application/json\")\n\n$reqheaders = @{\"X-Octopus-ApiKey\" = $octopusAPIKey }\n$putReqHeaders = @{\"X-HTTP-Method-Override\" = \"PUT\"; \"X-Octopus-ApiKey\" = $octopusAPIKey }\n\nfunction Test-SpacesApi {\n\tWrite-Verbose \"Checking API compatibility\";\n\t$rootDocument = Invoke-WebRequest \"$baseUri/api\" -Headers $reqheaders -UseBasicParsing | ConvertFrom-Json;\n if($rootDocument.Links -ne $null -and $rootDocument.Links.Spaces -ne $null) {\n \tWrite-Verbose \"Spaces API found\"\n \treturn $true;\n }\n Write-Verbose \"Pre-spaces API found\"\n return $false;\n}\n\nif(Test-SpacesApi) {\n\t$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 recieved a blank value, which could indicate you do not have the correct permissions.\";\n }\n\t$baseApiUrl = \"/api/$spaceId\" ;\n} else {\n\t$baseApiUrl = \"/api\" ;\n}\n\n# Get the current release\n$releaseUri = \"$baseUri$baseApiUrl/projects/$octopusProjectId/releases/$thisReleaseNumber\"\nwrite-host \"Release uri $($releaseUri)\"\ntry {\n $currentRelease = Invoke-RestMethod $releaseUri -Headers $reqheaders -UseBasicParsing \n} catch {\n if ($_.Exception.Response.StatusCode.Value__ -ne 404) {\n $result = $_.Exception.Response.GetResponseStream()\n $reader = New-Object System.Io.StreamReader($result);\n $responseBody = $reader.ReadToEnd();\n throw \"Error occurred: $responseBody\"\n }\n}\n\nif(![string]::IsNullOrWhiteSpace($currentRelease.ReleaseNotes)){\n\twrite-host \"Release notes already filled in. $($currentRelease.ReleaseNotes)\"\n Set-OctopusVariable -name \"ReleaseNotes\" -value $releaseNotes\n\texit;\n}\n\n\n#Get projectid\n$url = \"http://$($instance)/tfs/$($collection)/$($projectId)/_apis/projects/$($project)?&includeCapabilities=false&includeHistory=false&api-version=2.2\"\nwrite-host \"Invoking url= $($url)\"\n$projectresponse = Invoke-RestMethod $url -Method GET -Headers $headers\n\n$projectid = $projectresponse.id\nwrite-host \"projectid $($projectid)\"\n\n#Get the ID of the query to execute\n$queryResult = Invoke-RestMethod \"http://$($instance)/tfs/$($collection)/$($projectId)/_apis/$($pathquery)?$depth=1&api-version=2.2\" -Method GET -Headers $headers\nwrite-host \"queryResult $($queryResult)\"\n\n#https://{instance}/DefaultCollection/[{project}/]_apis/wit/wiql/{id}?api-version={version}\n$queryResult = Invoke-RestMethod \"http://$($instance)/tfs/$($collection)/$($projectId)/_apis/wit/wiql/$($queryResult.Id)?api-version=2.2\" -Method GET -Headers $headers\n\nWrite-Host \"Found $($queryResult.workItems.Count) number of workitems for query: ReleaseNotes$($releaseTag)\"\n\n$releaseNotes = \"**Work Items:**\"\n\n\nif($queryResult.workItems.Count -eq 0)\n{\n\tWrite-Host \"No work items for release\"\n\t$releaseNotes = \"`n no new work items\"\n}\nelse\n{\n\t#Create a list of ids\n\t$ids = [string]::Join(\"%2C\", ($queryResult.workItems.id))\n\n\t#Get all the work items\n\t$workItems = Invoke-RestMethod \"http://$($instance)/tfs/$($collection)/_apis/wit/workItems?ids=$($ids)&fields=System.Title\" -Method GET -Headers $headers\n\n\tforeach($workItem in $workItems.value)\n\t{\n\t\t#Add line for each work item\n\t\t$releaseNotes = $releaseNotes + \"`n* [$($workItem.id)] (http://$($instance)/tfs/$($collection)/9981e67f-b27c-4628-b5cf-fba1d327aa07/_workitems/edit/$($workItem.id)) : $($workItem.fields.'System.Title')\"\n\t}\n\n}\n\n\n\n# Update the release notes for the current release\n$currentRelease.ReleaseNotes = $releaseNotes \nwrite-host \"Release notes $($currentRelease.ReleaseNotes)\"\nWrite-Host \"Updating release notes for $thisReleaseNumber`:`n`n\"\ntry {\n $releaseUri = \"$baseUri$baseApiUrl/releases/$($currentRelease.Id)\"\n write-host \"Release uri $($releaseUri)\"\n $currentReleaseBody = $currentRelease | ConvertTo-Json\n write-host \"Current release body $($currentReleaseBody)\"\n $result = Invoke-RestMethod $releaseUri -Method Post -Headers $putReqHeaders -Body $currentReleaseBody -UseBasicParsing\n\twrite-host \"result $($result)\"\n} catch {\n $result = $_.Exception.Response.GetResponseStream()\n $reader = New-Object System.Io.StreamReader($result);\n $responseBody = $reader.ReadToEnd();\n Write-Host \"error $($responseBody)\"\n throw \"Error occurred: $responseBody\"\n}\n\nSet-OctopusVariable -name \"ReleaseNotes\" -value $releaseNotes\n" }, "Parameters": [ { From 521c6399de21728f99ad77d1f79a3182a171c174 Mon Sep 17 00:00:00 2001 From: Jim Burger Date: Wed, 19 Dec 2018 17:43:24 +1030 Subject: [PATCH 10/16] Fixes the 'register linux tentacle' and provides space awareness A recent change to move Environments and Roles to DeploymentTargetResource from MachineResource broke this step --- step-templates/register-linux-tentacle.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/step-templates/register-linux-tentacle.json b/step-templates/register-linux-tentacle.json index 0bbe31c66..1c1ed027c 100644 --- a/step-templates/register-linux-tentacle.json +++ b/step-templates/register-linux-tentacle.json @@ -3,13 +3,13 @@ "Name": "Register Linux Tentacle", "Description": "This Step Template will Register a Linux Tentacle.\nReference : http://docs.octopusdeploy.com/display/OD/SSH+Targets.", "ActionType": "Octopus.Script", - "Version": 14, + "Version": 15, "CommunityActionTemplateId": null, "Properties": { "Octopus.Action.Script.Syntax": "PowerShell", "Octopus.Action.Script.ScriptSource": "Inline", "Octopus.Action.RunOnServer": "false", - "Octopus.Action.Script.ScriptBody": "$envname = \"#{Octopus.Environment.Name}\"\r\n$serverurl = \"#{Octopus.Web.BaseUrl}\"\r\n\r\n$headers = @{\"X-Octopus-ApiKey\"=\"$apikey\"} \r\n$environments = Invoke-RestMethod \"$serverurl/api/environments/all\" -Headers $headers -Method Get\r\n$theEnvironment = $environments | ? { $_.Name -eq $envname }\r\n\r\n$machines = Invoke-RestMethod \"$serverurl/api/machines/all\" -Headers $headers -Method Get\r\n$theMachine = $machines | ? { $_.Name -eq $machineName }\r\n\r\n$accounts = Invoke-RestMethod \"$serverurl/api/accounts/all\" -Headers $headers -Method Get\r\n$theAccount = $accounts | ? { $_.Name -eq $accountname }\r\n\r\nif (!($theMachine.Name -eq $machineName))\r\n{ \r\n $discovered = Invoke-RestMethod \"$serverurl/api/machines/discover?host=$hostdetails&type=Ssh\" -Headers $headers -Method Get\r\n $discovered.Roles += \"$role\"\r\n $discovered.EnvironmentIds += $theEnvironment.Id\r\n $discovered.Endpoint.AccountId = $theAccount.Id\r\n $discovered.Name = $machineName\r\n\r\n $discovered | ConvertTo-Json -Depth 10\r\n\r\n $registerStatus = Invoke-RestMethod \"$serverurl/api/machines\" -Headers $headers -Method Post -Body ($discovered | ConvertTo-Json -Depth 10)\r\n\r\n If ($registerStatus.Status -eq \"Online\")\r\n {\r\n Write-Output \"$registerStatus.Name is Successfully Registered\"\r\n }\r\n else\r\n {\r\n Write-Warning \"$hostdetails had issues, Please check Environments Page\"\r\n }\r\n}\r\nelse\r\n{\r\n Write-Output \"Machine with name $machineName already exists with the status $($theMachine.Status)\" \r\n}", + "Octopus.Action.Script.ScriptBody": "$envname = \"#{Octopus.Environment.Name}\"\n$serverurl = \"#{Octopus.Web.BaseUrl}\"\n\n$headers = @{\"X-Octopus-ApiKey\"=\"$apikey\"}\n$putHeaders = @{\"X-HTTP-Method-Override\"=\"PUT\"; \"X-Octopus-ApiKey\"=\"$apikey\"}\n\nfunction Test-SpacesApi {\n\tWrite-Verbose \"Checking API compatibility\";\n\t$rootDocument = Invoke-WebRequest \"$serverurl/api\" -Headers $headers -Method Get -UseBasicParsing | ConvertFrom-Json;\n if($rootDocument.Links -ne $null -and $rootDocument.Links.Spaces -ne $null) {\n \tWrite-Verbose \"Spaces API found\"\n \treturn $true;\n }\n Write-Verbose \"Pre-spaces API found\"\n return $false;\n}\n\nif(Test-SpacesApi) {\n\t$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 could indicate you do not have the correct permissions.\";\n }\n\t$baseApiUrl = \"/api/$spaceId\" ;\n} else {\n\t$baseApiUrl = \"/api\" ;\n}\n\n$environments = Invoke-RestMethod \"$serverurl$baseApiUrl/environments/all\" -Headers $headers -Method Get\n$theEnvironment = $environments | ? { $_.Name -eq $envname }\n\n$machines = Invoke-RestMethod \"$serverurl$baseApiUrl/machines/all\" -Headers $headers -Method Get\n$theMachine = $machines | ? { $_.Name -eq $machineName }\n\n$accounts = Invoke-RestMethod \"$serverurl$baseApiUrl/accounts/all\" -Headers $headers -Method Get\n$theAccount = $accounts | ? { $_.Name -eq $accountname }\n\nif (!($theMachine.Name -eq $machineName))\n{\n\t#this returns a MachineResource, but we need to post a DeploymentTargetResource which requires environments and roles\n\t$discovered = Invoke-RestMethod \"$serverurl$baseApiUrl/machines/discover?host=$hostdetails&type=Ssh\" -Headers $headers -Method Get\n $discovered.Endpoint.AccountId = $theAccount.Id\n $discovered.Name = $machineName\n\t$discovered | add-member -Name \"Roles\" -value @($role) -MemberType NoteProperty\n $discovered | add-member -Name \"EnvironmentIds\" -value @($theEnvironment.Id) -MemberType NoteProperty\n\t\n\twrite-verbose ($discovered | ConvertTo-Json)\n write-verbose \"$serverurl$baseApiUrl/machines\"\n $registerStatus = Invoke-RestMethod \"$serverurl$baseApiUrl/machines\" -Headers $headers -Method Post -Body ($discovered | ConvertTo-Json -Depth 10)\n \n If ($registerStatus.Status -eq \"Online\")\n {\n Write-Output \"$registerStatus.Name is Successfully Registered\"\n }\n else\n {\n Write-Warning \"$hostdetails had issues, Please check Environments Page\"\n }\n}\nelse\n{\n Write-Output \"Machine with name $machineName already exists with the status $($theMachine.Status)\" \n}", "Octopus.Action.Script.ScriptFileName": null, "Octopus.Action.Package.FeedId": null, "Octopus.Action.Package.PackageId": null From 6ab37b833f108100656c322d03c3aa4ff1a301d4 Mon Sep 17 00:00:00 2001 From: Jim Burger Date: Wed, 19 Dec 2018 17:59:33 +1030 Subject: [PATCH 11/16] Removed debugging statements --- step-templates/register-linux-tentacle.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/step-templates/register-linux-tentacle.json b/step-templates/register-linux-tentacle.json index 1c1ed027c..f16ef8273 100644 --- a/step-templates/register-linux-tentacle.json +++ b/step-templates/register-linux-tentacle.json @@ -9,7 +9,7 @@ "Octopus.Action.Script.Syntax": "PowerShell", "Octopus.Action.Script.ScriptSource": "Inline", "Octopus.Action.RunOnServer": "false", - "Octopus.Action.Script.ScriptBody": "$envname = \"#{Octopus.Environment.Name}\"\n$serverurl = \"#{Octopus.Web.BaseUrl}\"\n\n$headers = @{\"X-Octopus-ApiKey\"=\"$apikey\"}\n$putHeaders = @{\"X-HTTP-Method-Override\"=\"PUT\"; \"X-Octopus-ApiKey\"=\"$apikey\"}\n\nfunction Test-SpacesApi {\n\tWrite-Verbose \"Checking API compatibility\";\n\t$rootDocument = Invoke-WebRequest \"$serverurl/api\" -Headers $headers -Method Get -UseBasicParsing | ConvertFrom-Json;\n if($rootDocument.Links -ne $null -and $rootDocument.Links.Spaces -ne $null) {\n \tWrite-Verbose \"Spaces API found\"\n \treturn $true;\n }\n Write-Verbose \"Pre-spaces API found\"\n return $false;\n}\n\nif(Test-SpacesApi) {\n\t$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 could indicate you do not have the correct permissions.\";\n }\n\t$baseApiUrl = \"/api/$spaceId\" ;\n} else {\n\t$baseApiUrl = \"/api\" ;\n}\n\n$environments = Invoke-RestMethod \"$serverurl$baseApiUrl/environments/all\" -Headers $headers -Method Get\n$theEnvironment = $environments | ? { $_.Name -eq $envname }\n\n$machines = Invoke-RestMethod \"$serverurl$baseApiUrl/machines/all\" -Headers $headers -Method Get\n$theMachine = $machines | ? { $_.Name -eq $machineName }\n\n$accounts = Invoke-RestMethod \"$serverurl$baseApiUrl/accounts/all\" -Headers $headers -Method Get\n$theAccount = $accounts | ? { $_.Name -eq $accountname }\n\nif (!($theMachine.Name -eq $machineName))\n{\n\t#this returns a MachineResource, but we need to post a DeploymentTargetResource which requires environments and roles\n\t$discovered = Invoke-RestMethod \"$serverurl$baseApiUrl/machines/discover?host=$hostdetails&type=Ssh\" -Headers $headers -Method Get\n $discovered.Endpoint.AccountId = $theAccount.Id\n $discovered.Name = $machineName\n\t$discovered | add-member -Name \"Roles\" -value @($role) -MemberType NoteProperty\n $discovered | add-member -Name \"EnvironmentIds\" -value @($theEnvironment.Id) -MemberType NoteProperty\n\t\n\twrite-verbose ($discovered | ConvertTo-Json)\n write-verbose \"$serverurl$baseApiUrl/machines\"\n $registerStatus = Invoke-RestMethod \"$serverurl$baseApiUrl/machines\" -Headers $headers -Method Post -Body ($discovered | ConvertTo-Json -Depth 10)\n \n If ($registerStatus.Status -eq \"Online\")\n {\n Write-Output \"$registerStatus.Name is Successfully Registered\"\n }\n else\n {\n Write-Warning \"$hostdetails had issues, Please check Environments Page\"\n }\n}\nelse\n{\n Write-Output \"Machine with name $machineName already exists with the status $($theMachine.Status)\" \n}", + "Octopus.Action.Script.ScriptBody": "$envname = \"#{Octopus.Environment.Name}\"\n$serverurl = \"#{Octopus.Web.BaseUrl}\"\n\n$headers = @{\"X-Octopus-ApiKey\"=\"$apikey\"}\n$putHeaders = @{\"X-HTTP-Method-Override\"=\"PUT\"; \"X-Octopus-ApiKey\"=\"$apikey\"}\n\nfunction Test-SpacesApi {\n\tWrite-Verbose \"Checking API compatibility\";\n\t$rootDocument = Invoke-WebRequest \"$serverurl/api\" -Headers $headers -Method Get -UseBasicParsing | ConvertFrom-Json;\n if($rootDocument.Links -ne $null -and $rootDocument.Links.Spaces -ne $null) {\n \tWrite-Verbose \"Spaces API found\"\n \treturn $true;\n }\n Write-Verbose \"Pre-spaces API found\"\n return $false;\n}\n\nif(Test-SpacesApi) {\n\t$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 could indicate you do not have the correct permissions.\";\n }\n\t$baseApiUrl = \"/api/$spaceId\" ;\n} else {\n\t$baseApiUrl = \"/api\" ;\n}\n\n$environments = Invoke-RestMethod \"$serverurl$baseApiUrl/environments/all\" -Headers $headers -Method Get\n$theEnvironment = $environments | ? { $_.Name -eq $envname }\n\n$machines = Invoke-RestMethod \"$serverurl$baseApiUrl/machines/all\" -Headers $headers -Method Get\n$theMachine = $machines | ? { $_.Name -eq $machineName }\n\n$accounts = Invoke-RestMethod \"$serverurl$baseApiUrl/accounts/all\" -Headers $headers -Method Get\n$theAccount = $accounts | ? { $_.Name -eq $accountname }\n\nif (!($theMachine.Name -eq $machineName))\n{\n\t#this returns a MachineResource, but we need to post a DeploymentTargetResource which requires environments and roles\n\t$discovered = Invoke-RestMethod \"$serverurl$baseApiUrl/machines/discover?host=$hostdetails&type=Ssh\" -Headers $headers -Method Get\n $discovered.Endpoint.AccountId = $theAccount.Id\n $discovered.Name = $machineName\n\t$discovered | add-member -Name \"Roles\" -value @($role) -MemberType NoteProperty\n $discovered | add-member -Name \"EnvironmentIds\" -value @($theEnvironment.Id) -MemberType NoteProperty\n\t\n $registerStatus = Invoke-RestMethod \"$serverurl$baseApiUrl/machines\" -Headers $headers -Method Post -Body ($discovered | ConvertTo-Json -Depth 10)\n \n If ($registerStatus.Status -eq \"Online\")\n {\n Write-Output \"$registerStatus.Name is Successfully Registered\"\n }\n else\n {\n Write-Warning \"$hostdetails had issues, Please check Environments Page\"\n }\n}\nelse\n{\n Write-Output \"Machine with name $machineName already exists with the status $($theMachine.Status)\" \n}", "Octopus.Action.Script.ScriptFileName": null, "Octopus.Action.Package.FeedId": null, "Octopus.Action.Package.PackageId": null From 1518b3221d422f2348c37884e375e9e1ed85a6fc Mon Sep 17 00:00:00 2001 From: Jim Burger Date: Wed, 19 Dec 2018 18:00:59 +1030 Subject: [PATCH 12/16] modified 'wait for tentacle registration' to be space aware --- .../octopus-wait-for-deployment-target-registration.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/step-templates/octopus-wait-for-deployment-target-registration.json b/step-templates/octopus-wait-for-deployment-target-registration.json index 4daf9f0ab..195801f83 100644 --- a/step-templates/octopus-wait-for-deployment-target-registration.json +++ b/step-templates/octopus-wait-for-deployment-target-registration.json @@ -3,12 +3,12 @@ "Name": "Octopus - Wait for Deployment Target registration", "Description": "This step will poll Octopus Deploy until it detects that the expected Deployment Target has been registered.\n\nThe goal being that a deployment will be paused until the expected Deployment Target is available (eg [Transient Targets](https://octopus.com/docs/infrastructure/environments/elastic-and-transient-environments/deploying-to-transient-targets)). On subsequent deploys, the Deployment Target would quickly be identified as registered, and the deployment would continue as expected.\n\nWith a couple of extra step templates you can:\n- Create a new EC2 Instance (_AWS - Launch EC2 Instance_)\n- Include the new Deployment Target in subsequent deployment steps (_Health Check_)", "ActionType": "Octopus.Script", - "Version": 1, + "Version": 2, "CommunityActionTemplateId": null, "Properties": { "Octopus.Action.Script.Syntax": "PowerShell", "Octopus.Action.Script.ScriptSource": "Inline", - "Octopus.Action.Script.ScriptBody": "# Running outside octopus\nparam(\n [string]$odEnv,\n [string]$odName,\n [string]$odRole,\n [int]$odTimeout,\n [string]$odUrl,\n [string]$odApiKey,\n [switch]$whatIf\n) \n\n$ErrorActionPreference = \"Stop\" \n\nfunction Get-Param($Name, [switch]$Required, $Default) {\n $result = $null\n\n if ($OctopusParameters -ne $null) {\n $result = $OctopusParameters[$Name]\n }\n\n if ($result -eq $null) {\n $variable = Get-Variable $Name -EA SilentlyContinue \n if ($variable -ne $null) {\n $result = $variable.Value\n }\n }\n\n if (!$result -or $result -eq $null) {\n if ($Default) {\n $result = $Default\n } elseif ($Required) {\n throw \"Missing parameter value $Name\"\n }\n }\n\n return $result\n}\n\n& {\n param(\n [string]$odEnv,\n [string]$odName,\n [string]$odRole,\n [int]$odTimeout,\n [string]$odUrl,\n [string]$odApiKey\n )\n\n # If Octopus Deploy's URL/API Key are not provided as params, attempt to retrieve them from Environment Variables\n if (!$odUrl) {\n if ([Environment]::GetEnvironmentVariable(\"OD_API_URL\", \"Machine\")) {\n $odUrl = [Environment]::GetEnvironmentVariable(\"OD_API_URL\", \"Machine\")\n }\n }\n \n if (!$odUrl) { throw \"Octopus Deploy API URL was not available/provided.\" }\n\n if (!$odApiKey) {\n if ([Environment]::GetEnvironmentVariable(\"OD_API_KEY\", \"Machine\")) {\n $odApiKey = [Environment]::GetEnvironmentVariable(\"OD_API_KEY\", \"Machine\")\n }\n } \n \n if (!$odApiKey) { throw \"Octopus Deploy API key was not available/provided.\" }\n\n $header = @{ \"X-Octopus-ApiKey\" = $odApiKey }\n $environments = (Invoke-WebRequest \"$odUrl/api/environments/all\" -Headers $header).content | ConvertFrom-Json\n $environment = $environments | Where-Object { $_.Name -contains $odEnv }\n if (@($environment).Count -eq 0) { throw \"Could not find environment with the name '$odEnv'\" }\n \n $timeout = new-timespan -Seconds $odTimeout\n $sw = [diagnostics.stopwatch]::StartNew()\n \n Write-Output (\"------------------------------\")\n Write-Output (\"Checking the Deployment Target's registration status:\")\n Write-Output (\"------------------------------\")\n\n while ($true)\n {\n if ($sw.elapsed -gt $timeout) { throw \"Timed out waiting for the Deployment Target to register\" }\n \n $machines = ((Invoke-WebRequest ($odUrl + $environment.Links.Self + \"/machines\") -Headers $header).content | ConvertFrom-Json).items\n if ($odName) { $machines = $machines | Where-Object { $_.Name -like \"*$odName*\" } }\n if ($odRole) { $machines = $machines | Where-Object { $_.Roles -like \"*$odRole*\" } }\n if (@($machines).Count -gt 0) { break }\n\n Write-Output (\"$(Get-Date) | Waiting for Deployment Target to register with the name '$odName' and role '$odRole'\")\n\n Sleep -Seconds 5\n }\n \n Write-Output (\"$(Get-Date) | Deployment Target registered with the name '$odName' and role '$odRole'!\")\n } `\n (Get-Param 'odEnv' -Required) `\n (Get-Param 'odName' -Required) `\n (Get-Param 'odRole') `\n (Get-Param 'odTimeout' -Required) `\n (Get-Param 'odUrl') `\n (Get-Param 'odApiKey')" + "Octopus.Action.Script.ScriptBody": "# Running outside octopus\nparam(\n [string]$odEnv,\n [string]$odName,\n [string]$odRole,\n [int]$odTimeout,\n [string]$odUrl,\n [string]$odApiKey,\n [switch]$whatIf\n) \n\n$ErrorActionPreference = \"Stop\" \n\nfunction Get-Param($Name, [switch]$Required, $Default) {\n $result = $null\n\n if ($OctopusParameters -ne $null) {\n $result = $OctopusParameters[$Name]\n }\n\n if ($result -eq $null) {\n $variable = Get-Variable $Name -EA SilentlyContinue \n if ($variable -ne $null) {\n $result = $variable.Value\n }\n }\n\n if (!$result -or $result -eq $null) {\n if ($Default) {\n $result = $Default\n } elseif ($Required) {\n throw \"Missing parameter value $Name\"\n }\n }\n\n return $result\n}\n\n& {\n param(\n [string]$odEnv,\n [string]$odName,\n [string]$odRole,\n [int]$odTimeout,\n [string]$odUrl,\n [string]$odApiKey\n )\n\n # If Octopus Deploy's URL/API Key are not provided as params, attempt to retrieve them from Environment Variables\n if (!$odUrl) {\n if ([Environment]::GetEnvironmentVariable(\"OD_API_URL\", \"Machine\")) {\n $odUrl = [Environment]::GetEnvironmentVariable(\"OD_API_URL\", \"Machine\")\n }\n }\n \n if (!$odUrl) { throw \"Octopus Deploy API URL was not available/provided.\" }\n\n if (!$odApiKey) {\n if ([Environment]::GetEnvironmentVariable(\"OD_API_KEY\", \"Machine\")) {\n $odApiKey = [Environment]::GetEnvironmentVariable(\"OD_API_KEY\", \"Machine\")\n }\n } \n \n if (!$odApiKey) { throw \"Octopus Deploy API key was not available/provided.\" }\n\n $header = @{ \"X-Octopus-ApiKey\" = $odApiKey }\n \n Write-Verbose \"Checking API compatibility\";\n $rootDocument = Invoke-WebRequest \"$odUrl/api/\" -Header $header -UseBasicParsing | ConvertFrom-Json;\n if($rootDocument.Links -ne $null -and $rootDocument.Links.Spaces -ne $null) {\n Write-Verbose \"Spaces API found\"\n $hasSpacesApi = $true;\n } else {\n Write-Verbose \"Pre-spaces API found\"\n $hasSpacesApi = $false;\n }\n \n if($hasSpacesApi) {\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 recieved a blank value, which could indicate you do not have the correct permissions.\";\n }\n $baseApiUrl = \"/api/$spaceId\" ;\n } else {\n $baseApiUrl = \"/api\" ;\n } \n \n $environments = (Invoke-WebRequest \"$odUrl$baseApiUrl/environments/all\" -Headers $header -UseBasicParsing).content | ConvertFrom-Json\n $environment = $environments | Where-Object { $_.Name -contains $odEnv }\n if (@($environment).Count -eq 0) { throw \"Could not find environment with the name '$odEnv'\" }\n \n $timeout = new-timespan -Seconds $odTimeout\n $sw = [diagnostics.stopwatch]::StartNew()\n\n Write-Output (\"------------------------------\")\n Write-Output (\"Checking the Deployment Target's registration status:\")\n Write-Output (\"------------------------------\")\n\n while ($true)\n {\n if ($sw.elapsed -gt $timeout) { throw \"Timed out waiting for the Deployment Target to register\" }\n \n $machines = ((Invoke-WebRequest ($odUrl + $environment.Links.Self + \"/machines\") -Headers $header -UseBasicParsing).content | ConvertFrom-Json).items\n if ($odName) { $machines = $machines | Where-Object { $_.Name -like \"*$odName*\" } }\n if ($odRole) { $machines = $machines | Where-Object { $_.Roles -like \"*$odRole*\" } }\n if (@($machines).Count -gt 0) { break }\n\n Write-Output (\"$(Get-Date) | Waiting for Deployment Target to register with the name '$odName' and role '$odRole'\")\n\n Sleep -Seconds 5\n }\n \n Write-Output (\"$(Get-Date) | Deployment Target registered with the name '$odName' and role '$odRole'!\")\n } `\n (Get-Param 'odEnv' -Required) `\n (Get-Param 'odName' -Required) `\n (Get-Param 'odRole') `\n (Get-Param 'odTimeout' -Required) `\n (Get-Param 'odUrl') `\n (Get-Param 'odApiKey')" }, "Parameters": [ { From 09e61152576a441f56649081977dd8acfb287d18 Mon Sep 17 00:00:00 2001 From: Jim Burger Date: Thu, 20 Dec 2018 10:47:05 +1030 Subject: [PATCH 13/16] Updated error with a call to action to contact support... ...rather than guessing at the root cause :) --- step-templates/clone-tenant.json | 2 +- step-templates/create-tenant.json | 2 +- step-templates/octopus-chain-deployment.json | 2 +- step-templates/octopus-consolidate-releasenotes.json | 2 +- step-templates/octopus-import-certificate.json | 2 +- .../octopus-set-Octopus-releaese-notes-from-TFS-query.json | 2 +- .../octopus-wait-for-deployment-target-registration.json | 2 +- step-templates/save-octopus-output-variable-with-scoping.json | 2 +- step-templates/save-octopus-output-variable.json | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/step-templates/clone-tenant.json b/step-templates/clone-tenant.json index ed5bdcd35..50ee9ed3b 100644 --- a/step-templates/clone-tenant.json +++ b/step-templates/clone-tenant.json @@ -8,7 +8,7 @@ "Properties": { "Octopus.Action.Script.ScriptSource": "Inline", "Octopus.Action.Script.Syntax": "PowerShell", - "Octopus.Action.Script.ScriptBody": "$apiKey = $CloneTenantStep_ApiKey\n$tenantToClone = $CloneTenantStep_TenantIdToClone\n$tenantName = $CloneTenantStep_TenantName\n$octopusBaseUrl = $OctopusParameters['Octopus.Web.BaseUrl'].Trim('/')\n$cloneVariables = $CloneTenantStep_CloneVariables\n\n$ErrorActionPreference = 'Stop'\n\nif ([string]::IsNullOrWhiteSpace($apiKey)) {\n throw \"The step parameter 'Octopus API Key' was not found. This step requires an API Key to function, please provide one and try again.\"\n}\n\nfunction Invoke-OctopusApi {\n param(\n [Parameter(Position = 0, Mandatory)]$Uri,\n [ValidateSet(\"Get\", \"Post\", \"Put\", \"Delete\")]$Method = 'Get',\n $Body\n )\n $requestParameters = @{\n Uri = ('{0}/{1}' -f $octopusBaseUrl, $Uri.TrimStart('/'))\n Method = $Method\n Headers = @{ \"X-Octopus-ApiKey\" = $apiKey }\n UseBasicParsing = $true\n }\n if ($null -ne $Body) { $requestParameters.Add('Body', ($Body | ConvertTo-Json -Depth 10)) }\n Write-Verbose \"$($Method.ToUpperInvariant()) $($requestParameters.Uri)\" \n \n\n return Invoke-WebRequest @requestParameters | % Content | ConvertFrom-Json\n}\n\nfunction Test-SpacesApi {\n\tWrite-Verbose \"Checking API compatibility\";\n\t$rootDocument = Invoke-OctopusApi 'api/';\n if($rootDocument.Links -ne $null -and $rootDocument.Links.Spaces -ne $null) {\n \tWrite-Verbose \"Spaces API found\"\n \treturn $true;\n }\n Write-Verbose \"Pre-spaces API found\"\n return $false;\n}\n\nif(Test-SpacesApi) {\n\t$spaceId = $OctopusParameters['Octopus.Space.Id'];\n\tif([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 recieved a blank value, which could indicate you do not have the correct permissions.\";\n }\n\t$tenantApiUrl = \"api/$spaceId/tenants\" ;\n} else {\n\t$tenantApiUrl = \"api/tenants\" ;\n}\n\n$tenant = Invoke-OctopusApi \"$tenantApiUrl/$tenantToClone\"\n\n$tenant.Id = $null\n$tenant.Name = $tenantName\n\n$newTenant = Invoke-OctopusApi $tenantApiUrl -Method Post -Body $tenant\n\nif ($cloneVariables -eq $true) {\n $variables = Invoke-OctopusApi $tenant.Links.Variables\n $variables.TenantId = $newTenant.Id\n $variables.TenantName = $tenantName\n\n $newVars = Invoke-OctopusApi $newTenant.Links.Variables -Method Put -Body $variables\n}" + "Octopus.Action.Script.ScriptBody": "$apiKey = $CloneTenantStep_ApiKey\n$tenantToClone = $CloneTenantStep_TenantIdToClone\n$tenantName = $CloneTenantStep_TenantName\n$octopusBaseUrl = $OctopusParameters['Octopus.Web.BaseUrl'].Trim('/')\n$cloneVariables = $CloneTenantStep_CloneVariables\n\n$ErrorActionPreference = 'Stop'\n\nif ([string]::IsNullOrWhiteSpace($apiKey)) {\n throw \"The step parameter 'Octopus API Key' was not found. This step requires an API Key to function, please provide one and try again.\"\n}\n\nfunction Invoke-OctopusApi {\n param(\n [Parameter(Position = 0, Mandatory)]$Uri,\n [ValidateSet(\"Get\", \"Post\", \"Put\", \"Delete\")]$Method = 'Get',\n $Body\n )\n $requestParameters = @{\n Uri = ('{0}/{1}' -f $octopusBaseUrl, $Uri.TrimStart('/'))\n Method = $Method\n Headers = @{ \"X-Octopus-ApiKey\" = $apiKey }\n UseBasicParsing = $true\n }\n if ($null -ne $Body) { $requestParameters.Add('Body', ($Body | ConvertTo-Json -Depth 10)) }\n Write-Verbose \"$($Method.ToUpperInvariant()) $($requestParameters.Uri)\" \n \n\n return Invoke-WebRequest @requestParameters | % Content | ConvertFrom-Json\n}\n\nfunction Test-SpacesApi {\n\tWrite-Verbose \"Checking API compatibility\";\n\t$rootDocument = Invoke-OctopusApi 'api/';\n if($rootDocument.Links -ne $null -and $rootDocument.Links.Spaces -ne $null) {\n \tWrite-Verbose \"Spaces API found\"\n \treturn $true;\n }\n Write-Verbose \"Pre-spaces API found\"\n return $false;\n}\n\nif(Test-SpacesApi) {\n\t$spaceId = $OctopusParameters['Octopus.Space.Id'];\n\tif([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 recieved 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\t$tenantApiUrl = \"api/$spaceId/tenants\" ;\n} else {\n\t$tenantApiUrl = \"api/tenants\" ;\n}\n\n$tenant = Invoke-OctopusApi \"$tenantApiUrl/$tenantToClone\"\n\n$tenant.Id = $null\n$tenant.Name = $tenantName\n\n$newTenant = Invoke-OctopusApi $tenantApiUrl -Method Post -Body $tenant\n\nif ($cloneVariables -eq $true) {\n $variables = Invoke-OctopusApi $tenant.Links.Variables\n $variables.TenantId = $newTenant.Id\n $variables.TenantName = $tenantName\n\n $newVars = Invoke-OctopusApi $newTenant.Links.Variables -Method Put -Body $variables\n}" }, "Parameters": [ { diff --git a/step-templates/create-tenant.json b/step-templates/create-tenant.json index 8c8fc14c7..1cee20ff7 100644 --- a/step-templates/create-tenant.json +++ b/step-templates/create-tenant.json @@ -8,7 +8,7 @@ "Properties": { "Octopus.Action.Script.ScriptSource": "Inline", "Octopus.Action.Script.Syntax": "PowerShell", - "Octopus.Action.Script.ScriptBody": "$ErrorActionPreference = 'Stop'\n\n$apiKey = $CreateTenantStep_ApiKey\n$tenantName = $CreateTenantStep_TenantName\n$tenantTags = if ($CreateTenantStep_TenantTags -eq $null) { @() } else { $CreateTenantStep_TenantTags | ConvertFrom-Json }\n$projectEnvironments = if ($CreateTenantStep_ProjectEnvironments -eq $null) { @{} } else { $CreateTenantStep_ProjectEnvironments | ConvertFrom-Json }\n$octopusBaseUrl = $OctopusParameters['Octopus.Web.BaseUrl'].Trim('/')\n\nif ([string]::IsNullOrWhiteSpace($apiKey)) {\n throw \"The step parameter 'Octopus API Key' was not found. This step requires an API Key to function, please provide one and try again.\"\n}\n\nfunction Invoke-OctopusApi {\n param(\n [Parameter(Position = 0, Mandatory)]$Uri,\n [ValidateSet(\"Get\", \"Post\", \"Put\", \"Delete\")]$Method = 'Get',\n $Body\n )\n $requestParameters = @{\n Uri = ('{0}/{1}' -f $octopusBaseUrl, $Uri.TrimStart('/'))\n Method = $Method\n Headers = @{ \"X-Octopus-ApiKey\" = $apiKey }\n UseBasicParsing = $true\n }\n if ($null -ne $Body) { $requestParameters.Add('Body', ($Body | ConvertTo-Json -Depth 10)) }\n Write-Verbose \"$($Method.ToUpperInvariant()) $($requestParameters.Uri)\" \n \n return Invoke-WebRequest @requestParameters | % Content | ConvertFrom-Json\n}\n\nfunction Test-SpacesApi {\n\tWrite-Verbose \"Checking API compatibility\";\n\t$rootDocument = Invoke-OctopusApi 'api/';\n if($rootDocument.Links -ne $null -and $rootDocument.Links.Spaces -ne $null) {\n \tWrite-Verbose \"Spaces API found\"\n \treturn $true;\n }\n Write-Verbose \"Pre-spaces API found\"\n return $false;\n}\n\nif(Test-SpacesApi) {\n\t$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 recieved a blank value, which could indicate you do not have the correct permissions.\";\n }\n\t$baseApiUrl = \"/api/$spaceId\" ;\n} else {\n\t$baseApiUrl = \"/api\" ;\n}\n\n$body = @{\n\tId = $null\n Name = $tenantName\n TenantTags = @($tenantTags)\n ProjectEnvironments = $projectEnvironments #@{ \"Projects-63\" = @(\"Environments-1\",\"Environments-2\") }\n}\n\nInvoke-OctopusApi \"$baseApiUrl/tenants\" -Method Post -Body $body | Out-Null" + "Octopus.Action.Script.ScriptBody": "$ErrorActionPreference = 'Stop'\n\n$apiKey = $CreateTenantStep_ApiKey\n$tenantName = $CreateTenantStep_TenantName\n$tenantTags = if ($CreateTenantStep_TenantTags -eq $null) { @() } else { $CreateTenantStep_TenantTags | ConvertFrom-Json }\n$projectEnvironments = if ($CreateTenantStep_ProjectEnvironments -eq $null) { @{} } else { $CreateTenantStep_ProjectEnvironments | ConvertFrom-Json }\n$octopusBaseUrl = $OctopusParameters['Octopus.Web.BaseUrl'].Trim('/')\n\nif ([string]::IsNullOrWhiteSpace($apiKey)) {\n throw \"The step parameter 'Octopus API Key' was not found. This step requires an API Key to function, please provide one and try again.\"\n}\n\nfunction Invoke-OctopusApi {\n param(\n [Parameter(Position = 0, Mandatory)]$Uri,\n [ValidateSet(\"Get\", \"Post\", \"Put\", \"Delete\")]$Method = 'Get',\n $Body\n )\n $requestParameters = @{\n Uri = ('{0}/{1}' -f $octopusBaseUrl, $Uri.TrimStart('/'))\n Method = $Method\n Headers = @{ \"X-Octopus-ApiKey\" = $apiKey }\n UseBasicParsing = $true\n }\n if ($null -ne $Body) { $requestParameters.Add('Body', ($Body | ConvertTo-Json -Depth 10)) }\n Write-Verbose \"$($Method.ToUpperInvariant()) $($requestParameters.Uri)\" \n \n return Invoke-WebRequest @requestParameters | % Content | ConvertFrom-Json\n}\n\nfunction Test-SpacesApi {\n\tWrite-Verbose \"Checking API compatibility\";\n\t$rootDocument = Invoke-OctopusApi 'api/';\n if($rootDocument.Links -ne $null -and $rootDocument.Links.Spaces -ne $null) {\n \tWrite-Verbose \"Spaces API found\"\n \treturn $true;\n }\n Write-Verbose \"Pre-spaces API found\"\n return $false;\n}\n\nif(Test-SpacesApi) {\n\t$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 recieved 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\t$baseApiUrl = \"/api/$spaceId\" ;\n} else {\n\t$baseApiUrl = \"/api\" ;\n}\n\n$body = @{\n\tId = $null\n Name = $tenantName\n TenantTags = @($tenantTags)\n ProjectEnvironments = $projectEnvironments #@{ \"Projects-63\" = @(\"Environments-1\",\"Environments-2\") }\n}\n\nInvoke-OctopusApi \"$baseApiUrl/tenants\" -Method Post -Body $body | Out-Null" }, "Parameters": [ { diff --git a/step-templates/octopus-chain-deployment.json b/step-templates/octopus-chain-deployment.json index 863906d7c..6c6d3ba07 100644 --- a/step-templates/octopus-chain-deployment.json +++ b/step-templates/octopus-chain-deployment.json @@ -8,7 +8,7 @@ "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\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. - will apply to all steps in the deployment, e.g.\n Octopus.Action.DebugLogging\n Octopus.Action[Step 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 -----\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\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$Chain_BaseUrl = (Get-OctopusSetting OctopusServerUrl $OctopusParameters['Octopus.Web.BaseUrl']).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\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 $Uri = $Uri -replace '{.*?}',''\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 } 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 Test-SpacesApi {\n\tWrite-Verbose \"Checking API compatibility\";\n\t$rootDocument = Invoke-OctopusApi 'api/';\n if($rootDocument.Links -ne $null -and $rootDocument.Links.Spaces -ne $null) {\n \tWrite-Verbose \"Spaces API found\"\n \treturn $true;\n }\n Write-Verbose \"Pre-spaces API found\"\n return $false;\n}\n\n$Chain_BaseApiUrl = \"/api\"\nif(Test-SpacesApi) {\n\t$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 recieved a blank value, which could indicate you do not have the correct permissions.\";\n }\n\t$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\thidden $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\" | ? 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)$($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)$($this.Lifecycle.Links.Self)\"\n }\n \n hidden $Channel\n [void] SetChannel($channelName) {\n $useDefaultChannel = Test-String $channelName -ForAbsence\n $this.Channel = Invoke-OctopusApi $this.Project.Links.Channels | % Items | ? { $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)$($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)$($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 $this.Channel.Links.Releases | % 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 [void] CreateRelease($releaseVersion) {\n $template = Invoke-OctopusApi ('{0}/template?channel={1}' -f $this.Project.Links.DeploymentProcess, $this.Channel.Id)\n $selectedPackages = @()\n Write-Host 'Resolving package versions...'\n $template.Packages | % {\n $preReleaseTag = $this.Channel.Rules | ? Actions -contains $_.StepName | ? { $null -ne $_ } | % { '&preReleaseTag={0}' -f $_.Tag }\n\n $package = Invoke-OctopusApi (\"$($this.BaseApiUrl)/feeds/{0}/packages?packageId={1}&partialMatch=false&includeMultipleVersions=false&includeNotes=false&includePreRelease=true&take=1{2}\" -f $_.FeedId, $_.PackageId, $preReleaseTag)\n\n Write-Host \"Found $($package.Title) @ $($package.Version) for step $($_.StepName)\"\n $selectedPackages += @{\n StepName = $_.StepName\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 $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 $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) | % {'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]$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 (?MON|TUE|WED|THU|FRI|SAT|SUN)? - Capture an optional day\n \\s*@\\s* - '@' indicates deploying at a specific time\n (?(?:[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 (?\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)(?:(?MON|TUE|WED|THU|FRI|SAT|SUN)?\\s*@\\s*(?(?:[01]?[0-9]|2[0-3]):[0-5][0-9]))?\\s*(?:\\+\\s*(?\\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] [+ ]'\" \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 | ? Name -eq $environmentName | % {\n $_.AutomaticDeploymentTargets\n $_.OptionalDeploymentTargets\n }\n $this.Environments = $this.DeploymentTemplate.PromoteTo | ? { $_.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 | % 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 | % Id\n $this.Tenants = $tenantFilter.Split(\"`n\") | % { [uri]::EscapeUriString($_.Trim()) } | % {\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 } | ? 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 | % 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 | ? Id -eq $tenant.Id | % PromoteTo | ? Id -eq $environment.Id) {\n $logPrefix = ($envPrefix,$tenantPrefix | ? { $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\") | % Trim\n $this.SkipActions = $this.DeploymentPreview.StepsToExecute | ? {\n $_.CanBeSkipped -and ($_.ActionName -in $comparisonArray -or $_.ActionNumber -in $comparisonArray)\n } | % {\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 hidden [hashtable]$FormValues\n [void] SetFormValues($formValuesToSet) {\n $this.FormValues = @{}\n $this.DeploymentPreview.Form.Values | Get-Member -MemberType NoteProperty | % {\n $this.FormValues.Add($_.Name, $this.DeploymentPreview.Form.Values.$($_.Name))\n }\n\n $formValuesToSet.Split(\"`n\") | % {\n $entry = $_.Split('=') | % Trim\n $entryName, $entryValues = $entry\n $entry = @($entryName, $($entryValues -join \"=\"))\n $this.DeploymentPreview.Form.Elements | ? { $_.Control.Name -ieq $entry[0] } | % {\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 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)$($deployment.Links.Web)\"\n Write-Verbose \"`t$($this.BaseUrl)$($deployment.Links.Self)\"\n Write-Verbose \"`t$($this.BaseUrl)$($this.DeploymentContext.BaseApiUrl)/deploymentprocesses/$($deployment.DeploymentProcessId)\"\n Write-Verbose \"`t$($this.BaseUrl)$($deployment.Links.Variables)\"\n Write-Verbose \"`t$($this.BaseUrl)$($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) | % Items\n }\n foreach ($activity in $ActivityLogs) {\n $correlatedInterrupts = $interrupts | ? CorrelationId -eq $activity.Id \n $correlatedInterrupts | ? IsPending -eq $false | % { $this.LogInterruptMessages($activity, $_) }\n\n $this.LogStepTransition($activity) \n $this.LogErrorsAndWarnings($activity)\n $correlatedInterrupts | ? IsPending -eq $true | % { \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 | ? Name -eq Instructions | % Control | % 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 $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 | ? 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 $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 $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$($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$($deploymentContext.Release.Links.Web)\"\nif ($Chain_SnapshotVariables -ieq 'True') {\n $deploymentContext.UpdateVariableSnapshot()\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\n$deploymentControllers = $deploymentContext.GetDeploymentControllers()\nif (Test-String $Chain_StepsToSkip) {\n $deploymentControllers | % { $_.SetStepsToSkip($Chain_StepsToSkip) }\n}\nif (Test-String $Chain_FormValues) {\n $deploymentControllers | % { $_.SetFormValues($Chain_FormValues) }\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 | % 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 | % Task | ? FinishedSuccessfully -eq $false) {\n Show-Heading 'Deployment Failed!'\n Write-Fatal (($deploymentControllers | % Task | % 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']) |\n % { Invoke-OctopusApi $_.Links.ProjectDeploymentProcessSnapshot } |\n % Steps | ? Id -eq $OctopusParameters['Octopus.Step.Id'] |\n % Actions | ? Id -eq $OctopusParameters['Octopus.Action.Id'] |\n % { $_.Properties.Chain_PostDeploy }\nWrite-Verbose \"Raw Post-Deploy Script:`n$rawPostDeployScript\"\n\nAdd-Type -Path (Get-WmiObject Win32_Process | ? ProcessId -eq $PID | % { Get-Process -Id $_.ParentProcessId } | % { Join-Path (Split-Path -Path $_.Path -Parent) 'Octostache.dll' })\n\n$deploymentControllers | % {\n $deployment = $_.Task.Deployment\n $tenant = $_.Tenant\n $variablesDictionary = [Octostache.VariableDictionary]::new()\n Invoke-OctopusApi (\"$Chain_BaseApiUrl/variables/{0}\" -f $deployment.ManifestVariableSetId) | % Variables | ? {\n ($_.IsSensitive -eq $false) -and `\n ($_.Scope.Private -ne 'True') -and `\n\t\t($null -eq $_.Scope.Action) -and `\n\t\t($null -eq $_.Scope.Machine) -and `\n ($null -eq $_.Scope.TargetRole) -and `\n\t\t($null -eq $_.Scope.Role) -and `\n ($null -eq $_.Scope.Tenant -or $_.Scope.Tenant -contains $tenant.Id) -and `\n\t\t($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\t\t($null -eq $_.Scope.Channel -or $_.Scope.Channel -contains $deployment.ChannelId) -and `\n\t\t($null -eq $_.Scope.Project -or $_.Scope.Project -contains $deployment.ProjectId)\n } | % { $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}", + "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\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. - will apply to all steps in the deployment, e.g.\n Octopus.Action.DebugLogging\n Octopus.Action[Step 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 -----\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\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$Chain_BaseUrl = (Get-OctopusSetting OctopusServerUrl $OctopusParameters['Octopus.Web.BaseUrl']).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\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 $Uri = $Uri -replace '{.*?}',''\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 } 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 Test-SpacesApi {\n\tWrite-Verbose \"Checking API compatibility\";\n\t$rootDocument = Invoke-OctopusApi 'api/';\n if($rootDocument.Links -ne $null -and $rootDocument.Links.Spaces -ne $null) {\n \tWrite-Verbose \"Spaces API found\"\n \treturn $true;\n }\n Write-Verbose \"Pre-spaces API found\"\n return $false;\n}\n\n$Chain_BaseApiUrl = \"/api\"\nif(Test-SpacesApi) {\n\t$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 recieved 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\t$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\thidden $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\" | ? 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)$($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)$($this.Lifecycle.Links.Self)\"\n }\n \n hidden $Channel\n [void] SetChannel($channelName) {\n $useDefaultChannel = Test-String $channelName -ForAbsence\n $this.Channel = Invoke-OctopusApi $this.Project.Links.Channels | % Items | ? { $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)$($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)$($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 $this.Channel.Links.Releases | % 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 [void] CreateRelease($releaseVersion) {\n $template = Invoke-OctopusApi ('{0}/template?channel={1}' -f $this.Project.Links.DeploymentProcess, $this.Channel.Id)\n $selectedPackages = @()\n Write-Host 'Resolving package versions...'\n $template.Packages | % {\n $preReleaseTag = $this.Channel.Rules | ? Actions -contains $_.StepName | ? { $null -ne $_ } | % { '&preReleaseTag={0}' -f $_.Tag }\n\n $package = Invoke-OctopusApi (\"$($this.BaseApiUrl)/feeds/{0}/packages?packageId={1}&partialMatch=false&includeMultipleVersions=false&includeNotes=false&includePreRelease=true&take=1{2}\" -f $_.FeedId, $_.PackageId, $preReleaseTag)\n\n Write-Host \"Found $($package.Title) @ $($package.Version) for step $($_.StepName)\"\n $selectedPackages += @{\n StepName = $_.StepName\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 $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 $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) | % {'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]$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 (?MON|TUE|WED|THU|FRI|SAT|SUN)? - Capture an optional day\n \\s*@\\s* - '@' indicates deploying at a specific time\n (?(?:[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 (?\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)(?:(?MON|TUE|WED|THU|FRI|SAT|SUN)?\\s*@\\s*(?(?:[01]?[0-9]|2[0-3]):[0-5][0-9]))?\\s*(?:\\+\\s*(?\\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] [+ ]'\" \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 | ? Name -eq $environmentName | % {\n $_.AutomaticDeploymentTargets\n $_.OptionalDeploymentTargets\n }\n $this.Environments = $this.DeploymentTemplate.PromoteTo | ? { $_.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 | % 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 | % Id\n $this.Tenants = $tenantFilter.Split(\"`n\") | % { [uri]::EscapeUriString($_.Trim()) } | % {\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 } | ? 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 | % 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 | ? Id -eq $tenant.Id | % PromoteTo | ? Id -eq $environment.Id) {\n $logPrefix = ($envPrefix,$tenantPrefix | ? { $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\") | % Trim\n $this.SkipActions = $this.DeploymentPreview.StepsToExecute | ? {\n $_.CanBeSkipped -and ($_.ActionName -in $comparisonArray -or $_.ActionNumber -in $comparisonArray)\n } | % {\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 hidden [hashtable]$FormValues\n [void] SetFormValues($formValuesToSet) {\n $this.FormValues = @{}\n $this.DeploymentPreview.Form.Values | Get-Member -MemberType NoteProperty | % {\n $this.FormValues.Add($_.Name, $this.DeploymentPreview.Form.Values.$($_.Name))\n }\n\n $formValuesToSet.Split(\"`n\") | % {\n $entry = $_.Split('=') | % Trim\n $entryName, $entryValues = $entry\n $entry = @($entryName, $($entryValues -join \"=\"))\n $this.DeploymentPreview.Form.Elements | ? { $_.Control.Name -ieq $entry[0] } | % {\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 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)$($deployment.Links.Web)\"\n Write-Verbose \"`t$($this.BaseUrl)$($deployment.Links.Self)\"\n Write-Verbose \"`t$($this.BaseUrl)$($this.DeploymentContext.BaseApiUrl)/deploymentprocesses/$($deployment.DeploymentProcessId)\"\n Write-Verbose \"`t$($this.BaseUrl)$($deployment.Links.Variables)\"\n Write-Verbose \"`t$($this.BaseUrl)$($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) | % Items\n }\n foreach ($activity in $ActivityLogs) {\n $correlatedInterrupts = $interrupts | ? CorrelationId -eq $activity.Id \n $correlatedInterrupts | ? IsPending -eq $false | % { $this.LogInterruptMessages($activity, $_) }\n\n $this.LogStepTransition($activity) \n $this.LogErrorsAndWarnings($activity)\n $correlatedInterrupts | ? IsPending -eq $true | % { \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 | ? Name -eq Instructions | % Control | % 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 $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 | ? 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 $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 $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$($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$($deploymentContext.Release.Links.Web)\"\nif ($Chain_SnapshotVariables -ieq 'True') {\n $deploymentContext.UpdateVariableSnapshot()\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\n$deploymentControllers = $deploymentContext.GetDeploymentControllers()\nif (Test-String $Chain_StepsToSkip) {\n $deploymentControllers | % { $_.SetStepsToSkip($Chain_StepsToSkip) }\n}\nif (Test-String $Chain_FormValues) {\n $deploymentControllers | % { $_.SetFormValues($Chain_FormValues) }\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 | % 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 | % Task | ? FinishedSuccessfully -eq $false) {\n Show-Heading 'Deployment Failed!'\n Write-Fatal (($deploymentControllers | % Task | % 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']) |\n % { Invoke-OctopusApi $_.Links.ProjectDeploymentProcessSnapshot } |\n % Steps | ? Id -eq $OctopusParameters['Octopus.Step.Id'] |\n % Actions | ? Id -eq $OctopusParameters['Octopus.Action.Id'] |\n % { $_.Properties.Chain_PostDeploy }\nWrite-Verbose \"Raw Post-Deploy Script:`n$rawPostDeployScript\"\n\nAdd-Type -Path (Get-WmiObject Win32_Process | ? ProcessId -eq $PID | % { Get-Process -Id $_.ParentProcessId } | % { Join-Path (Split-Path -Path $_.Path -Parent) 'Octostache.dll' })\n\n$deploymentControllers | % {\n $deployment = $_.Task.Deployment\n $tenant = $_.Tenant\n $variablesDictionary = [Octostache.VariableDictionary]::new()\n Invoke-OctopusApi (\"$Chain_BaseApiUrl/variables/{0}\" -f $deployment.ManifestVariableSetId) | % Variables | ? {\n ($_.IsSensitive -eq $false) -and `\n ($_.Scope.Private -ne 'True') -and `\n\t\t($null -eq $_.Scope.Action) -and `\n\t\t($null -eq $_.Scope.Machine) -and `\n ($null -eq $_.Scope.TargetRole) -and `\n\t\t($null -eq $_.Scope.Role) -and `\n ($null -eq $_.Scope.Tenant -or $_.Scope.Tenant -contains $tenant.Id) -and `\n\t\t($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\t\t($null -eq $_.Scope.Channel -or $_.Scope.Channel -contains $deployment.ChannelId) -and `\n\t\t($null -eq $_.Scope.Project -or $_.Scope.Project -contains $deployment.ProjectId)\n } | % { $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}", "Octopus.Action.Script.ScriptFileName": null, "Octopus.Action.Package.FeedId": null, "Octopus.Action.Package.PackageId": null diff --git a/step-templates/octopus-consolidate-releasenotes.json b/step-templates/octopus-consolidate-releasenotes.json index 8850e7ab7..16421af60 100644 --- a/step-templates/octopus-consolidate-releasenotes.json +++ b/step-templates/octopus-consolidate-releasenotes.json @@ -8,7 +8,7 @@ "Octopus.Action.Script.Syntax": "PowerShell", "Octopus.Action.Script.ScriptSource": "Inline", "Octopus.Action.RunOnServer": "true", - "Octopus.Action.Script.ScriptBody": "$baseUri = $OctopusParameters['Octopus.Web.BaseUrl']\n$reqheaders = @{\"X-Octopus-ApiKey\" = $Consolidate_ApiKey }\n$putReqHeaders = @{\"X-HTTP-Method-Override\" = \"PUT\"; \"X-Octopus-ApiKey\" = $Consolidate_ApiKey }\n\n$remWhiteSpace = [bool]::Parse($Consolidate_RemoveWhitespace)\n$deDupe = [bool]::Parse($Consolidate_Dedupe)\n$reverse = ($Consolidate_Order -eq \"Oldest\")\n\n# Get details we'll need\n$projectId = $OctopusParameters['Octopus.Project.Id']\n$thisReleaseNumber = $OctopusParameters['Octopus.Release.Number']\n$lastSuccessfulReleaseId = $OctopusParameters['Octopus.Release.CurrentForEnvironment.Id']\n$lastSuccessfulReleaseNumber = $OctopusParameters['Octopus.Release.CurrentForEnvironment.Number']\n\nfunction Test-SpacesApi {\n\tWrite-Verbose \"Checking API compatibility\";\n\t$rootDocument = Invoke-WebRequest \"$baseUri/api\" -Headers $reqHeaders -UseBasicParsing | ConvertFrom-Json;\n if($rootDocument.Links -ne $null -and $rootDocument.Links.Spaces -ne $null) {\n \tWrite-Verbose \"Spaces API found\"\n \treturn $true;\n }\n Write-Verbose \"Pre-spaces API found\"\n return $false;\n}\n\nif(Test-SpacesApi) {\n\t$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 recieved a blank value, which could indicate you do not have the correct permissions.\";\n }\n\t$baseApiUrl = \"/api/$spaceId\" ;\n} else {\n\t$baseApiUrl = \"/api\" ;\n}\n\n# Get all previous releases to this environment\n$releaseUri = \"$baseUri$baseApiUrl/projects/$projectId/releases\"\ntry {\n $allReleases = Invoke-WebRequest $releaseUri -Headers $reqheaders -UseBasicParsing | ConvertFrom-Json\n} catch {\n if ($_.Exception.Response.StatusCode.Value__ -ne 404) {\n $result = $_.Exception.Response.GetResponseStream()\n $reader = New-Object System.Io.StreamReader($result);\n $responseBody = $reader.ReadToEnd();\n throw \"Error occurred: $responseBody\"\n }\n}\n\n# Find and aggregate release notes\n$aggregateNotes = @()\n\nWrite-Host \"Finding all release notes between the last successful release: $lastSuccessfulReleaseNumber and this release: $thisReleaseNumber\"\nforeach ($rel in $allReleases.Items) {\n if ($rel.Id -ne $lastSuccessfulReleaseId) {\n Write-Host \"Found release notes for $($rel.Version)\"\n $theseNotes = @()\n #split into lines\n $lines = $rel.ReleaseNotes -split \"`n\"\n foreach ($line in $lines) {\n if (-not $remWhitespace -or -not [string]::IsNullOrWhiteSpace($line)) {\n if (-not $deDupe -or -not $aggregateNotes.Contains($line)) {\n $theseNotes = $theseNotes + $line\n }\n }\n }\n if ($reverse) {\n $aggregateNotes = $theseNotes + $aggregateNotes\n } else {\n $aggregateNotes = $aggregateNotes + $theseNotes\n }\n } else {\n break\n }\n}\n$aggregateNotesText = $aggregateNotes -join \"`n`n\"\n\n# Get the current release\n$releaseUri = \"$baseUri$baseApiUrl/projects/$projectId/releases/$thisReleaseNumber\"\ntry {\n $currentRelease = Invoke-WebRequest $releaseUri -Headers $reqheaders -UseBasicParsing | ConvertFrom-Json\n} catch {\n if ($_.Exception.Response.StatusCode.Value__ -ne 404) {\n $result = $_.Exception.Response.GetResponseStream()\n $reader = New-Object System.Io.StreamReader($result);\n $responseBody = $reader.ReadToEnd();\n throw \"Error occurred: $responseBody\"\n }\n}\n\n# Update the release notes for the current release\n$currentRelease.ReleaseNotes = $aggregateNotesText\nWrite-Host \"Updating release notes for $thisReleaseNumber`:`n`n\"\nWrite-Host $aggregateNotesText\ntry {\n $releaseUri = \"$baseUri$baseApiUrl/releases/$($currentRelease.Id)\"\n $currentReleaseBody = $currentRelease | ConvertTo-Json\n $result = Invoke-WebRequest $releaseUri -Method Post -Headers $putReqHeaders -Body $currentReleaseBody -UseBasicParsing | ConvertFrom-Json\n} catch {\n $result = $_.Exception.Response.GetResponseStream()\n $reader = New-Object System.Io.StreamReader($result);\n $responseBody = $reader.ReadToEnd();\n Write-Host $responseBody\n throw \"Error occurred: $responseBody\"\n}", + "Octopus.Action.Script.ScriptBody": "$baseUri = $OctopusParameters['Octopus.Web.BaseUrl']\n$reqheaders = @{\"X-Octopus-ApiKey\" = $Consolidate_ApiKey }\n$putReqHeaders = @{\"X-HTTP-Method-Override\" = \"PUT\"; \"X-Octopus-ApiKey\" = $Consolidate_ApiKey }\n\n$remWhiteSpace = [bool]::Parse($Consolidate_RemoveWhitespace)\n$deDupe = [bool]::Parse($Consolidate_Dedupe)\n$reverse = ($Consolidate_Order -eq \"Oldest\")\n\n# Get details we'll need\n$projectId = $OctopusParameters['Octopus.Project.Id']\n$thisReleaseNumber = $OctopusParameters['Octopus.Release.Number']\n$lastSuccessfulReleaseId = $OctopusParameters['Octopus.Release.CurrentForEnvironment.Id']\n$lastSuccessfulReleaseNumber = $OctopusParameters['Octopus.Release.CurrentForEnvironment.Number']\n\nfunction Test-SpacesApi {\n\tWrite-Verbose \"Checking API compatibility\";\n\t$rootDocument = Invoke-WebRequest \"$baseUri/api\" -Headers $reqHeaders -UseBasicParsing | ConvertFrom-Json;\n if($rootDocument.Links -ne $null -and $rootDocument.Links.Spaces -ne $null) {\n \tWrite-Verbose \"Spaces API found\"\n \treturn $true;\n }\n Write-Verbose \"Pre-spaces API found\"\n return $false;\n}\n\nif(Test-SpacesApi) {\n\t$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 recieved 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\t$baseApiUrl = \"/api/$spaceId\" ;\n} else {\n\t$baseApiUrl = \"/api\" ;\n}\n\n# Get all previous releases to this environment\n$releaseUri = \"$baseUri$baseApiUrl/projects/$projectId/releases\"\ntry {\n $allReleases = Invoke-WebRequest $releaseUri -Headers $reqheaders -UseBasicParsing | ConvertFrom-Json\n} catch {\n if ($_.Exception.Response.StatusCode.Value__ -ne 404) {\n $result = $_.Exception.Response.GetResponseStream()\n $reader = New-Object System.Io.StreamReader($result);\n $responseBody = $reader.ReadToEnd();\n throw \"Error occurred: $responseBody\"\n }\n}\n\n# Find and aggregate release notes\n$aggregateNotes = @()\n\nWrite-Host \"Finding all release notes between the last successful release: $lastSuccessfulReleaseNumber and this release: $thisReleaseNumber\"\nforeach ($rel in $allReleases.Items) {\n if ($rel.Id -ne $lastSuccessfulReleaseId) {\n Write-Host \"Found release notes for $($rel.Version)\"\n $theseNotes = @()\n #split into lines\n $lines = $rel.ReleaseNotes -split \"`n\"\n foreach ($line in $lines) {\n if (-not $remWhitespace -or -not [string]::IsNullOrWhiteSpace($line)) {\n if (-not $deDupe -or -not $aggregateNotes.Contains($line)) {\n $theseNotes = $theseNotes + $line\n }\n }\n }\n if ($reverse) {\n $aggregateNotes = $theseNotes + $aggregateNotes\n } else {\n $aggregateNotes = $aggregateNotes + $theseNotes\n }\n } else {\n break\n }\n}\n$aggregateNotesText = $aggregateNotes -join \"`n`n\"\n\n# Get the current release\n$releaseUri = \"$baseUri$baseApiUrl/projects/$projectId/releases/$thisReleaseNumber\"\ntry {\n $currentRelease = Invoke-WebRequest $releaseUri -Headers $reqheaders -UseBasicParsing | ConvertFrom-Json\n} catch {\n if ($_.Exception.Response.StatusCode.Value__ -ne 404) {\n $result = $_.Exception.Response.GetResponseStream()\n $reader = New-Object System.Io.StreamReader($result);\n $responseBody = $reader.ReadToEnd();\n throw \"Error occurred: $responseBody\"\n }\n}\n\n# Update the release notes for the current release\n$currentRelease.ReleaseNotes = $aggregateNotesText\nWrite-Host \"Updating release notes for $thisReleaseNumber`:`n`n\"\nWrite-Host $aggregateNotesText\ntry {\n $releaseUri = \"$baseUri$baseApiUrl/releases/$($currentRelease.Id)\"\n $currentReleaseBody = $currentRelease | ConvertTo-Json\n $result = Invoke-WebRequest $releaseUri -Method Post -Headers $putReqHeaders -Body $currentReleaseBody -UseBasicParsing | ConvertFrom-Json\n} catch {\n $result = $_.Exception.Response.GetResponseStream()\n $reader = New-Object System.Io.StreamReader($result);\n $responseBody = $reader.ReadToEnd();\n Write-Host $responseBody\n throw \"Error occurred: $responseBody\"\n}", "Octopus.Action.Script.ScriptFileName": null, "Octopus.Action.Package.FeedId": null, "Octopus.Action.Package.PackageId": null diff --git a/step-templates/octopus-import-certificate.json b/step-templates/octopus-import-certificate.json index 1841793e3..61817f6bb 100644 --- a/step-templates/octopus-import-certificate.json +++ b/step-templates/octopus-import-certificate.json @@ -8,7 +8,7 @@ "Octopus.Action.Script.Syntax": "PowerShell", "Octopus.Action.Script.ScriptSource": "Inline", "Octopus.Action.RunOnServer": "false", - "Octopus.Action.Script.ScriptBody": "<#\n ----- Octopus - Import Certificate ----- \n Paul Marston @paulmarsy (paul@marston.me)\nLinks\n https://github.com/OctopusDeploy/Library/commits/master/step-templates/octopus-import-certificate.json\n#> \n \n$ErrorActionPreference = 'Stop'\n\n$StepTemplate_BaseUrl = $OctopusParameters['Octopus.Web.BaseUrl'].Trim('/')\nif ([string]::IsNullOrWhiteSpace($StepTemplate_ApiKey)) {\n throw \"The step parameter 'API Key' was not found. This step requires an API Key to function, please provide one and try again.\"\n}\nfilter Out-Verbose {\n Write-Verbose ($_ | Out-String)\n}\nfilter Out-Indented {\n $_ | Out-String | % Trim | % Split \"`n\" | % { \"`t$_\" } \n}\nfunction Invoke-OctopusApi {\n param(\n [Parameter(Position = 0, Mandatory)]$Uri,\n [ValidateSet(\"Get\", \"Post\")]$Method = 'Get',\n $Body\n )\n $requestParameters = @{\n Uri = ('{0}/{1}' -f $StepTemplate_BaseUrl, $Uri.TrimStart('/'))\n Method = $Method\n Headers = @{ \"X-Octopus-ApiKey\" = $StepTemplate_ApiKey }\n UseBasicParsing = $true\n }\n Write-Verbose \"$($Method.ToUpperInvariant()) $($requestParameters.Uri)\" \n if ($null -ne $Body) { $requestParameters.Add('Body', ($Body | ConvertTo-Json -Depth 10)) }\n try {\n Invoke-WebRequest @requestParameters | % Content | ConvertFrom-Json | Write-Output\n }\n catch [System.Net.WebException] {\n if ($_.Exception.Response) {\n $errorResponse = [System.IO.StreamReader]::new($_.Exception.Response.GetResponseStream()).ReadToEnd()\n throw (\"$($_.Exception.Message)`n{0}\" -f $errorResponse)\n }\n }\n}\n\nfunction Test-SpacesApi {\n\tWrite-Verbose \"Checking API compatibility\";\n\t$rootDocument = Invoke-OctopusApi 'api/';\n if($rootDocument.Links -ne $null -and $rootDocument.Links.Spaces -ne $null) {\n \tWrite-Verbose \"Spaces API found\"\n \treturn $true;\n }\n Write-Verbose \"Pre-spaces API found\"\n return $false;\n}\n\nif(Test-SpacesApi) {\n\t$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 recieved a blank value, which could indicate you do not have the correct permissions.\";\n }\n\t$baseApiUrl = \"/api/$spaceId\" ;\n} else {\n\t$baseApiUrl = \"/api\" ;\n}\n\n$certificate = switch ($StepTemplate_CertEncoding) {\n 'file' { \n if (!(Test-Path $StepTemplate_Certificate)) {\n throw \"Certificate file $StepTemplate_Certificate does not exist\"\n }\n $certificateBytes = Get-Content -Path $StepTemplate_Certificate -Encoding Byte\n [System.Convert]::ToBase64String($certificateBytes)\n }\n 'base64' {\n $StepTemplate_Certificate\n }\n}\n\n$existingCert = Invoke-OctopusApi \"$baseApiUrl/certificates\" | % Items | ? Name -eq $StepTemplate_CertificateName\nif ($existingCert) {\n Write-Host 'Existing certificate will be archived & replaced...'\n Invoke-OctopusApi (\"$baseApiUrl/certificates/{0}/replace\" -f $existingCert.Id) -Method Post -Body @{\n certificateData = $certificate\n password = $StepTemplate_Password\n } | % {\n $_.CertificateData = $null\n $_.Password = $null\n $_\n } | Out-Verbose\n} else {\n Write-Host 'Creating & importing new certificate...'\n Invoke-OctopusApi \"$baseApiUrl/certificates\" -Method Post -Body @{\n Name = $StepTemplate_CertificateName\n CertificateData = @{\n HasValue = $true\n NewValue = $certificate\n }\n Password = @{\n HasValue = $true\n NewValue = $StepTemplate_Password\n }\n } | Out-Verbose\n}\nWrite-Host 'Certificate has been imported:'\nInvoke-OctopusApi \"$baseApiUrl/certificates\" | % Items | ? Name -eq $StepTemplate_CertificateName | Out-Indented", + "Octopus.Action.Script.ScriptBody": "<#\n ----- Octopus - Import Certificate ----- \n Paul Marston @paulmarsy (paul@marston.me)\nLinks\n https://github.com/OctopusDeploy/Library/commits/master/step-templates/octopus-import-certificate.json\n#> \n \n$ErrorActionPreference = 'Stop'\n\n$StepTemplate_BaseUrl = $OctopusParameters['Octopus.Web.BaseUrl'].Trim('/')\nif ([string]::IsNullOrWhiteSpace($StepTemplate_ApiKey)) {\n throw \"The step parameter 'API Key' was not found. This step requires an API Key to function, please provide one and try again.\"\n}\nfilter Out-Verbose {\n Write-Verbose ($_ | Out-String)\n}\nfilter Out-Indented {\n $_ | Out-String | % Trim | % Split \"`n\" | % { \"`t$_\" } \n}\nfunction Invoke-OctopusApi {\n param(\n [Parameter(Position = 0, Mandatory)]$Uri,\n [ValidateSet(\"Get\", \"Post\")]$Method = 'Get',\n $Body\n )\n $requestParameters = @{\n Uri = ('{0}/{1}' -f $StepTemplate_BaseUrl, $Uri.TrimStart('/'))\n Method = $Method\n Headers = @{ \"X-Octopus-ApiKey\" = $StepTemplate_ApiKey }\n UseBasicParsing = $true\n }\n Write-Verbose \"$($Method.ToUpperInvariant()) $($requestParameters.Uri)\" \n if ($null -ne $Body) { $requestParameters.Add('Body', ($Body | ConvertTo-Json -Depth 10)) }\n try {\n Invoke-WebRequest @requestParameters | % Content | ConvertFrom-Json | Write-Output\n }\n catch [System.Net.WebException] {\n if ($_.Exception.Response) {\n $errorResponse = [System.IO.StreamReader]::new($_.Exception.Response.GetResponseStream()).ReadToEnd()\n throw (\"$($_.Exception.Message)`n{0}\" -f $errorResponse)\n }\n }\n}\n\nfunction Test-SpacesApi {\n\tWrite-Verbose \"Checking API compatibility\";\n\t$rootDocument = Invoke-OctopusApi 'api/';\n if($rootDocument.Links -ne $null -and $rootDocument.Links.Spaces -ne $null) {\n \tWrite-Verbose \"Spaces API found\"\n \treturn $true;\n }\n Write-Verbose \"Pre-spaces API found\"\n return $false;\n}\n\nif(Test-SpacesApi) {\n\t$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 recieved 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\t$baseApiUrl = \"/api/$spaceId\" ;\n} else {\n\t$baseApiUrl = \"/api\" ;\n}\n\n$certificate = switch ($StepTemplate_CertEncoding) {\n 'file' { \n if (!(Test-Path $StepTemplate_Certificate)) {\n throw \"Certificate file $StepTemplate_Certificate does not exist\"\n }\n $certificateBytes = Get-Content -Path $StepTemplate_Certificate -Encoding Byte\n [System.Convert]::ToBase64String($certificateBytes)\n }\n 'base64' {\n $StepTemplate_Certificate\n }\n}\n\n$existingCert = Invoke-OctopusApi \"$baseApiUrl/certificates\" | % Items | ? Name -eq $StepTemplate_CertificateName\nif ($existingCert) {\n Write-Host 'Existing certificate will be archived & replaced...'\n Invoke-OctopusApi (\"$baseApiUrl/certificates/{0}/replace\" -f $existingCert.Id) -Method Post -Body @{\n certificateData = $certificate\n password = $StepTemplate_Password\n } | % {\n $_.CertificateData = $null\n $_.Password = $null\n $_\n } | Out-Verbose\n} else {\n Write-Host 'Creating & importing new certificate...'\n Invoke-OctopusApi \"$baseApiUrl/certificates\" -Method Post -Body @{\n Name = $StepTemplate_CertificateName\n CertificateData = @{\n HasValue = $true\n NewValue = $certificate\n }\n Password = @{\n HasValue = $true\n NewValue = $StepTemplate_Password\n }\n } | Out-Verbose\n}\nWrite-Host 'Certificate has been imported:'\nInvoke-OctopusApi \"$baseApiUrl/certificates\" | % Items | ? Name -eq $StepTemplate_CertificateName | Out-Indented", "Octopus.Action.Script.ScriptFileName": null, "Octopus.Action.Package.FeedId": null, "Octopus.Action.Package.PackageId": null diff --git a/step-templates/octopus-set-Octopus-releaese-notes-from-TFS-query.json b/step-templates/octopus-set-Octopus-releaese-notes-from-TFS-query.json index 8dce2a209..25918507c 100644 --- a/step-templates/octopus-set-Octopus-releaese-notes-from-TFS-query.json +++ b/step-templates/octopus-set-Octopus-releaese-notes-from-TFS-query.json @@ -8,7 +8,7 @@ "Properties": { "Octopus.Action.Script.ScriptSource": "Inline", "Octopus.Action.Script.Syntax": "PowerShell", - "Octopus.Action.Script.ScriptBody": "#TFS\n$instance = $OctopusParameters[\"tfsInstance\"]\n$collection = $OctopusParameters[\"tfsCollection\"]\n$project = $OctopusParameters[\"tfsProject\"]\n$PAT = $OctopusParameters[\"tfsPat\"]\n$pathquery = $OctopusParameters[\"tfsPathQuery\"]\n\n#Octopus\n$octopusAPIKey = $OctopusParameters['octopusAPIKey']\n$baseUri = $OctopusParameters['Octopus.Web.BaseUrl']\n$octopusProjectId = $OctopusParameters['Octopus.Project.Id']\n$thisReleaseNumber = $OctopusParameters['Octopus.Release.Number']\n\nwrite-host \"Instance: $($instance)\"\nwrite-host \"collection: $($collection)\"\nwrite-host \"project: $($project)\"\nwrite-host \"baseUri: $($baseUri)\"\nwrite-host \"projectId: $($projectId)\"\nwrite-host \"thisReleaseNumber: $($thisReleaseNumber)\"\nwrite-host \"TFS path: $($pathquery)\"\n\n#Create HEADERS\n$bytes = [System.Text.Encoding]::ASCII.GetBytes($PAT)\n$base64 = [System.Convert]::ToBase64String($bytes)\n$basicAuthValue = \"Basic $base64\"\n$headers = @{ }\n$headers.Add(\"Authorization\", $basicAuthValue)\n$headers.Add(\"Accept\",\"application/json\")\n$headers.Add(\"Content-Type\",\"application/json\")\n\n$reqheaders = @{\"X-Octopus-ApiKey\" = $octopusAPIKey }\n$putReqHeaders = @{\"X-HTTP-Method-Override\" = \"PUT\"; \"X-Octopus-ApiKey\" = $octopusAPIKey }\n\nfunction Test-SpacesApi {\n\tWrite-Verbose \"Checking API compatibility\";\n\t$rootDocument = Invoke-WebRequest \"$baseUri/api\" -Headers $reqheaders -UseBasicParsing | ConvertFrom-Json;\n if($rootDocument.Links -ne $null -and $rootDocument.Links.Spaces -ne $null) {\n \tWrite-Verbose \"Spaces API found\"\n \treturn $true;\n }\n Write-Verbose \"Pre-spaces API found\"\n return $false;\n}\n\nif(Test-SpacesApi) {\n\t$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 recieved a blank value, which could indicate you do not have the correct permissions.\";\n }\n\t$baseApiUrl = \"/api/$spaceId\" ;\n} else {\n\t$baseApiUrl = \"/api\" ;\n}\n\n# Get the current release\n$releaseUri = \"$baseUri$baseApiUrl/projects/$octopusProjectId/releases/$thisReleaseNumber\"\nwrite-host \"Release uri $($releaseUri)\"\ntry {\n $currentRelease = Invoke-RestMethod $releaseUri -Headers $reqheaders -UseBasicParsing \n} catch {\n if ($_.Exception.Response.StatusCode.Value__ -ne 404) {\n $result = $_.Exception.Response.GetResponseStream()\n $reader = New-Object System.Io.StreamReader($result);\n $responseBody = $reader.ReadToEnd();\n throw \"Error occurred: $responseBody\"\n }\n}\n\nif(![string]::IsNullOrWhiteSpace($currentRelease.ReleaseNotes)){\n\twrite-host \"Release notes already filled in. $($currentRelease.ReleaseNotes)\"\n Set-OctopusVariable -name \"ReleaseNotes\" -value $releaseNotes\n\texit;\n}\n\n\n#Get projectid\n$url = \"http://$($instance)/tfs/$($collection)/$($projectId)/_apis/projects/$($project)?&includeCapabilities=false&includeHistory=false&api-version=2.2\"\nwrite-host \"Invoking url= $($url)\"\n$projectresponse = Invoke-RestMethod $url -Method GET -Headers $headers\n\n$projectid = $projectresponse.id\nwrite-host \"projectid $($projectid)\"\n\n#Get the ID of the query to execute\n$queryResult = Invoke-RestMethod \"http://$($instance)/tfs/$($collection)/$($projectId)/_apis/$($pathquery)?$depth=1&api-version=2.2\" -Method GET -Headers $headers\nwrite-host \"queryResult $($queryResult)\"\n\n#https://{instance}/DefaultCollection/[{project}/]_apis/wit/wiql/{id}?api-version={version}\n$queryResult = Invoke-RestMethod \"http://$($instance)/tfs/$($collection)/$($projectId)/_apis/wit/wiql/$($queryResult.Id)?api-version=2.2\" -Method GET -Headers $headers\n\nWrite-Host \"Found $($queryResult.workItems.Count) number of workitems for query: ReleaseNotes$($releaseTag)\"\n\n$releaseNotes = \"**Work Items:**\"\n\n\nif($queryResult.workItems.Count -eq 0)\n{\n\tWrite-Host \"No work items for release\"\n\t$releaseNotes = \"`n no new work items\"\n}\nelse\n{\n\t#Create a list of ids\n\t$ids = [string]::Join(\"%2C\", ($queryResult.workItems.id))\n\n\t#Get all the work items\n\t$workItems = Invoke-RestMethod \"http://$($instance)/tfs/$($collection)/_apis/wit/workItems?ids=$($ids)&fields=System.Title\" -Method GET -Headers $headers\n\n\tforeach($workItem in $workItems.value)\n\t{\n\t\t#Add line for each work item\n\t\t$releaseNotes = $releaseNotes + \"`n* [$($workItem.id)] (http://$($instance)/tfs/$($collection)/9981e67f-b27c-4628-b5cf-fba1d327aa07/_workitems/edit/$($workItem.id)) : $($workItem.fields.'System.Title')\"\n\t}\n\n}\n\n\n\n# Update the release notes for the current release\n$currentRelease.ReleaseNotes = $releaseNotes \nwrite-host \"Release notes $($currentRelease.ReleaseNotes)\"\nWrite-Host \"Updating release notes for $thisReleaseNumber`:`n`n\"\ntry {\n $releaseUri = \"$baseUri$baseApiUrl/releases/$($currentRelease.Id)\"\n write-host \"Release uri $($releaseUri)\"\n $currentReleaseBody = $currentRelease | ConvertTo-Json\n write-host \"Current release body $($currentReleaseBody)\"\n $result = Invoke-RestMethod $releaseUri -Method Post -Headers $putReqHeaders -Body $currentReleaseBody -UseBasicParsing\n\twrite-host \"result $($result)\"\n} catch {\n $result = $_.Exception.Response.GetResponseStream()\n $reader = New-Object System.Io.StreamReader($result);\n $responseBody = $reader.ReadToEnd();\n Write-Host \"error $($responseBody)\"\n throw \"Error occurred: $responseBody\"\n}\n\nSet-OctopusVariable -name \"ReleaseNotes\" -value $releaseNotes\n" + "Octopus.Action.Script.ScriptBody": "#TFS\n$instance = $OctopusParameters[\"tfsInstance\"]\n$collection = $OctopusParameters[\"tfsCollection\"]\n$project = $OctopusParameters[\"tfsProject\"]\n$PAT = $OctopusParameters[\"tfsPat\"]\n$pathquery = $OctopusParameters[\"tfsPathQuery\"]\n\n#Octopus\n$octopusAPIKey = $OctopusParameters['octopusAPIKey']\n$baseUri = $OctopusParameters['Octopus.Web.BaseUrl']\n$octopusProjectId = $OctopusParameters['Octopus.Project.Id']\n$thisReleaseNumber = $OctopusParameters['Octopus.Release.Number']\n\nwrite-host \"Instance: $($instance)\"\nwrite-host \"collection: $($collection)\"\nwrite-host \"project: $($project)\"\nwrite-host \"baseUri: $($baseUri)\"\nwrite-host \"projectId: $($projectId)\"\nwrite-host \"thisReleaseNumber: $($thisReleaseNumber)\"\nwrite-host \"TFS path: $($pathquery)\"\n\n#Create HEADERS\n$bytes = [System.Text.Encoding]::ASCII.GetBytes($PAT)\n$base64 = [System.Convert]::ToBase64String($bytes)\n$basicAuthValue = \"Basic $base64\"\n$headers = @{ }\n$headers.Add(\"Authorization\", $basicAuthValue)\n$headers.Add(\"Accept\",\"application/json\")\n$headers.Add(\"Content-Type\",\"application/json\")\n\n$reqheaders = @{\"X-Octopus-ApiKey\" = $octopusAPIKey }\n$putReqHeaders = @{\"X-HTTP-Method-Override\" = \"PUT\"; \"X-Octopus-ApiKey\" = $octopusAPIKey }\n\nfunction Test-SpacesApi {\n\tWrite-Verbose \"Checking API compatibility\";\n\t$rootDocument = Invoke-WebRequest \"$baseUri/api\" -Headers $reqheaders -UseBasicParsing | ConvertFrom-Json;\n if($rootDocument.Links -ne $null -and $rootDocument.Links.Spaces -ne $null) {\n \tWrite-Verbose \"Spaces API found\"\n \treturn $true;\n }\n Write-Verbose \"Pre-spaces API found\"\n return $false;\n}\n\nif(Test-SpacesApi) {\n\t$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 recieved 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\t$baseApiUrl = \"/api/$spaceId\" ;\n} else {\n\t$baseApiUrl = \"/api\" ;\n}\n\n# Get the current release\n$releaseUri = \"$baseUri$baseApiUrl/projects/$octopusProjectId/releases/$thisReleaseNumber\"\nwrite-host \"Release uri $($releaseUri)\"\ntry {\n $currentRelease = Invoke-RestMethod $releaseUri -Headers $reqheaders -UseBasicParsing \n} catch {\n if ($_.Exception.Response.StatusCode.Value__ -ne 404) {\n $result = $_.Exception.Response.GetResponseStream()\n $reader = New-Object System.Io.StreamReader($result);\n $responseBody = $reader.ReadToEnd();\n throw \"Error occurred: $responseBody\"\n }\n}\n\nif(![string]::IsNullOrWhiteSpace($currentRelease.ReleaseNotes)){\n\twrite-host \"Release notes already filled in. $($currentRelease.ReleaseNotes)\"\n Set-OctopusVariable -name \"ReleaseNotes\" -value $releaseNotes\n\texit;\n}\n\n\n#Get projectid\n$url = \"http://$($instance)/tfs/$($collection)/$($projectId)/_apis/projects/$($project)?&includeCapabilities=false&includeHistory=false&api-version=2.2\"\nwrite-host \"Invoking url= $($url)\"\n$projectresponse = Invoke-RestMethod $url -Method GET -Headers $headers\n\n$projectid = $projectresponse.id\nwrite-host \"projectid $($projectid)\"\n\n#Get the ID of the query to execute\n$queryResult = Invoke-RestMethod \"http://$($instance)/tfs/$($collection)/$($projectId)/_apis/$($pathquery)?$depth=1&api-version=2.2\" -Method GET -Headers $headers\nwrite-host \"queryResult $($queryResult)\"\n\n#https://{instance}/DefaultCollection/[{project}/]_apis/wit/wiql/{id}?api-version={version}\n$queryResult = Invoke-RestMethod \"http://$($instance)/tfs/$($collection)/$($projectId)/_apis/wit/wiql/$($queryResult.Id)?api-version=2.2\" -Method GET -Headers $headers\n\nWrite-Host \"Found $($queryResult.workItems.Count) number of workitems for query: ReleaseNotes$($releaseTag)\"\n\n$releaseNotes = \"**Work Items:**\"\n\n\nif($queryResult.workItems.Count -eq 0)\n{\n\tWrite-Host \"No work items for release\"\n\t$releaseNotes = \"`n no new work items\"\n}\nelse\n{\n\t#Create a list of ids\n\t$ids = [string]::Join(\"%2C\", ($queryResult.workItems.id))\n\n\t#Get all the work items\n\t$workItems = Invoke-RestMethod \"http://$($instance)/tfs/$($collection)/_apis/wit/workItems?ids=$($ids)&fields=System.Title\" -Method GET -Headers $headers\n\n\tforeach($workItem in $workItems.value)\n\t{\n\t\t#Add line for each work item\n\t\t$releaseNotes = $releaseNotes + \"`n* [$($workItem.id)] (http://$($instance)/tfs/$($collection)/9981e67f-b27c-4628-b5cf-fba1d327aa07/_workitems/edit/$($workItem.id)) : $($workItem.fields.'System.Title')\"\n\t}\n\n}\n\n\n\n# Update the release notes for the current release\n$currentRelease.ReleaseNotes = $releaseNotes \nwrite-host \"Release notes $($currentRelease.ReleaseNotes)\"\nWrite-Host \"Updating release notes for $thisReleaseNumber`:`n`n\"\ntry {\n $releaseUri = \"$baseUri$baseApiUrl/releases/$($currentRelease.Id)\"\n write-host \"Release uri $($releaseUri)\"\n $currentReleaseBody = $currentRelease | ConvertTo-Json\n write-host \"Current release body $($currentReleaseBody)\"\n $result = Invoke-RestMethod $releaseUri -Method Post -Headers $putReqHeaders -Body $currentReleaseBody -UseBasicParsing\n\twrite-host \"result $($result)\"\n} catch {\n $result = $_.Exception.Response.GetResponseStream()\n $reader = New-Object System.Io.StreamReader($result);\n $responseBody = $reader.ReadToEnd();\n Write-Host \"error $($responseBody)\"\n throw \"Error occurred: $responseBody\"\n}\n\nSet-OctopusVariable -name \"ReleaseNotes\" -value $releaseNotes\n" }, "Parameters": [ { diff --git a/step-templates/octopus-wait-for-deployment-target-registration.json b/step-templates/octopus-wait-for-deployment-target-registration.json index 195801f83..4f966b9c4 100644 --- a/step-templates/octopus-wait-for-deployment-target-registration.json +++ b/step-templates/octopus-wait-for-deployment-target-registration.json @@ -8,7 +8,7 @@ "Properties": { "Octopus.Action.Script.Syntax": "PowerShell", "Octopus.Action.Script.ScriptSource": "Inline", - "Octopus.Action.Script.ScriptBody": "# Running outside octopus\nparam(\n [string]$odEnv,\n [string]$odName,\n [string]$odRole,\n [int]$odTimeout,\n [string]$odUrl,\n [string]$odApiKey,\n [switch]$whatIf\n) \n\n$ErrorActionPreference = \"Stop\" \n\nfunction Get-Param($Name, [switch]$Required, $Default) {\n $result = $null\n\n if ($OctopusParameters -ne $null) {\n $result = $OctopusParameters[$Name]\n }\n\n if ($result -eq $null) {\n $variable = Get-Variable $Name -EA SilentlyContinue \n if ($variable -ne $null) {\n $result = $variable.Value\n }\n }\n\n if (!$result -or $result -eq $null) {\n if ($Default) {\n $result = $Default\n } elseif ($Required) {\n throw \"Missing parameter value $Name\"\n }\n }\n\n return $result\n}\n\n& {\n param(\n [string]$odEnv,\n [string]$odName,\n [string]$odRole,\n [int]$odTimeout,\n [string]$odUrl,\n [string]$odApiKey\n )\n\n # If Octopus Deploy's URL/API Key are not provided as params, attempt to retrieve them from Environment Variables\n if (!$odUrl) {\n if ([Environment]::GetEnvironmentVariable(\"OD_API_URL\", \"Machine\")) {\n $odUrl = [Environment]::GetEnvironmentVariable(\"OD_API_URL\", \"Machine\")\n }\n }\n \n if (!$odUrl) { throw \"Octopus Deploy API URL was not available/provided.\" }\n\n if (!$odApiKey) {\n if ([Environment]::GetEnvironmentVariable(\"OD_API_KEY\", \"Machine\")) {\n $odApiKey = [Environment]::GetEnvironmentVariable(\"OD_API_KEY\", \"Machine\")\n }\n } \n \n if (!$odApiKey) { throw \"Octopus Deploy API key was not available/provided.\" }\n\n $header = @{ \"X-Octopus-ApiKey\" = $odApiKey }\n \n Write-Verbose \"Checking API compatibility\";\n $rootDocument = Invoke-WebRequest \"$odUrl/api/\" -Header $header -UseBasicParsing | ConvertFrom-Json;\n if($rootDocument.Links -ne $null -and $rootDocument.Links.Spaces -ne $null) {\n Write-Verbose \"Spaces API found\"\n $hasSpacesApi = $true;\n } else {\n Write-Verbose \"Pre-spaces API found\"\n $hasSpacesApi = $false;\n }\n \n if($hasSpacesApi) {\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 recieved a blank value, which could indicate you do not have the correct permissions.\";\n }\n $baseApiUrl = \"/api/$spaceId\" ;\n } else {\n $baseApiUrl = \"/api\" ;\n } \n \n $environments = (Invoke-WebRequest \"$odUrl$baseApiUrl/environments/all\" -Headers $header -UseBasicParsing).content | ConvertFrom-Json\n $environment = $environments | Where-Object { $_.Name -contains $odEnv }\n if (@($environment).Count -eq 0) { throw \"Could not find environment with the name '$odEnv'\" }\n \n $timeout = new-timespan -Seconds $odTimeout\n $sw = [diagnostics.stopwatch]::StartNew()\n\n Write-Output (\"------------------------------\")\n Write-Output (\"Checking the Deployment Target's registration status:\")\n Write-Output (\"------------------------------\")\n\n while ($true)\n {\n if ($sw.elapsed -gt $timeout) { throw \"Timed out waiting for the Deployment Target to register\" }\n \n $machines = ((Invoke-WebRequest ($odUrl + $environment.Links.Self + \"/machines\") -Headers $header -UseBasicParsing).content | ConvertFrom-Json).items\n if ($odName) { $machines = $machines | Where-Object { $_.Name -like \"*$odName*\" } }\n if ($odRole) { $machines = $machines | Where-Object { $_.Roles -like \"*$odRole*\" } }\n if (@($machines).Count -gt 0) { break }\n\n Write-Output (\"$(Get-Date) | Waiting for Deployment Target to register with the name '$odName' and role '$odRole'\")\n\n Sleep -Seconds 5\n }\n \n Write-Output (\"$(Get-Date) | Deployment Target registered with the name '$odName' and role '$odRole'!\")\n } `\n (Get-Param 'odEnv' -Required) `\n (Get-Param 'odName' -Required) `\n (Get-Param 'odRole') `\n (Get-Param 'odTimeout' -Required) `\n (Get-Param 'odUrl') `\n (Get-Param 'odApiKey')" + "Octopus.Action.Script.ScriptBody": "# Running outside octopus\nparam(\n [string]$odEnv,\n [string]$odName,\n [string]$odRole,\n [int]$odTimeout,\n [string]$odUrl,\n [string]$odApiKey,\n [switch]$whatIf\n) \n\n$ErrorActionPreference = \"Stop\" \n\nfunction Get-Param($Name, [switch]$Required, $Default) {\n $result = $null\n\n if ($OctopusParameters -ne $null) {\n $result = $OctopusParameters[$Name]\n }\n\n if ($result -eq $null) {\n $variable = Get-Variable $Name -EA SilentlyContinue \n if ($variable -ne $null) {\n $result = $variable.Value\n }\n }\n\n if (!$result -or $result -eq $null) {\n if ($Default) {\n $result = $Default\n } elseif ($Required) {\n throw \"Missing parameter value $Name\"\n }\n }\n\n return $result\n}\n\n& {\n param(\n [string]$odEnv,\n [string]$odName,\n [string]$odRole,\n [int]$odTimeout,\n [string]$odUrl,\n [string]$odApiKey\n )\n\n # If Octopus Deploy's URL/API Key are not provided as params, attempt to retrieve them from Environment Variables\n if (!$odUrl) {\n if ([Environment]::GetEnvironmentVariable(\"OD_API_URL\", \"Machine\")) {\n $odUrl = [Environment]::GetEnvironmentVariable(\"OD_API_URL\", \"Machine\")\n }\n }\n \n if (!$odUrl) { throw \"Octopus Deploy API URL was not available/provided.\" }\n\n if (!$odApiKey) {\n if ([Environment]::GetEnvironmentVariable(\"OD_API_KEY\", \"Machine\")) {\n $odApiKey = [Environment]::GetEnvironmentVariable(\"OD_API_KEY\", \"Machine\")\n }\n } \n \n if (!$odApiKey) { throw \"Octopus Deploy API key was not available/provided.\" }\n\n $header = @{ \"X-Octopus-ApiKey\" = $odApiKey }\n \n Write-Verbose \"Checking API compatibility\";\n $rootDocument = Invoke-WebRequest \"$odUrl/api/\" -Header $header -UseBasicParsing | ConvertFrom-Json;\n if($rootDocument.Links -ne $null -and $rootDocument.Links.Spaces -ne $null) {\n Write-Verbose \"Spaces API found\"\n $hasSpacesApi = $true;\n } else {\n Write-Verbose \"Pre-spaces API found\"\n $hasSpacesApi = $false;\n }\n \n if($hasSpacesApi) {\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 recieved 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 $baseApiUrl = \"/api/$spaceId\" ;\n } else {\n $baseApiUrl = \"/api\" ;\n } \n \n $environments = (Invoke-WebRequest \"$odUrl$baseApiUrl/environments/all\" -Headers $header -UseBasicParsing).content | ConvertFrom-Json\n $environment = $environments | Where-Object { $_.Name -contains $odEnv }\n if (@($environment).Count -eq 0) { throw \"Could not find environment with the name '$odEnv'\" }\n \n $timeout = new-timespan -Seconds $odTimeout\n $sw = [diagnostics.stopwatch]::StartNew()\n\n Write-Output (\"------------------------------\")\n Write-Output (\"Checking the Deployment Target's registration status:\")\n Write-Output (\"------------------------------\")\n\n while ($true)\n {\n if ($sw.elapsed -gt $timeout) { throw \"Timed out waiting for the Deployment Target to register\" }\n \n $machines = ((Invoke-WebRequest ($odUrl + $environment.Links.Self + \"/machines\") -Headers $header -UseBasicParsing).content | ConvertFrom-Json).items\n if ($odName) { $machines = $machines | Where-Object { $_.Name -like \"*$odName*\" } }\n if ($odRole) { $machines = $machines | Where-Object { $_.Roles -like \"*$odRole*\" } }\n if (@($machines).Count -gt 0) { break }\n\n Write-Output (\"$(Get-Date) | Waiting for Deployment Target to register with the name '$odName' and role '$odRole'\")\n\n Sleep -Seconds 5\n }\n \n Write-Output (\"$(Get-Date) | Deployment Target registered with the name '$odName' and role '$odRole'!\")\n } `\n (Get-Param 'odEnv' -Required) `\n (Get-Param 'odName' -Required) `\n (Get-Param 'odRole') `\n (Get-Param 'odTimeout' -Required) `\n (Get-Param 'odUrl') `\n (Get-Param 'odApiKey')" }, "Parameters": [ { diff --git a/step-templates/save-octopus-output-variable-with-scoping.json b/step-templates/save-octopus-output-variable-with-scoping.json index fcb29d44b..287b9469d 100644 --- a/step-templates/save-octopus-output-variable-with-scoping.json +++ b/step-templates/save-octopus-output-variable-with-scoping.json @@ -10,7 +10,7 @@ "Octopus.Action.Script.Syntax": "PowerShell", "Octopus.Action.Script.ScriptSource": "Inline", "Octopus.Action.RunOnServer": "true", - "Octopus.Action.Script.ScriptBody": "$ErrorActionPreference = 'Stop'\n\n$StepTemplate_BaseUrl = $OctopusParameters['Octopus.Web.BaseUrl'].Trim('/')\nif ([string]::IsNullOrWhiteSpace($StepTemplate_ApiKey)) {\n throw \"The step parameter 'API Key' was not found. This step requires an API Key to function, please provide one and try again.\"\n}\n\nfunction Invoke-OctopusApi {\n param(\n [Parameter(Position = 0, Mandatory)]$Uri,\n [ValidateSet(\"Get\", \"Put\")]$Method = 'Get',\n $Body\n )\n $requestParameters = @{\n Uri = ('{0}/{1}' -f $StepTemplate_BaseUrl, $Uri.TrimStart('/'))\n Method = $Method\n Headers = @{ \"X-Octopus-ApiKey\" = $StepTemplate_ApiKey }\n UseBasicParsing = $true\n }\n if ($null -ne $Body) { $requestParameters.Add('Body', ($Body | ConvertTo-Json -Depth 10)) }\n Write-Verbose \"$($Method.ToUpperInvariant()) $($requestParameters.Uri)\" \n Invoke-WebRequest @requestParameters | % Content | ConvertFrom-Json | Write-Output\n}\n\nfunction Test-SpacesApi {\n\tWrite-Verbose \"Checking API compatibility\";\n\t$rootDocument = Invoke-OctopusApi 'api/';\n if($rootDocument.Links -ne $null -and $rootDocument.Links.Spaces -ne $null) {\n \tWrite-Verbose \"Spaces API found\"\n \treturn $true;\n }\n Write-Verbose \"Pre-spaces API found\"\n return $false;\n}\n\nif(Test-SpacesApi) {\n\t$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 recieved a blank value, which could indicate you do not have the correct permissions.\";\n }\n\t$baseApiUrl = \"api/$spaceId\" ;\n} else {\n\t$baseApiUrl = \"api\" ;\n}\n\nfunction Check-Scope {\n\tparam(\n \t[Parameter(Position = 0, Mandatory)][string]$ScopeName,\n [Parameter(Position = 1, Mandatory)][array]$ScopeValues,\n [Parameter(Position = 2)][array]$ExistingScopeValue,\n [Parameter(Position = 3)][string]$LookingForScopeValue\n )\n \n if ($LookingForScopeValue) {\n \t\n \tWrite-Host \"Checking $ScopeName Scope\"\n \n $scopes = Create-Scope $ScopeName $ScopeValues $LookingForScopeValue\n \n if (-not ($ExistingScopeValue -and (Compare-Object $ExistingScopeValue $scopes) -eq $null)) {\n \tWrite-Host \"$ScopeName scope does not match\"\n \treturn $false\n }\n Write-Host \"$ScopeName scope matches\"\n } else {\n \tif ($ExistingScopeValue) {\n \tWrite-Host \"$ScopeName scope does not match\"\n \treturn $false\n }\n }\n \n return $true\n}\n\nfunction Create-Scope {\n\tparam(\n \t[Parameter(Position = 0, Mandatory)][string]$ScopeName,\n [Parameter(Position = 1, Mandatory)][array]$ScopeValues,\n [Parameter(Position = 2)][string]$ScopeValue\n )\n \n $scopes = @()\n \n foreach ($scope in $ScopeValue.Split($StepTemplate_ScopeDelimiter)) {\n \tif ($ScopeName -eq \"TenantTag\") {\n \t\t$value = $ScopeValues | Where { $_.Id -eq $scope } | Select -First 1\n \t}\n else {\n \t\t$value = $ScopeValues | Where { $_.Name -eq $scope } | Select -First 1\n \t}\n \t$scopes += $value.Id\n }\n \n return $scopes\n}\n\n$outputVariableKey = \"Octopus.Action[${StepTemplate_DeploymentStep}].Output.${StepTemplate_VariableName}\"\nif (!$OctopusParameters.ContainsKey($outputVariableKey)) {\n throw \"Variable '$StepTemplate_VariableName' has not been output from '$StepTemplate_DeploymentStep'\"\n}\n$isSensitive = [System.Convert]::ToBoolean($StepTemplate_IsSensitive)\n$variableType = if ($isSensitive) { \"Sensitive\" } else { \"String\" }\n\n$variableValue = $OctopusParameters[$outputVariableKey]\nWrite-Host \"Name: $StepTemplate_VariableName\"\nWrite-Host \"Type: $variableType\"\nWrite-Host \"Value: $(if ($isSensitive) { \"********\" } else { $variableValue })\"\nWrite-Host ' '\n\nWrite-Host \"Retrieving $StepTemplate_VariableSetType variable set...\"\nif ($StepTemplate_VariableSetType -eq 'project') {\n $variableSet = Invoke-OctopusApi \"$baseApiUrl/projects/all\" | ? Name -eq $StepTemplate_VariableSetName | % { Invoke-OctopusApi $_.Links.Variables }\n}\nif ($StepTemplate_VariableSetType -eq 'library') {\n $variableSet = Invoke-OctopusApi \"$baseApiUrl/libraryvariablesets/all?ContentType=Variables\" | ? Name -eq $StepTemplate_VariableSetName | % { Invoke-OctopusApi $_.Links.Variables }\n}\nif ($null -eq $variableSet) {\n throw \"Unable to find $StepTemplate_VariableSetType variable set '$StepTemplate_VariableSetName'\"\n}\n\n$variableExists = $false\n\n$variableSet.Variables | ? Name -eq $StepTemplate_TargetName | % {\n\tif (-not (Check-Scope 'Environment' $variableSet.ScopeValues.Environments $_.Scope.Environment $StepTemplate_EnvironmentScope)) {\n \treturn\n }\n\n\tif (-not (Check-Scope 'Machine' $variableSet.ScopeValues.Machines $_.Scope.Machine $StepTemplate_MachineScope)) {\n \treturn\n }\n \n if (-not (Check-Scope 'Role' $variableSet.ScopeValues.Roles $_.Scope.Role $StepTemplate_RoleScope)) {\n \treturn\n }\n \n if (-not (Check-Scope 'Action' $variableSet.ScopeValues.Actions $_.Scope.Action $StepTemplate_ActionScope)) {\n \treturn\n }\n \n if (-not (Check-Scope 'Channel' $variableSet.ScopeValues.Channels $_.Scope.Channel $StepTemplate_ChannelScope)) {\n \treturn\n }\n \n if (-not (Check-Scope 'TenantTag' $variableSet.ScopeValues.TenantTags $_.Scope.TenantTag $StepTemplate_TenantTagScope)) {\n \treturn\n }\n\n Write-Host \"Updating existing variable...\"\n Write-Host \"Existing value:\"\n\tWrite-Host \"$(if ($isSensitive) { \"********\" } else { $_.Value })\"\n $_.Value = $variableValue\n $_.Type = $variableType\n $_.IsSensitive = $isSensitive\n $variableExists = $true\n}\n\nif (!$variableExists) {\n Write-Host \"Creating new variable...\"\n \n $variable = @{\n Name = $StepTemplate_TargetName\n Value = $variableValue\n Type = $variableType\n IsSensitive = $isSensitive\n Scope = @{}\n }\n \n if ($StepTemplate_EnvironmentScope) {\n \t$variable.Scope['Environment'] = (Create-Scope 'Environment' $variableSet.ScopeValues.Environments $StepTemplate_EnvironmentScope)\n }\n if ($StepTemplate_RoleScope) {\n \t$variable.Scope['Role'] = (Create-Scope 'Role' $variableSet.ScopeValues.Roles $StepTemplate_RoleScope)\n }\n if ($StepTemplate_MachineScope) {\n \t$variable.Scope['Machine'] = (Create-Scope 'Machine' $variableSet.ScopeValues.Machines $StepTemplate_MachineScope)\n }\n if ($StepTemplate_ActionScope) {\n \t$variable.Scope['Action'] = (Create-Scope 'Action' $variableSet.ScopeValues.Actions $StepTemplate_ActionScope)\n }\n if ($StepTemplate_ChannelScope) {\n \t$variable.Scope['Channel'] = (Create-Scope 'Channel' $variableSet.ScopeValues.Channels $StepTemplate_ChannelScope)\n }\n if ($StepTemplate_TenantTagScope) {\n $variable.Scope['TenantTag'] = (Create-Scope 'TenantTag' $variableSet.ScopeValues.TenantTags $StepTemplate_TenantTagScope)\n }\n \n $variableSet.Variables += $variable\n}\n\nWrite-Host \"Saving updated variable set...\"\nInvoke-OctopusApi $variableSet.Links.Self -Method Put -Body $variableSet | Out-Null\n" + "Octopus.Action.Script.ScriptBody": "$ErrorActionPreference = 'Stop'\n\n$StepTemplate_BaseUrl = $OctopusParameters['Octopus.Web.BaseUrl'].Trim('/')\nif ([string]::IsNullOrWhiteSpace($StepTemplate_ApiKey)) {\n throw \"The step parameter 'API Key' was not found. This step requires an API Key to function, please provide one and try again.\"\n}\n\nfunction Invoke-OctopusApi {\n param(\n [Parameter(Position = 0, Mandatory)]$Uri,\n [ValidateSet(\"Get\", \"Put\")]$Method = 'Get',\n $Body\n )\n $requestParameters = @{\n Uri = ('{0}/{1}' -f $StepTemplate_BaseUrl, $Uri.TrimStart('/'))\n Method = $Method\n Headers = @{ \"X-Octopus-ApiKey\" = $StepTemplate_ApiKey }\n UseBasicParsing = $true\n }\n if ($null -ne $Body) { $requestParameters.Add('Body', ($Body | ConvertTo-Json -Depth 10)) }\n Write-Verbose \"$($Method.ToUpperInvariant()) $($requestParameters.Uri)\" \n Invoke-WebRequest @requestParameters | % Content | ConvertFrom-Json | Write-Output\n}\n\nfunction Test-SpacesApi {\n\tWrite-Verbose \"Checking API compatibility\";\n\t$rootDocument = Invoke-OctopusApi 'api/';\n if($rootDocument.Links -ne $null -and $rootDocument.Links.Spaces -ne $null) {\n \tWrite-Verbose \"Spaces API found\"\n \treturn $true;\n }\n Write-Verbose \"Pre-spaces API found\"\n return $false;\n}\n\nif(Test-SpacesApi) {\n\t$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 recieved 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\t$baseApiUrl = \"api/$spaceId\" ;\n} else {\n\t$baseApiUrl = \"api\" ;\n}\n\nfunction Check-Scope {\n\tparam(\n \t[Parameter(Position = 0, Mandatory)][string]$ScopeName,\n [Parameter(Position = 1, Mandatory)][array]$ScopeValues,\n [Parameter(Position = 2)][array]$ExistingScopeValue,\n [Parameter(Position = 3)][string]$LookingForScopeValue\n )\n \n if ($LookingForScopeValue) {\n \t\n \tWrite-Host \"Checking $ScopeName Scope\"\n \n $scopes = Create-Scope $ScopeName $ScopeValues $LookingForScopeValue\n \n if (-not ($ExistingScopeValue -and (Compare-Object $ExistingScopeValue $scopes) -eq $null)) {\n \tWrite-Host \"$ScopeName scope does not match\"\n \treturn $false\n }\n Write-Host \"$ScopeName scope matches\"\n } else {\n \tif ($ExistingScopeValue) {\n \tWrite-Host \"$ScopeName scope does not match\"\n \treturn $false\n }\n }\n \n return $true\n}\n\nfunction Create-Scope {\n\tparam(\n \t[Parameter(Position = 0, Mandatory)][string]$ScopeName,\n [Parameter(Position = 1, Mandatory)][array]$ScopeValues,\n [Parameter(Position = 2)][string]$ScopeValue\n )\n \n $scopes = @()\n \n foreach ($scope in $ScopeValue.Split($StepTemplate_ScopeDelimiter)) {\n \tif ($ScopeName -eq \"TenantTag\") {\n \t\t$value = $ScopeValues | Where { $_.Id -eq $scope } | Select -First 1\n \t}\n else {\n \t\t$value = $ScopeValues | Where { $_.Name -eq $scope } | Select -First 1\n \t}\n \t$scopes += $value.Id\n }\n \n return $scopes\n}\n\n$outputVariableKey = \"Octopus.Action[${StepTemplate_DeploymentStep}].Output.${StepTemplate_VariableName}\"\nif (!$OctopusParameters.ContainsKey($outputVariableKey)) {\n throw \"Variable '$StepTemplate_VariableName' has not been output from '$StepTemplate_DeploymentStep'\"\n}\n$isSensitive = [System.Convert]::ToBoolean($StepTemplate_IsSensitive)\n$variableType = if ($isSensitive) { \"Sensitive\" } else { \"String\" }\n\n$variableValue = $OctopusParameters[$outputVariableKey]\nWrite-Host \"Name: $StepTemplate_VariableName\"\nWrite-Host \"Type: $variableType\"\nWrite-Host \"Value: $(if ($isSensitive) { \"********\" } else { $variableValue })\"\nWrite-Host ' '\n\nWrite-Host \"Retrieving $StepTemplate_VariableSetType variable set...\"\nif ($StepTemplate_VariableSetType -eq 'project') {\n $variableSet = Invoke-OctopusApi \"$baseApiUrl/projects/all\" | ? Name -eq $StepTemplate_VariableSetName | % { Invoke-OctopusApi $_.Links.Variables }\n}\nif ($StepTemplate_VariableSetType -eq 'library') {\n $variableSet = Invoke-OctopusApi \"$baseApiUrl/libraryvariablesets/all?ContentType=Variables\" | ? Name -eq $StepTemplate_VariableSetName | % { Invoke-OctopusApi $_.Links.Variables }\n}\nif ($null -eq $variableSet) {\n throw \"Unable to find $StepTemplate_VariableSetType variable set '$StepTemplate_VariableSetName'\"\n}\n\n$variableExists = $false\n\n$variableSet.Variables | ? Name -eq $StepTemplate_TargetName | % {\n\tif (-not (Check-Scope 'Environment' $variableSet.ScopeValues.Environments $_.Scope.Environment $StepTemplate_EnvironmentScope)) {\n \treturn\n }\n\n\tif (-not (Check-Scope 'Machine' $variableSet.ScopeValues.Machines $_.Scope.Machine $StepTemplate_MachineScope)) {\n \treturn\n }\n \n if (-not (Check-Scope 'Role' $variableSet.ScopeValues.Roles $_.Scope.Role $StepTemplate_RoleScope)) {\n \treturn\n }\n \n if (-not (Check-Scope 'Action' $variableSet.ScopeValues.Actions $_.Scope.Action $StepTemplate_ActionScope)) {\n \treturn\n }\n \n if (-not (Check-Scope 'Channel' $variableSet.ScopeValues.Channels $_.Scope.Channel $StepTemplate_ChannelScope)) {\n \treturn\n }\n \n if (-not (Check-Scope 'TenantTag' $variableSet.ScopeValues.TenantTags $_.Scope.TenantTag $StepTemplate_TenantTagScope)) {\n \treturn\n }\n\n Write-Host \"Updating existing variable...\"\n Write-Host \"Existing value:\"\n\tWrite-Host \"$(if ($isSensitive) { \"********\" } else { $_.Value })\"\n $_.Value = $variableValue\n $_.Type = $variableType\n $_.IsSensitive = $isSensitive\n $variableExists = $true\n}\n\nif (!$variableExists) {\n Write-Host \"Creating new variable...\"\n \n $variable = @{\n Name = $StepTemplate_TargetName\n Value = $variableValue\n Type = $variableType\n IsSensitive = $isSensitive\n Scope = @{}\n }\n \n if ($StepTemplate_EnvironmentScope) {\n \t$variable.Scope['Environment'] = (Create-Scope 'Environment' $variableSet.ScopeValues.Environments $StepTemplate_EnvironmentScope)\n }\n if ($StepTemplate_RoleScope) {\n \t$variable.Scope['Role'] = (Create-Scope 'Role' $variableSet.ScopeValues.Roles $StepTemplate_RoleScope)\n }\n if ($StepTemplate_MachineScope) {\n \t$variable.Scope['Machine'] = (Create-Scope 'Machine' $variableSet.ScopeValues.Machines $StepTemplate_MachineScope)\n }\n if ($StepTemplate_ActionScope) {\n \t$variable.Scope['Action'] = (Create-Scope 'Action' $variableSet.ScopeValues.Actions $StepTemplate_ActionScope)\n }\n if ($StepTemplate_ChannelScope) {\n \t$variable.Scope['Channel'] = (Create-Scope 'Channel' $variableSet.ScopeValues.Channels $StepTemplate_ChannelScope)\n }\n if ($StepTemplate_TenantTagScope) {\n $variable.Scope['TenantTag'] = (Create-Scope 'TenantTag' $variableSet.ScopeValues.TenantTags $StepTemplate_TenantTagScope)\n }\n \n $variableSet.Variables += $variable\n}\n\nWrite-Host \"Saving updated variable set...\"\nInvoke-OctopusApi $variableSet.Links.Self -Method Put -Body $variableSet | Out-Null\n" }, "Parameters": [ { diff --git a/step-templates/save-octopus-output-variable.json b/step-templates/save-octopus-output-variable.json index 31818712d..d0eb31780 100644 --- a/step-templates/save-octopus-output-variable.json +++ b/step-templates/save-octopus-output-variable.json @@ -8,7 +8,7 @@ "Octopus.Action.Script.Syntax": "PowerShell", "Octopus.Action.Script.ScriptSource": "Inline", "Octopus.Action.RunOnServer": "true", - "Octopus.Action.Script.ScriptBody": "$ErrorActionPreference = 'Stop'\n\n$StepTemplate_BaseUrl = $OctopusParameters['Octopus.Web.BaseUrl'].Trim('/')\nif ([string]::IsNullOrWhiteSpace($StepTemplate_ApiKey)) {\n throw \"The step parameter 'API Key' was not found. This step requires an API Key to function, please provide one and try again.\"\n}\n\nfunction Invoke-OctopusApi {\n param(\n [Parameter(Position = 0, Mandatory)]$Uri,\n [ValidateSet(\"Get\", \"Put\")]$Method = 'Get',\n $Body\n )\n $requestParameters = @{\n Uri = ('{0}/{1}' -f $StepTemplate_BaseUrl, $Uri.TrimStart('/'))\n Method = $Method\n Headers = @{ \"X-Octopus-ApiKey\" = $StepTemplate_ApiKey }\n UseBasicParsing = $true\n }\n if ($null -ne $Body) { $requestParameters.Add('Body', ($Body | ConvertTo-Json -Depth 10)) }\n Write-Verbose \"$($Method.ToUpperInvariant()) $($requestParameters.Uri)\" \n Invoke-WebRequest @requestParameters | % Content | ConvertFrom-Json | Write-Output\n}\n\nfunction Test-SpacesApi {\n\tWrite-Verbose \"Checking API compatibility\";\n\t$rootDocument = Invoke-OctopusApi 'api/';\n if($rootDocument.Links -ne $null -and $rootDocument.Links.Spaces -ne $null) {\n \tWrite-Verbose \"Spaces API found\"\n \treturn $true;\n }\n Write-Verbose \"Pre-spaces API found\"\n return $false;\n}\n\nif(Test-SpacesApi) {\n\t$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 recieved a blank value, which could indicate you do not have the correct permissions.\";\n }\n\t$baseApiUrl = \"api/$spaceId\" ;\n} else {\n\t$baseApiUrl = \"api\" ;\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 [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$outputVariableKey = \"Octopus.Action[${StepTemplate_DeploymentStep}].Output.${StepTemplate_VariableName}\"\nif (!$OctopusParameters.ContainsKey($outputVariableKey)) {\n throw \"Variable '$StepTemplate_VariableName' has not been output from '$StepTemplate_DeploymentStep'\"\n}\n$isSensitive = [System.Convert]::ToBoolean($StepTemplate_IsSensitive)\n$variableType = if ($isSensitive) { \"Sensitive\" } else { \"String\" }\n\n$variableValue = $OctopusParameters[$outputVariableKey]\nWrite-Host \"Name: $StepTemplate_VariableName\"\nWrite-Host \"Type: $variableType\"\nWrite-Host \"Value: $(if ($isSensitive) { \"********\" } else { $variableValue })\"\nWrite-Host ' '\n\nWrite-Host \"Retrieving $StepTemplate_VariableSetType variable set...\"\nif ($StepTemplate_VariableSetType -eq 'project') {\n $variableSet = Invoke-OctopusApi \"$baseApiUrl/projects/all\" | ? Name -eq $StepTemplate_VariableSetName | % { Invoke-OctopusApi $_.Links.Variables }\n}\nif ($StepTemplate_VariableSetType -eq 'library') {\n $variableSet = Invoke-OctopusApi \"$baseApiUrl/libraryvariablesets/all?ContentType=Variables\" | ? Name -eq $StepTemplate_VariableSetName | % { Invoke-OctopusApi $_.Links.Variables }\n}\nif ($null -eq $variableSet) {\n throw \"Unable to find $StepTemplate_VariableSetType variable set '$StepTemplate_VariableSetName'\"\n}\n\n$variableExists = $false\n$variableSet.Variables | ? Name -eq $StepTemplate_VariableName | % {\n Write-Host \"Updating existing variable...\"\n Write-Verbose \"Existing value: $(if ($isSensitive) { \"********\" } else { $_.Value })\"\n $_.Value = $variableValue\n $_.Type = $variableType\n $_.IsSensitive = $isSensitive\n $_.Scope = Get-OctopusSetting Scope $_.Scope\n $variableExists = $true\n}\nif (!$variableExists) {\n Write-Host \"Creating new variable...\"\n $variableSet.Variables += @{\n Name = $StepTemplate_VariableName\n Value = $variableValue\n Type = $variableType\n IsSensitive = $isSensitive\n Scope = (Get-OctopusSetting Scope @{})\n }\n}\n\nWrite-Host \"Saving updated variable set...\"\nInvoke-OctopusApi $variableSet.Links.Self -Method Put -Body $variableSet | Out-Null", + "Octopus.Action.Script.ScriptBody": "$ErrorActionPreference = 'Stop'\n\n$StepTemplate_BaseUrl = $OctopusParameters['Octopus.Web.BaseUrl'].Trim('/')\nif ([string]::IsNullOrWhiteSpace($StepTemplate_ApiKey)) {\n throw \"The step parameter 'API Key' was not found. This step requires an API Key to function, please provide one and try again.\"\n}\n\nfunction Invoke-OctopusApi {\n param(\n [Parameter(Position = 0, Mandatory)]$Uri,\n [ValidateSet(\"Get\", \"Put\")]$Method = 'Get',\n $Body\n )\n $requestParameters = @{\n Uri = ('{0}/{1}' -f $StepTemplate_BaseUrl, $Uri.TrimStart('/'))\n Method = $Method\n Headers = @{ \"X-Octopus-ApiKey\" = $StepTemplate_ApiKey }\n UseBasicParsing = $true\n }\n if ($null -ne $Body) { $requestParameters.Add('Body', ($Body | ConvertTo-Json -Depth 10)) }\n Write-Verbose \"$($Method.ToUpperInvariant()) $($requestParameters.Uri)\" \n Invoke-WebRequest @requestParameters | % Content | ConvertFrom-Json | Write-Output\n}\n\nfunction Test-SpacesApi {\n\tWrite-Verbose \"Checking API compatibility\";\n\t$rootDocument = Invoke-OctopusApi 'api/';\n if($rootDocument.Links -ne $null -and $rootDocument.Links.Spaces -ne $null) {\n \tWrite-Verbose \"Spaces API found\"\n \treturn $true;\n }\n Write-Verbose \"Pre-spaces API found\"\n return $false;\n}\n\nif(Test-SpacesApi) {\n\t$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 recieved 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\t$baseApiUrl = \"api/$spaceId\" ;\n} else {\n\t$baseApiUrl = \"api\" ;\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 [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$outputVariableKey = \"Octopus.Action[${StepTemplate_DeploymentStep}].Output.${StepTemplate_VariableName}\"\nif (!$OctopusParameters.ContainsKey($outputVariableKey)) {\n throw \"Variable '$StepTemplate_VariableName' has not been output from '$StepTemplate_DeploymentStep'\"\n}\n$isSensitive = [System.Convert]::ToBoolean($StepTemplate_IsSensitive)\n$variableType = if ($isSensitive) { \"Sensitive\" } else { \"String\" }\n\n$variableValue = $OctopusParameters[$outputVariableKey]\nWrite-Host \"Name: $StepTemplate_VariableName\"\nWrite-Host \"Type: $variableType\"\nWrite-Host \"Value: $(if ($isSensitive) { \"********\" } else { $variableValue })\"\nWrite-Host ' '\n\nWrite-Host \"Retrieving $StepTemplate_VariableSetType variable set...\"\nif ($StepTemplate_VariableSetType -eq 'project') {\n $variableSet = Invoke-OctopusApi \"$baseApiUrl/projects/all\" | ? Name -eq $StepTemplate_VariableSetName | % { Invoke-OctopusApi $_.Links.Variables }\n}\nif ($StepTemplate_VariableSetType -eq 'library') {\n $variableSet = Invoke-OctopusApi \"$baseApiUrl/libraryvariablesets/all?ContentType=Variables\" | ? Name -eq $StepTemplate_VariableSetName | % { Invoke-OctopusApi $_.Links.Variables }\n}\nif ($null -eq $variableSet) {\n throw \"Unable to find $StepTemplate_VariableSetType variable set '$StepTemplate_VariableSetName'\"\n}\n\n$variableExists = $false\n$variableSet.Variables | ? Name -eq $StepTemplate_VariableName | % {\n Write-Host \"Updating existing variable...\"\n Write-Verbose \"Existing value: $(if ($isSensitive) { \"********\" } else { $_.Value })\"\n $_.Value = $variableValue\n $_.Type = $variableType\n $_.IsSensitive = $isSensitive\n $_.Scope = Get-OctopusSetting Scope $_.Scope\n $variableExists = $true\n}\nif (!$variableExists) {\n Write-Host \"Creating new variable...\"\n $variableSet.Variables += @{\n Name = $StepTemplate_VariableName\n Value = $variableValue\n Type = $variableType\n IsSensitive = $isSensitive\n Scope = (Get-OctopusSetting Scope @{})\n }\n}\n\nWrite-Host \"Saving updated variable set...\"\nInvoke-OctopusApi $variableSet.Links.Self -Method Put -Body $variableSet | Out-Null", "Octopus.Action.Script.ScriptFileName": null, "Octopus.Action.Package.FeedId": null, "Octopus.Action.Package.PackageId": null From 2313c17bb90daee0105aba6d8ad8a985ea7f4f6c Mon Sep 17 00:00:00 2001 From: Jim Burger Date: Thu, 20 Dec 2018 12:05:51 +1030 Subject: [PATCH 14/16] Corrects the spelling of `received` --- step-templates/clone-tenant.json | 2 +- step-templates/create-tenant.json | 2 +- step-templates/octopus-chain-deployment.json | 2 +- step-templates/octopus-consolidate-releasenotes.json | 2 +- step-templates/octopus-import-certificate.json | 2 +- .../octopus-set-Octopus-releaese-notes-from-TFS-query.json | 2 +- .../octopus-wait-for-deployment-target-registration.json | 2 +- step-templates/save-octopus-output-variable-with-scoping.json | 2 +- step-templates/save-octopus-output-variable.json | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/step-templates/clone-tenant.json b/step-templates/clone-tenant.json index 50ee9ed3b..2e0e34322 100644 --- a/step-templates/clone-tenant.json +++ b/step-templates/clone-tenant.json @@ -8,7 +8,7 @@ "Properties": { "Octopus.Action.Script.ScriptSource": "Inline", "Octopus.Action.Script.Syntax": "PowerShell", - "Octopus.Action.Script.ScriptBody": "$apiKey = $CloneTenantStep_ApiKey\n$tenantToClone = $CloneTenantStep_TenantIdToClone\n$tenantName = $CloneTenantStep_TenantName\n$octopusBaseUrl = $OctopusParameters['Octopus.Web.BaseUrl'].Trim('/')\n$cloneVariables = $CloneTenantStep_CloneVariables\n\n$ErrorActionPreference = 'Stop'\n\nif ([string]::IsNullOrWhiteSpace($apiKey)) {\n throw \"The step parameter 'Octopus API Key' was not found. This step requires an API Key to function, please provide one and try again.\"\n}\n\nfunction Invoke-OctopusApi {\n param(\n [Parameter(Position = 0, Mandatory)]$Uri,\n [ValidateSet(\"Get\", \"Post\", \"Put\", \"Delete\")]$Method = 'Get',\n $Body\n )\n $requestParameters = @{\n Uri = ('{0}/{1}' -f $octopusBaseUrl, $Uri.TrimStart('/'))\n Method = $Method\n Headers = @{ \"X-Octopus-ApiKey\" = $apiKey }\n UseBasicParsing = $true\n }\n if ($null -ne $Body) { $requestParameters.Add('Body', ($Body | ConvertTo-Json -Depth 10)) }\n Write-Verbose \"$($Method.ToUpperInvariant()) $($requestParameters.Uri)\" \n \n\n return Invoke-WebRequest @requestParameters | % Content | ConvertFrom-Json\n}\n\nfunction Test-SpacesApi {\n\tWrite-Verbose \"Checking API compatibility\";\n\t$rootDocument = Invoke-OctopusApi 'api/';\n if($rootDocument.Links -ne $null -and $rootDocument.Links.Spaces -ne $null) {\n \tWrite-Verbose \"Spaces API found\"\n \treturn $true;\n }\n Write-Verbose \"Pre-spaces API found\"\n return $false;\n}\n\nif(Test-SpacesApi) {\n\t$spaceId = $OctopusParameters['Octopus.Space.Id'];\n\tif([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 recieved 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\t$tenantApiUrl = \"api/$spaceId/tenants\" ;\n} else {\n\t$tenantApiUrl = \"api/tenants\" ;\n}\n\n$tenant = Invoke-OctopusApi \"$tenantApiUrl/$tenantToClone\"\n\n$tenant.Id = $null\n$tenant.Name = $tenantName\n\n$newTenant = Invoke-OctopusApi $tenantApiUrl -Method Post -Body $tenant\n\nif ($cloneVariables -eq $true) {\n $variables = Invoke-OctopusApi $tenant.Links.Variables\n $variables.TenantId = $newTenant.Id\n $variables.TenantName = $tenantName\n\n $newVars = Invoke-OctopusApi $newTenant.Links.Variables -Method Put -Body $variables\n}" + "Octopus.Action.Script.ScriptBody": "$apiKey = $CloneTenantStep_ApiKey\n$tenantToClone = $CloneTenantStep_TenantIdToClone\n$tenantName = $CloneTenantStep_TenantName\n$octopusBaseUrl = $OctopusParameters['Octopus.Web.BaseUrl'].Trim('/')\n$cloneVariables = $CloneTenantStep_CloneVariables\n\n$ErrorActionPreference = 'Stop'\n\nif ([string]::IsNullOrWhiteSpace($apiKey)) {\n throw \"The step parameter 'Octopus API Key' was not found. This step requires an API Key to function, please provide one and try again.\"\n}\n\nfunction Invoke-OctopusApi {\n param(\n [Parameter(Position = 0, Mandatory)]$Uri,\n [ValidateSet(\"Get\", \"Post\", \"Put\", \"Delete\")]$Method = 'Get',\n $Body\n )\n $requestParameters = @{\n Uri = ('{0}/{1}' -f $octopusBaseUrl, $Uri.TrimStart('/'))\n Method = $Method\n Headers = @{ \"X-Octopus-ApiKey\" = $apiKey }\n UseBasicParsing = $true\n }\n if ($null -ne $Body) { $requestParameters.Add('Body', ($Body | ConvertTo-Json -Depth 10)) }\n Write-Verbose \"$($Method.ToUpperInvariant()) $($requestParameters.Uri)\" \n \n\n return Invoke-WebRequest @requestParameters | % Content | ConvertFrom-Json\n}\n\nfunction Test-SpacesApi {\n\tWrite-Verbose \"Checking API compatibility\";\n\t$rootDocument = Invoke-OctopusApi 'api/';\n if($rootDocument.Links -ne $null -and $rootDocument.Links.Spaces -ne $null) {\n \tWrite-Verbose \"Spaces API found\"\n \treturn $true;\n }\n Write-Verbose \"Pre-spaces API found\"\n return $false;\n}\n\nif(Test-SpacesApi) {\n\t$spaceId = $OctopusParameters['Octopus.Space.Id'];\n\tif([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\t$tenantApiUrl = \"api/$spaceId/tenants\" ;\n} else {\n\t$tenantApiUrl = \"api/tenants\" ;\n}\n\n$tenant = Invoke-OctopusApi \"$tenantApiUrl/$tenantToClone\"\n\n$tenant.Id = $null\n$tenant.Name = $tenantName\n\n$newTenant = Invoke-OctopusApi $tenantApiUrl -Method Post -Body $tenant\n\nif ($cloneVariables -eq $true) {\n $variables = Invoke-OctopusApi $tenant.Links.Variables\n $variables.TenantId = $newTenant.Id\n $variables.TenantName = $tenantName\n\n $newVars = Invoke-OctopusApi $newTenant.Links.Variables -Method Put -Body $variables\n}" }, "Parameters": [ { diff --git a/step-templates/create-tenant.json b/step-templates/create-tenant.json index 1cee20ff7..dee00369d 100644 --- a/step-templates/create-tenant.json +++ b/step-templates/create-tenant.json @@ -8,7 +8,7 @@ "Properties": { "Octopus.Action.Script.ScriptSource": "Inline", "Octopus.Action.Script.Syntax": "PowerShell", - "Octopus.Action.Script.ScriptBody": "$ErrorActionPreference = 'Stop'\n\n$apiKey = $CreateTenantStep_ApiKey\n$tenantName = $CreateTenantStep_TenantName\n$tenantTags = if ($CreateTenantStep_TenantTags -eq $null) { @() } else { $CreateTenantStep_TenantTags | ConvertFrom-Json }\n$projectEnvironments = if ($CreateTenantStep_ProjectEnvironments -eq $null) { @{} } else { $CreateTenantStep_ProjectEnvironments | ConvertFrom-Json }\n$octopusBaseUrl = $OctopusParameters['Octopus.Web.BaseUrl'].Trim('/')\n\nif ([string]::IsNullOrWhiteSpace($apiKey)) {\n throw \"The step parameter 'Octopus API Key' was not found. This step requires an API Key to function, please provide one and try again.\"\n}\n\nfunction Invoke-OctopusApi {\n param(\n [Parameter(Position = 0, Mandatory)]$Uri,\n [ValidateSet(\"Get\", \"Post\", \"Put\", \"Delete\")]$Method = 'Get',\n $Body\n )\n $requestParameters = @{\n Uri = ('{0}/{1}' -f $octopusBaseUrl, $Uri.TrimStart('/'))\n Method = $Method\n Headers = @{ \"X-Octopus-ApiKey\" = $apiKey }\n UseBasicParsing = $true\n }\n if ($null -ne $Body) { $requestParameters.Add('Body', ($Body | ConvertTo-Json -Depth 10)) }\n Write-Verbose \"$($Method.ToUpperInvariant()) $($requestParameters.Uri)\" \n \n return Invoke-WebRequest @requestParameters | % Content | ConvertFrom-Json\n}\n\nfunction Test-SpacesApi {\n\tWrite-Verbose \"Checking API compatibility\";\n\t$rootDocument = Invoke-OctopusApi 'api/';\n if($rootDocument.Links -ne $null -and $rootDocument.Links.Spaces -ne $null) {\n \tWrite-Verbose \"Spaces API found\"\n \treturn $true;\n }\n Write-Verbose \"Pre-spaces API found\"\n return $false;\n}\n\nif(Test-SpacesApi) {\n\t$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 recieved 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\t$baseApiUrl = \"/api/$spaceId\" ;\n} else {\n\t$baseApiUrl = \"/api\" ;\n}\n\n$body = @{\n\tId = $null\n Name = $tenantName\n TenantTags = @($tenantTags)\n ProjectEnvironments = $projectEnvironments #@{ \"Projects-63\" = @(\"Environments-1\",\"Environments-2\") }\n}\n\nInvoke-OctopusApi \"$baseApiUrl/tenants\" -Method Post -Body $body | Out-Null" + "Octopus.Action.Script.ScriptBody": "$ErrorActionPreference = 'Stop'\n\n$apiKey = $CreateTenantStep_ApiKey\n$tenantName = $CreateTenantStep_TenantName\n$tenantTags = if ($CreateTenantStep_TenantTags -eq $null) { @() } else { $CreateTenantStep_TenantTags | ConvertFrom-Json }\n$projectEnvironments = if ($CreateTenantStep_ProjectEnvironments -eq $null) { @{} } else { $CreateTenantStep_ProjectEnvironments | ConvertFrom-Json }\n$octopusBaseUrl = $OctopusParameters['Octopus.Web.BaseUrl'].Trim('/')\n\nif ([string]::IsNullOrWhiteSpace($apiKey)) {\n throw \"The step parameter 'Octopus API Key' was not found. This step requires an API Key to function, please provide one and try again.\"\n}\n\nfunction Invoke-OctopusApi {\n param(\n [Parameter(Position = 0, Mandatory)]$Uri,\n [ValidateSet(\"Get\", \"Post\", \"Put\", \"Delete\")]$Method = 'Get',\n $Body\n )\n $requestParameters = @{\n Uri = ('{0}/{1}' -f $octopusBaseUrl, $Uri.TrimStart('/'))\n Method = $Method\n Headers = @{ \"X-Octopus-ApiKey\" = $apiKey }\n UseBasicParsing = $true\n }\n if ($null -ne $Body) { $requestParameters.Add('Body', ($Body | ConvertTo-Json -Depth 10)) }\n Write-Verbose \"$($Method.ToUpperInvariant()) $($requestParameters.Uri)\" \n \n return Invoke-WebRequest @requestParameters | % Content | ConvertFrom-Json\n}\n\nfunction Test-SpacesApi {\n\tWrite-Verbose \"Checking API compatibility\";\n\t$rootDocument = Invoke-OctopusApi 'api/';\n if($rootDocument.Links -ne $null -and $rootDocument.Links.Spaces -ne $null) {\n \tWrite-Verbose \"Spaces API found\"\n \treturn $true;\n }\n Write-Verbose \"Pre-spaces API found\"\n return $false;\n}\n\nif(Test-SpacesApi) {\n\t$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\t$baseApiUrl = \"/api/$spaceId\" ;\n} else {\n\t$baseApiUrl = \"/api\" ;\n}\n\n$body = @{\n\tId = $null\n Name = $tenantName\n TenantTags = @($tenantTags)\n ProjectEnvironments = $projectEnvironments #@{ \"Projects-63\" = @(\"Environments-1\",\"Environments-2\") }\n}\n\nInvoke-OctopusApi \"$baseApiUrl/tenants\" -Method Post -Body $body | Out-Null" }, "Parameters": [ { diff --git a/step-templates/octopus-chain-deployment.json b/step-templates/octopus-chain-deployment.json index 6c6d3ba07..2bbf5712a 100644 --- a/step-templates/octopus-chain-deployment.json +++ b/step-templates/octopus-chain-deployment.json @@ -8,7 +8,7 @@ "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\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. - will apply to all steps in the deployment, e.g.\n Octopus.Action.DebugLogging\n Octopus.Action[Step 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 -----\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\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$Chain_BaseUrl = (Get-OctopusSetting OctopusServerUrl $OctopusParameters['Octopus.Web.BaseUrl']).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\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 $Uri = $Uri -replace '{.*?}',''\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 } 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 Test-SpacesApi {\n\tWrite-Verbose \"Checking API compatibility\";\n\t$rootDocument = Invoke-OctopusApi 'api/';\n if($rootDocument.Links -ne $null -and $rootDocument.Links.Spaces -ne $null) {\n \tWrite-Verbose \"Spaces API found\"\n \treturn $true;\n }\n Write-Verbose \"Pre-spaces API found\"\n return $false;\n}\n\n$Chain_BaseApiUrl = \"/api\"\nif(Test-SpacesApi) {\n\t$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 recieved 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\t$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\thidden $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\" | ? 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)$($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)$($this.Lifecycle.Links.Self)\"\n }\n \n hidden $Channel\n [void] SetChannel($channelName) {\n $useDefaultChannel = Test-String $channelName -ForAbsence\n $this.Channel = Invoke-OctopusApi $this.Project.Links.Channels | % Items | ? { $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)$($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)$($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 $this.Channel.Links.Releases | % 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 [void] CreateRelease($releaseVersion) {\n $template = Invoke-OctopusApi ('{0}/template?channel={1}' -f $this.Project.Links.DeploymentProcess, $this.Channel.Id)\n $selectedPackages = @()\n Write-Host 'Resolving package versions...'\n $template.Packages | % {\n $preReleaseTag = $this.Channel.Rules | ? Actions -contains $_.StepName | ? { $null -ne $_ } | % { '&preReleaseTag={0}' -f $_.Tag }\n\n $package = Invoke-OctopusApi (\"$($this.BaseApiUrl)/feeds/{0}/packages?packageId={1}&partialMatch=false&includeMultipleVersions=false&includeNotes=false&includePreRelease=true&take=1{2}\" -f $_.FeedId, $_.PackageId, $preReleaseTag)\n\n Write-Host \"Found $($package.Title) @ $($package.Version) for step $($_.StepName)\"\n $selectedPackages += @{\n StepName = $_.StepName\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 $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 $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) | % {'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]$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 (?MON|TUE|WED|THU|FRI|SAT|SUN)? - Capture an optional day\n \\s*@\\s* - '@' indicates deploying at a specific time\n (?(?:[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 (?\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)(?:(?MON|TUE|WED|THU|FRI|SAT|SUN)?\\s*@\\s*(?(?:[01]?[0-9]|2[0-3]):[0-5][0-9]))?\\s*(?:\\+\\s*(?\\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] [+ ]'\" \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 | ? Name -eq $environmentName | % {\n $_.AutomaticDeploymentTargets\n $_.OptionalDeploymentTargets\n }\n $this.Environments = $this.DeploymentTemplate.PromoteTo | ? { $_.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 | % 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 | % Id\n $this.Tenants = $tenantFilter.Split(\"`n\") | % { [uri]::EscapeUriString($_.Trim()) } | % {\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 } | ? 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 | % 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 | ? Id -eq $tenant.Id | % PromoteTo | ? Id -eq $environment.Id) {\n $logPrefix = ($envPrefix,$tenantPrefix | ? { $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\") | % Trim\n $this.SkipActions = $this.DeploymentPreview.StepsToExecute | ? {\n $_.CanBeSkipped -and ($_.ActionName -in $comparisonArray -or $_.ActionNumber -in $comparisonArray)\n } | % {\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 hidden [hashtable]$FormValues\n [void] SetFormValues($formValuesToSet) {\n $this.FormValues = @{}\n $this.DeploymentPreview.Form.Values | Get-Member -MemberType NoteProperty | % {\n $this.FormValues.Add($_.Name, $this.DeploymentPreview.Form.Values.$($_.Name))\n }\n\n $formValuesToSet.Split(\"`n\") | % {\n $entry = $_.Split('=') | % Trim\n $entryName, $entryValues = $entry\n $entry = @($entryName, $($entryValues -join \"=\"))\n $this.DeploymentPreview.Form.Elements | ? { $_.Control.Name -ieq $entry[0] } | % {\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 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)$($deployment.Links.Web)\"\n Write-Verbose \"`t$($this.BaseUrl)$($deployment.Links.Self)\"\n Write-Verbose \"`t$($this.BaseUrl)$($this.DeploymentContext.BaseApiUrl)/deploymentprocesses/$($deployment.DeploymentProcessId)\"\n Write-Verbose \"`t$($this.BaseUrl)$($deployment.Links.Variables)\"\n Write-Verbose \"`t$($this.BaseUrl)$($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) | % Items\n }\n foreach ($activity in $ActivityLogs) {\n $correlatedInterrupts = $interrupts | ? CorrelationId -eq $activity.Id \n $correlatedInterrupts | ? IsPending -eq $false | % { $this.LogInterruptMessages($activity, $_) }\n\n $this.LogStepTransition($activity) \n $this.LogErrorsAndWarnings($activity)\n $correlatedInterrupts | ? IsPending -eq $true | % { \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 | ? Name -eq Instructions | % Control | % 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 $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 | ? 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 $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 $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$($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$($deploymentContext.Release.Links.Web)\"\nif ($Chain_SnapshotVariables -ieq 'True') {\n $deploymentContext.UpdateVariableSnapshot()\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\n$deploymentControllers = $deploymentContext.GetDeploymentControllers()\nif (Test-String $Chain_StepsToSkip) {\n $deploymentControllers | % { $_.SetStepsToSkip($Chain_StepsToSkip) }\n}\nif (Test-String $Chain_FormValues) {\n $deploymentControllers | % { $_.SetFormValues($Chain_FormValues) }\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 | % 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 | % Task | ? FinishedSuccessfully -eq $false) {\n Show-Heading 'Deployment Failed!'\n Write-Fatal (($deploymentControllers | % Task | % 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']) |\n % { Invoke-OctopusApi $_.Links.ProjectDeploymentProcessSnapshot } |\n % Steps | ? Id -eq $OctopusParameters['Octopus.Step.Id'] |\n % Actions | ? Id -eq $OctopusParameters['Octopus.Action.Id'] |\n % { $_.Properties.Chain_PostDeploy }\nWrite-Verbose \"Raw Post-Deploy Script:`n$rawPostDeployScript\"\n\nAdd-Type -Path (Get-WmiObject Win32_Process | ? ProcessId -eq $PID | % { Get-Process -Id $_.ParentProcessId } | % { Join-Path (Split-Path -Path $_.Path -Parent) 'Octostache.dll' })\n\n$deploymentControllers | % {\n $deployment = $_.Task.Deployment\n $tenant = $_.Tenant\n $variablesDictionary = [Octostache.VariableDictionary]::new()\n Invoke-OctopusApi (\"$Chain_BaseApiUrl/variables/{0}\" -f $deployment.ManifestVariableSetId) | % Variables | ? {\n ($_.IsSensitive -eq $false) -and `\n ($_.Scope.Private -ne 'True') -and `\n\t\t($null -eq $_.Scope.Action) -and `\n\t\t($null -eq $_.Scope.Machine) -and `\n ($null -eq $_.Scope.TargetRole) -and `\n\t\t($null -eq $_.Scope.Role) -and `\n ($null -eq $_.Scope.Tenant -or $_.Scope.Tenant -contains $tenant.Id) -and `\n\t\t($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\t\t($null -eq $_.Scope.Channel -or $_.Scope.Channel -contains $deployment.ChannelId) -and `\n\t\t($null -eq $_.Scope.Project -or $_.Scope.Project -contains $deployment.ProjectId)\n } | % { $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}", + "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\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. - will apply to all steps in the deployment, e.g.\n Octopus.Action.DebugLogging\n Octopus.Action[Step 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 -----\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\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$Chain_BaseUrl = (Get-OctopusSetting OctopusServerUrl $OctopusParameters['Octopus.Web.BaseUrl']).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\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 $Uri = $Uri -replace '{.*?}',''\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 } 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 Test-SpacesApi {\n\tWrite-Verbose \"Checking API compatibility\";\n\t$rootDocument = Invoke-OctopusApi 'api/';\n if($rootDocument.Links -ne $null -and $rootDocument.Links.Spaces -ne $null) {\n \tWrite-Verbose \"Spaces API found\"\n \treturn $true;\n }\n Write-Verbose \"Pre-spaces API found\"\n return $false;\n}\n\n$Chain_BaseApiUrl = \"/api\"\nif(Test-SpacesApi) {\n\t$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\t$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\thidden $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\" | ? 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)$($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)$($this.Lifecycle.Links.Self)\"\n }\n \n hidden $Channel\n [void] SetChannel($channelName) {\n $useDefaultChannel = Test-String $channelName -ForAbsence\n $this.Channel = Invoke-OctopusApi $this.Project.Links.Channels | % Items | ? { $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)$($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)$($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 $this.Channel.Links.Releases | % 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 [void] CreateRelease($releaseVersion) {\n $template = Invoke-OctopusApi ('{0}/template?channel={1}' -f $this.Project.Links.DeploymentProcess, $this.Channel.Id)\n $selectedPackages = @()\n Write-Host 'Resolving package versions...'\n $template.Packages | % {\n $preReleaseTag = $this.Channel.Rules | ? Actions -contains $_.StepName | ? { $null -ne $_ } | % { '&preReleaseTag={0}' -f $_.Tag }\n\n $package = Invoke-OctopusApi (\"$($this.BaseApiUrl)/feeds/{0}/packages?packageId={1}&partialMatch=false&includeMultipleVersions=false&includeNotes=false&includePreRelease=true&take=1{2}\" -f $_.FeedId, $_.PackageId, $preReleaseTag)\n\n Write-Host \"Found $($package.Title) @ $($package.Version) for step $($_.StepName)\"\n $selectedPackages += @{\n StepName = $_.StepName\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 $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 $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) | % {'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]$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 (?MON|TUE|WED|THU|FRI|SAT|SUN)? - Capture an optional day\n \\s*@\\s* - '@' indicates deploying at a specific time\n (?(?:[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 (?\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)(?:(?MON|TUE|WED|THU|FRI|SAT|SUN)?\\s*@\\s*(?(?:[01]?[0-9]|2[0-3]):[0-5][0-9]))?\\s*(?:\\+\\s*(?\\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] [+ ]'\" \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 | ? Name -eq $environmentName | % {\n $_.AutomaticDeploymentTargets\n $_.OptionalDeploymentTargets\n }\n $this.Environments = $this.DeploymentTemplate.PromoteTo | ? { $_.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 | % 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 | % Id\n $this.Tenants = $tenantFilter.Split(\"`n\") | % { [uri]::EscapeUriString($_.Trim()) } | % {\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 } | ? 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 | % 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 | ? Id -eq $tenant.Id | % PromoteTo | ? Id -eq $environment.Id) {\n $logPrefix = ($envPrefix,$tenantPrefix | ? { $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\") | % Trim\n $this.SkipActions = $this.DeploymentPreview.StepsToExecute | ? {\n $_.CanBeSkipped -and ($_.ActionName -in $comparisonArray -or $_.ActionNumber -in $comparisonArray)\n } | % {\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 hidden [hashtable]$FormValues\n [void] SetFormValues($formValuesToSet) {\n $this.FormValues = @{}\n $this.DeploymentPreview.Form.Values | Get-Member -MemberType NoteProperty | % {\n $this.FormValues.Add($_.Name, $this.DeploymentPreview.Form.Values.$($_.Name))\n }\n\n $formValuesToSet.Split(\"`n\") | % {\n $entry = $_.Split('=') | % Trim\n $entryName, $entryValues = $entry\n $entry = @($entryName, $($entryValues -join \"=\"))\n $this.DeploymentPreview.Form.Elements | ? { $_.Control.Name -ieq $entry[0] } | % {\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 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)$($deployment.Links.Web)\"\n Write-Verbose \"`t$($this.BaseUrl)$($deployment.Links.Self)\"\n Write-Verbose \"`t$($this.BaseUrl)$($this.DeploymentContext.BaseApiUrl)/deploymentprocesses/$($deployment.DeploymentProcessId)\"\n Write-Verbose \"`t$($this.BaseUrl)$($deployment.Links.Variables)\"\n Write-Verbose \"`t$($this.BaseUrl)$($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) | % Items\n }\n foreach ($activity in $ActivityLogs) {\n $correlatedInterrupts = $interrupts | ? CorrelationId -eq $activity.Id \n $correlatedInterrupts | ? IsPending -eq $false | % { $this.LogInterruptMessages($activity, $_) }\n\n $this.LogStepTransition($activity) \n $this.LogErrorsAndWarnings($activity)\n $correlatedInterrupts | ? IsPending -eq $true | % { \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 | ? Name -eq Instructions | % Control | % 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 $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 | ? 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 $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 $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$($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$($deploymentContext.Release.Links.Web)\"\nif ($Chain_SnapshotVariables -ieq 'True') {\n $deploymentContext.UpdateVariableSnapshot()\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\n$deploymentControllers = $deploymentContext.GetDeploymentControllers()\nif (Test-String $Chain_StepsToSkip) {\n $deploymentControllers | % { $_.SetStepsToSkip($Chain_StepsToSkip) }\n}\nif (Test-String $Chain_FormValues) {\n $deploymentControllers | % { $_.SetFormValues($Chain_FormValues) }\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 | % 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 | % Task | ? FinishedSuccessfully -eq $false) {\n Show-Heading 'Deployment Failed!'\n Write-Fatal (($deploymentControllers | % Task | % 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']) |\n % { Invoke-OctopusApi $_.Links.ProjectDeploymentProcessSnapshot } |\n % Steps | ? Id -eq $OctopusParameters['Octopus.Step.Id'] |\n % Actions | ? Id -eq $OctopusParameters['Octopus.Action.Id'] |\n % { $_.Properties.Chain_PostDeploy }\nWrite-Verbose \"Raw Post-Deploy Script:`n$rawPostDeployScript\"\n\nAdd-Type -Path (Get-WmiObject Win32_Process | ? ProcessId -eq $PID | % { Get-Process -Id $_.ParentProcessId } | % { Join-Path (Split-Path -Path $_.Path -Parent) 'Octostache.dll' })\n\n$deploymentControllers | % {\n $deployment = $_.Task.Deployment\n $tenant = $_.Tenant\n $variablesDictionary = [Octostache.VariableDictionary]::new()\n Invoke-OctopusApi (\"$Chain_BaseApiUrl/variables/{0}\" -f $deployment.ManifestVariableSetId) | % Variables | ? {\n ($_.IsSensitive -eq $false) -and `\n ($_.Scope.Private -ne 'True') -and `\n\t\t($null -eq $_.Scope.Action) -and `\n\t\t($null -eq $_.Scope.Machine) -and `\n ($null -eq $_.Scope.TargetRole) -and `\n\t\t($null -eq $_.Scope.Role) -and `\n ($null -eq $_.Scope.Tenant -or $_.Scope.Tenant -contains $tenant.Id) -and `\n\t\t($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\t\t($null -eq $_.Scope.Channel -or $_.Scope.Channel -contains $deployment.ChannelId) -and `\n\t\t($null -eq $_.Scope.Project -or $_.Scope.Project -contains $deployment.ProjectId)\n } | % { $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}", "Octopus.Action.Script.ScriptFileName": null, "Octopus.Action.Package.FeedId": null, "Octopus.Action.Package.PackageId": null diff --git a/step-templates/octopus-consolidate-releasenotes.json b/step-templates/octopus-consolidate-releasenotes.json index 16421af60..8e3b41d01 100644 --- a/step-templates/octopus-consolidate-releasenotes.json +++ b/step-templates/octopus-consolidate-releasenotes.json @@ -8,7 +8,7 @@ "Octopus.Action.Script.Syntax": "PowerShell", "Octopus.Action.Script.ScriptSource": "Inline", "Octopus.Action.RunOnServer": "true", - "Octopus.Action.Script.ScriptBody": "$baseUri = $OctopusParameters['Octopus.Web.BaseUrl']\n$reqheaders = @{\"X-Octopus-ApiKey\" = $Consolidate_ApiKey }\n$putReqHeaders = @{\"X-HTTP-Method-Override\" = \"PUT\"; \"X-Octopus-ApiKey\" = $Consolidate_ApiKey }\n\n$remWhiteSpace = [bool]::Parse($Consolidate_RemoveWhitespace)\n$deDupe = [bool]::Parse($Consolidate_Dedupe)\n$reverse = ($Consolidate_Order -eq \"Oldest\")\n\n# Get details we'll need\n$projectId = $OctopusParameters['Octopus.Project.Id']\n$thisReleaseNumber = $OctopusParameters['Octopus.Release.Number']\n$lastSuccessfulReleaseId = $OctopusParameters['Octopus.Release.CurrentForEnvironment.Id']\n$lastSuccessfulReleaseNumber = $OctopusParameters['Octopus.Release.CurrentForEnvironment.Number']\n\nfunction Test-SpacesApi {\n\tWrite-Verbose \"Checking API compatibility\";\n\t$rootDocument = Invoke-WebRequest \"$baseUri/api\" -Headers $reqHeaders -UseBasicParsing | ConvertFrom-Json;\n if($rootDocument.Links -ne $null -and $rootDocument.Links.Spaces -ne $null) {\n \tWrite-Verbose \"Spaces API found\"\n \treturn $true;\n }\n Write-Verbose \"Pre-spaces API found\"\n return $false;\n}\n\nif(Test-SpacesApi) {\n\t$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 recieved 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\t$baseApiUrl = \"/api/$spaceId\" ;\n} else {\n\t$baseApiUrl = \"/api\" ;\n}\n\n# Get all previous releases to this environment\n$releaseUri = \"$baseUri$baseApiUrl/projects/$projectId/releases\"\ntry {\n $allReleases = Invoke-WebRequest $releaseUri -Headers $reqheaders -UseBasicParsing | ConvertFrom-Json\n} catch {\n if ($_.Exception.Response.StatusCode.Value__ -ne 404) {\n $result = $_.Exception.Response.GetResponseStream()\n $reader = New-Object System.Io.StreamReader($result);\n $responseBody = $reader.ReadToEnd();\n throw \"Error occurred: $responseBody\"\n }\n}\n\n# Find and aggregate release notes\n$aggregateNotes = @()\n\nWrite-Host \"Finding all release notes between the last successful release: $lastSuccessfulReleaseNumber and this release: $thisReleaseNumber\"\nforeach ($rel in $allReleases.Items) {\n if ($rel.Id -ne $lastSuccessfulReleaseId) {\n Write-Host \"Found release notes for $($rel.Version)\"\n $theseNotes = @()\n #split into lines\n $lines = $rel.ReleaseNotes -split \"`n\"\n foreach ($line in $lines) {\n if (-not $remWhitespace -or -not [string]::IsNullOrWhiteSpace($line)) {\n if (-not $deDupe -or -not $aggregateNotes.Contains($line)) {\n $theseNotes = $theseNotes + $line\n }\n }\n }\n if ($reverse) {\n $aggregateNotes = $theseNotes + $aggregateNotes\n } else {\n $aggregateNotes = $aggregateNotes + $theseNotes\n }\n } else {\n break\n }\n}\n$aggregateNotesText = $aggregateNotes -join \"`n`n\"\n\n# Get the current release\n$releaseUri = \"$baseUri$baseApiUrl/projects/$projectId/releases/$thisReleaseNumber\"\ntry {\n $currentRelease = Invoke-WebRequest $releaseUri -Headers $reqheaders -UseBasicParsing | ConvertFrom-Json\n} catch {\n if ($_.Exception.Response.StatusCode.Value__ -ne 404) {\n $result = $_.Exception.Response.GetResponseStream()\n $reader = New-Object System.Io.StreamReader($result);\n $responseBody = $reader.ReadToEnd();\n throw \"Error occurred: $responseBody\"\n }\n}\n\n# Update the release notes for the current release\n$currentRelease.ReleaseNotes = $aggregateNotesText\nWrite-Host \"Updating release notes for $thisReleaseNumber`:`n`n\"\nWrite-Host $aggregateNotesText\ntry {\n $releaseUri = \"$baseUri$baseApiUrl/releases/$($currentRelease.Id)\"\n $currentReleaseBody = $currentRelease | ConvertTo-Json\n $result = Invoke-WebRequest $releaseUri -Method Post -Headers $putReqHeaders -Body $currentReleaseBody -UseBasicParsing | ConvertFrom-Json\n} catch {\n $result = $_.Exception.Response.GetResponseStream()\n $reader = New-Object System.Io.StreamReader($result);\n $responseBody = $reader.ReadToEnd();\n Write-Host $responseBody\n throw \"Error occurred: $responseBody\"\n}", + "Octopus.Action.Script.ScriptBody": "$baseUri = $OctopusParameters['Octopus.Web.BaseUrl']\n$reqheaders = @{\"X-Octopus-ApiKey\" = $Consolidate_ApiKey }\n$putReqHeaders = @{\"X-HTTP-Method-Override\" = \"PUT\"; \"X-Octopus-ApiKey\" = $Consolidate_ApiKey }\n\n$remWhiteSpace = [bool]::Parse($Consolidate_RemoveWhitespace)\n$deDupe = [bool]::Parse($Consolidate_Dedupe)\n$reverse = ($Consolidate_Order -eq \"Oldest\")\n\n# Get details we'll need\n$projectId = $OctopusParameters['Octopus.Project.Id']\n$thisReleaseNumber = $OctopusParameters['Octopus.Release.Number']\n$lastSuccessfulReleaseId = $OctopusParameters['Octopus.Release.CurrentForEnvironment.Id']\n$lastSuccessfulReleaseNumber = $OctopusParameters['Octopus.Release.CurrentForEnvironment.Number']\n\nfunction Test-SpacesApi {\n\tWrite-Verbose \"Checking API compatibility\";\n\t$rootDocument = Invoke-WebRequest \"$baseUri/api\" -Headers $reqHeaders -UseBasicParsing | ConvertFrom-Json;\n if($rootDocument.Links -ne $null -and $rootDocument.Links.Spaces -ne $null) {\n \tWrite-Verbose \"Spaces API found\"\n \treturn $true;\n }\n Write-Verbose \"Pre-spaces API found\"\n return $false;\n}\n\nif(Test-SpacesApi) {\n\t$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\t$baseApiUrl = \"/api/$spaceId\" ;\n} else {\n\t$baseApiUrl = \"/api\" ;\n}\n\n# Get all previous releases to this environment\n$releaseUri = \"$baseUri$baseApiUrl/projects/$projectId/releases\"\ntry {\n $allReleases = Invoke-WebRequest $releaseUri -Headers $reqheaders -UseBasicParsing | ConvertFrom-Json\n} catch {\n if ($_.Exception.Response.StatusCode.Value__ -ne 404) {\n $result = $_.Exception.Response.GetResponseStream()\n $reader = New-Object System.Io.StreamReader($result);\n $responseBody = $reader.ReadToEnd();\n throw \"Error occurred: $responseBody\"\n }\n}\n\n# Find and aggregate release notes\n$aggregateNotes = @()\n\nWrite-Host \"Finding all release notes between the last successful release: $lastSuccessfulReleaseNumber and this release: $thisReleaseNumber\"\nforeach ($rel in $allReleases.Items) {\n if ($rel.Id -ne $lastSuccessfulReleaseId) {\n Write-Host \"Found release notes for $($rel.Version)\"\n $theseNotes = @()\n #split into lines\n $lines = $rel.ReleaseNotes -split \"`n\"\n foreach ($line in $lines) {\n if (-not $remWhitespace -or -not [string]::IsNullOrWhiteSpace($line)) {\n if (-not $deDupe -or -not $aggregateNotes.Contains($line)) {\n $theseNotes = $theseNotes + $line\n }\n }\n }\n if ($reverse) {\n $aggregateNotes = $theseNotes + $aggregateNotes\n } else {\n $aggregateNotes = $aggregateNotes + $theseNotes\n }\n } else {\n break\n }\n}\n$aggregateNotesText = $aggregateNotes -join \"`n`n\"\n\n# Get the current release\n$releaseUri = \"$baseUri$baseApiUrl/projects/$projectId/releases/$thisReleaseNumber\"\ntry {\n $currentRelease = Invoke-WebRequest $releaseUri -Headers $reqheaders -UseBasicParsing | ConvertFrom-Json\n} catch {\n if ($_.Exception.Response.StatusCode.Value__ -ne 404) {\n $result = $_.Exception.Response.GetResponseStream()\n $reader = New-Object System.Io.StreamReader($result);\n $responseBody = $reader.ReadToEnd();\n throw \"Error occurred: $responseBody\"\n }\n}\n\n# Update the release notes for the current release\n$currentRelease.ReleaseNotes = $aggregateNotesText\nWrite-Host \"Updating release notes for $thisReleaseNumber`:`n`n\"\nWrite-Host $aggregateNotesText\ntry {\n $releaseUri = \"$baseUri$baseApiUrl/releases/$($currentRelease.Id)\"\n $currentReleaseBody = $currentRelease | ConvertTo-Json\n $result = Invoke-WebRequest $releaseUri -Method Post -Headers $putReqHeaders -Body $currentReleaseBody -UseBasicParsing | ConvertFrom-Json\n} catch {\n $result = $_.Exception.Response.GetResponseStream()\n $reader = New-Object System.Io.StreamReader($result);\n $responseBody = $reader.ReadToEnd();\n Write-Host $responseBody\n throw \"Error occurred: $responseBody\"\n}", "Octopus.Action.Script.ScriptFileName": null, "Octopus.Action.Package.FeedId": null, "Octopus.Action.Package.PackageId": null diff --git a/step-templates/octopus-import-certificate.json b/step-templates/octopus-import-certificate.json index 61817f6bb..d640394cd 100644 --- a/step-templates/octopus-import-certificate.json +++ b/step-templates/octopus-import-certificate.json @@ -8,7 +8,7 @@ "Octopus.Action.Script.Syntax": "PowerShell", "Octopus.Action.Script.ScriptSource": "Inline", "Octopus.Action.RunOnServer": "false", - "Octopus.Action.Script.ScriptBody": "<#\n ----- Octopus - Import Certificate ----- \n Paul Marston @paulmarsy (paul@marston.me)\nLinks\n https://github.com/OctopusDeploy/Library/commits/master/step-templates/octopus-import-certificate.json\n#> \n \n$ErrorActionPreference = 'Stop'\n\n$StepTemplate_BaseUrl = $OctopusParameters['Octopus.Web.BaseUrl'].Trim('/')\nif ([string]::IsNullOrWhiteSpace($StepTemplate_ApiKey)) {\n throw \"The step parameter 'API Key' was not found. This step requires an API Key to function, please provide one and try again.\"\n}\nfilter Out-Verbose {\n Write-Verbose ($_ | Out-String)\n}\nfilter Out-Indented {\n $_ | Out-String | % Trim | % Split \"`n\" | % { \"`t$_\" } \n}\nfunction Invoke-OctopusApi {\n param(\n [Parameter(Position = 0, Mandatory)]$Uri,\n [ValidateSet(\"Get\", \"Post\")]$Method = 'Get',\n $Body\n )\n $requestParameters = @{\n Uri = ('{0}/{1}' -f $StepTemplate_BaseUrl, $Uri.TrimStart('/'))\n Method = $Method\n Headers = @{ \"X-Octopus-ApiKey\" = $StepTemplate_ApiKey }\n UseBasicParsing = $true\n }\n Write-Verbose \"$($Method.ToUpperInvariant()) $($requestParameters.Uri)\" \n if ($null -ne $Body) { $requestParameters.Add('Body', ($Body | ConvertTo-Json -Depth 10)) }\n try {\n Invoke-WebRequest @requestParameters | % Content | ConvertFrom-Json | Write-Output\n }\n catch [System.Net.WebException] {\n if ($_.Exception.Response) {\n $errorResponse = [System.IO.StreamReader]::new($_.Exception.Response.GetResponseStream()).ReadToEnd()\n throw (\"$($_.Exception.Message)`n{0}\" -f $errorResponse)\n }\n }\n}\n\nfunction Test-SpacesApi {\n\tWrite-Verbose \"Checking API compatibility\";\n\t$rootDocument = Invoke-OctopusApi 'api/';\n if($rootDocument.Links -ne $null -and $rootDocument.Links.Spaces -ne $null) {\n \tWrite-Verbose \"Spaces API found\"\n \treturn $true;\n }\n Write-Verbose \"Pre-spaces API found\"\n return $false;\n}\n\nif(Test-SpacesApi) {\n\t$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 recieved 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\t$baseApiUrl = \"/api/$spaceId\" ;\n} else {\n\t$baseApiUrl = \"/api\" ;\n}\n\n$certificate = switch ($StepTemplate_CertEncoding) {\n 'file' { \n if (!(Test-Path $StepTemplate_Certificate)) {\n throw \"Certificate file $StepTemplate_Certificate does not exist\"\n }\n $certificateBytes = Get-Content -Path $StepTemplate_Certificate -Encoding Byte\n [System.Convert]::ToBase64String($certificateBytes)\n }\n 'base64' {\n $StepTemplate_Certificate\n }\n}\n\n$existingCert = Invoke-OctopusApi \"$baseApiUrl/certificates\" | % Items | ? Name -eq $StepTemplate_CertificateName\nif ($existingCert) {\n Write-Host 'Existing certificate will be archived & replaced...'\n Invoke-OctopusApi (\"$baseApiUrl/certificates/{0}/replace\" -f $existingCert.Id) -Method Post -Body @{\n certificateData = $certificate\n password = $StepTemplate_Password\n } | % {\n $_.CertificateData = $null\n $_.Password = $null\n $_\n } | Out-Verbose\n} else {\n Write-Host 'Creating & importing new certificate...'\n Invoke-OctopusApi \"$baseApiUrl/certificates\" -Method Post -Body @{\n Name = $StepTemplate_CertificateName\n CertificateData = @{\n HasValue = $true\n NewValue = $certificate\n }\n Password = @{\n HasValue = $true\n NewValue = $StepTemplate_Password\n }\n } | Out-Verbose\n}\nWrite-Host 'Certificate has been imported:'\nInvoke-OctopusApi \"$baseApiUrl/certificates\" | % Items | ? Name -eq $StepTemplate_CertificateName | Out-Indented", + "Octopus.Action.Script.ScriptBody": "<#\n ----- Octopus - Import Certificate ----- \n Paul Marston @paulmarsy (paul@marston.me)\nLinks\n https://github.com/OctopusDeploy/Library/commits/master/step-templates/octopus-import-certificate.json\n#> \n \n$ErrorActionPreference = 'Stop'\n\n$StepTemplate_BaseUrl = $OctopusParameters['Octopus.Web.BaseUrl'].Trim('/')\nif ([string]::IsNullOrWhiteSpace($StepTemplate_ApiKey)) {\n throw \"The step parameter 'API Key' was not found. This step requires an API Key to function, please provide one and try again.\"\n}\nfilter Out-Verbose {\n Write-Verbose ($_ | Out-String)\n}\nfilter Out-Indented {\n $_ | Out-String | % Trim | % Split \"`n\" | % { \"`t$_\" } \n}\nfunction Invoke-OctopusApi {\n param(\n [Parameter(Position = 0, Mandatory)]$Uri,\n [ValidateSet(\"Get\", \"Post\")]$Method = 'Get',\n $Body\n )\n $requestParameters = @{\n Uri = ('{0}/{1}' -f $StepTemplate_BaseUrl, $Uri.TrimStart('/'))\n Method = $Method\n Headers = @{ \"X-Octopus-ApiKey\" = $StepTemplate_ApiKey }\n UseBasicParsing = $true\n }\n Write-Verbose \"$($Method.ToUpperInvariant()) $($requestParameters.Uri)\" \n if ($null -ne $Body) { $requestParameters.Add('Body', ($Body | ConvertTo-Json -Depth 10)) }\n try {\n Invoke-WebRequest @requestParameters | % Content | ConvertFrom-Json | Write-Output\n }\n catch [System.Net.WebException] {\n if ($_.Exception.Response) {\n $errorResponse = [System.IO.StreamReader]::new($_.Exception.Response.GetResponseStream()).ReadToEnd()\n throw (\"$($_.Exception.Message)`n{0}\" -f $errorResponse)\n }\n }\n}\n\nfunction Test-SpacesApi {\n\tWrite-Verbose \"Checking API compatibility\";\n\t$rootDocument = Invoke-OctopusApi 'api/';\n if($rootDocument.Links -ne $null -and $rootDocument.Links.Spaces -ne $null) {\n \tWrite-Verbose \"Spaces API found\"\n \treturn $true;\n }\n Write-Verbose \"Pre-spaces API found\"\n return $false;\n}\n\nif(Test-SpacesApi) {\n\t$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\t$baseApiUrl = \"/api/$spaceId\" ;\n} else {\n\t$baseApiUrl = \"/api\" ;\n}\n\n$certificate = switch ($StepTemplate_CertEncoding) {\n 'file' { \n if (!(Test-Path $StepTemplate_Certificate)) {\n throw \"Certificate file $StepTemplate_Certificate does not exist\"\n }\n $certificateBytes = Get-Content -Path $StepTemplate_Certificate -Encoding Byte\n [System.Convert]::ToBase64String($certificateBytes)\n }\n 'base64' {\n $StepTemplate_Certificate\n }\n}\n\n$existingCert = Invoke-OctopusApi \"$baseApiUrl/certificates\" | % Items | ? Name -eq $StepTemplate_CertificateName\nif ($existingCert) {\n Write-Host 'Existing certificate will be archived & replaced...'\n Invoke-OctopusApi (\"$baseApiUrl/certificates/{0}/replace\" -f $existingCert.Id) -Method Post -Body @{\n certificateData = $certificate\n password = $StepTemplate_Password\n } | % {\n $_.CertificateData = $null\n $_.Password = $null\n $_\n } | Out-Verbose\n} else {\n Write-Host 'Creating & importing new certificate...'\n Invoke-OctopusApi \"$baseApiUrl/certificates\" -Method Post -Body @{\n Name = $StepTemplate_CertificateName\n CertificateData = @{\n HasValue = $true\n NewValue = $certificate\n }\n Password = @{\n HasValue = $true\n NewValue = $StepTemplate_Password\n }\n } | Out-Verbose\n}\nWrite-Host 'Certificate has been imported:'\nInvoke-OctopusApi \"$baseApiUrl/certificates\" | % Items | ? Name -eq $StepTemplate_CertificateName | Out-Indented", "Octopus.Action.Script.ScriptFileName": null, "Octopus.Action.Package.FeedId": null, "Octopus.Action.Package.PackageId": null diff --git a/step-templates/octopus-set-Octopus-releaese-notes-from-TFS-query.json b/step-templates/octopus-set-Octopus-releaese-notes-from-TFS-query.json index 25918507c..26291057c 100644 --- a/step-templates/octopus-set-Octopus-releaese-notes-from-TFS-query.json +++ b/step-templates/octopus-set-Octopus-releaese-notes-from-TFS-query.json @@ -8,7 +8,7 @@ "Properties": { "Octopus.Action.Script.ScriptSource": "Inline", "Octopus.Action.Script.Syntax": "PowerShell", - "Octopus.Action.Script.ScriptBody": "#TFS\n$instance = $OctopusParameters[\"tfsInstance\"]\n$collection = $OctopusParameters[\"tfsCollection\"]\n$project = $OctopusParameters[\"tfsProject\"]\n$PAT = $OctopusParameters[\"tfsPat\"]\n$pathquery = $OctopusParameters[\"tfsPathQuery\"]\n\n#Octopus\n$octopusAPIKey = $OctopusParameters['octopusAPIKey']\n$baseUri = $OctopusParameters['Octopus.Web.BaseUrl']\n$octopusProjectId = $OctopusParameters['Octopus.Project.Id']\n$thisReleaseNumber = $OctopusParameters['Octopus.Release.Number']\n\nwrite-host \"Instance: $($instance)\"\nwrite-host \"collection: $($collection)\"\nwrite-host \"project: $($project)\"\nwrite-host \"baseUri: $($baseUri)\"\nwrite-host \"projectId: $($projectId)\"\nwrite-host \"thisReleaseNumber: $($thisReleaseNumber)\"\nwrite-host \"TFS path: $($pathquery)\"\n\n#Create HEADERS\n$bytes = [System.Text.Encoding]::ASCII.GetBytes($PAT)\n$base64 = [System.Convert]::ToBase64String($bytes)\n$basicAuthValue = \"Basic $base64\"\n$headers = @{ }\n$headers.Add(\"Authorization\", $basicAuthValue)\n$headers.Add(\"Accept\",\"application/json\")\n$headers.Add(\"Content-Type\",\"application/json\")\n\n$reqheaders = @{\"X-Octopus-ApiKey\" = $octopusAPIKey }\n$putReqHeaders = @{\"X-HTTP-Method-Override\" = \"PUT\"; \"X-Octopus-ApiKey\" = $octopusAPIKey }\n\nfunction Test-SpacesApi {\n\tWrite-Verbose \"Checking API compatibility\";\n\t$rootDocument = Invoke-WebRequest \"$baseUri/api\" -Headers $reqheaders -UseBasicParsing | ConvertFrom-Json;\n if($rootDocument.Links -ne $null -and $rootDocument.Links.Spaces -ne $null) {\n \tWrite-Verbose \"Spaces API found\"\n \treturn $true;\n }\n Write-Verbose \"Pre-spaces API found\"\n return $false;\n}\n\nif(Test-SpacesApi) {\n\t$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 recieved 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\t$baseApiUrl = \"/api/$spaceId\" ;\n} else {\n\t$baseApiUrl = \"/api\" ;\n}\n\n# Get the current release\n$releaseUri = \"$baseUri$baseApiUrl/projects/$octopusProjectId/releases/$thisReleaseNumber\"\nwrite-host \"Release uri $($releaseUri)\"\ntry {\n $currentRelease = Invoke-RestMethod $releaseUri -Headers $reqheaders -UseBasicParsing \n} catch {\n if ($_.Exception.Response.StatusCode.Value__ -ne 404) {\n $result = $_.Exception.Response.GetResponseStream()\n $reader = New-Object System.Io.StreamReader($result);\n $responseBody = $reader.ReadToEnd();\n throw \"Error occurred: $responseBody\"\n }\n}\n\nif(![string]::IsNullOrWhiteSpace($currentRelease.ReleaseNotes)){\n\twrite-host \"Release notes already filled in. $($currentRelease.ReleaseNotes)\"\n Set-OctopusVariable -name \"ReleaseNotes\" -value $releaseNotes\n\texit;\n}\n\n\n#Get projectid\n$url = \"http://$($instance)/tfs/$($collection)/$($projectId)/_apis/projects/$($project)?&includeCapabilities=false&includeHistory=false&api-version=2.2\"\nwrite-host \"Invoking url= $($url)\"\n$projectresponse = Invoke-RestMethod $url -Method GET -Headers $headers\n\n$projectid = $projectresponse.id\nwrite-host \"projectid $($projectid)\"\n\n#Get the ID of the query to execute\n$queryResult = Invoke-RestMethod \"http://$($instance)/tfs/$($collection)/$($projectId)/_apis/$($pathquery)?$depth=1&api-version=2.2\" -Method GET -Headers $headers\nwrite-host \"queryResult $($queryResult)\"\n\n#https://{instance}/DefaultCollection/[{project}/]_apis/wit/wiql/{id}?api-version={version}\n$queryResult = Invoke-RestMethod \"http://$($instance)/tfs/$($collection)/$($projectId)/_apis/wit/wiql/$($queryResult.Id)?api-version=2.2\" -Method GET -Headers $headers\n\nWrite-Host \"Found $($queryResult.workItems.Count) number of workitems for query: ReleaseNotes$($releaseTag)\"\n\n$releaseNotes = \"**Work Items:**\"\n\n\nif($queryResult.workItems.Count -eq 0)\n{\n\tWrite-Host \"No work items for release\"\n\t$releaseNotes = \"`n no new work items\"\n}\nelse\n{\n\t#Create a list of ids\n\t$ids = [string]::Join(\"%2C\", ($queryResult.workItems.id))\n\n\t#Get all the work items\n\t$workItems = Invoke-RestMethod \"http://$($instance)/tfs/$($collection)/_apis/wit/workItems?ids=$($ids)&fields=System.Title\" -Method GET -Headers $headers\n\n\tforeach($workItem in $workItems.value)\n\t{\n\t\t#Add line for each work item\n\t\t$releaseNotes = $releaseNotes + \"`n* [$($workItem.id)] (http://$($instance)/tfs/$($collection)/9981e67f-b27c-4628-b5cf-fba1d327aa07/_workitems/edit/$($workItem.id)) : $($workItem.fields.'System.Title')\"\n\t}\n\n}\n\n\n\n# Update the release notes for the current release\n$currentRelease.ReleaseNotes = $releaseNotes \nwrite-host \"Release notes $($currentRelease.ReleaseNotes)\"\nWrite-Host \"Updating release notes for $thisReleaseNumber`:`n`n\"\ntry {\n $releaseUri = \"$baseUri$baseApiUrl/releases/$($currentRelease.Id)\"\n write-host \"Release uri $($releaseUri)\"\n $currentReleaseBody = $currentRelease | ConvertTo-Json\n write-host \"Current release body $($currentReleaseBody)\"\n $result = Invoke-RestMethod $releaseUri -Method Post -Headers $putReqHeaders -Body $currentReleaseBody -UseBasicParsing\n\twrite-host \"result $($result)\"\n} catch {\n $result = $_.Exception.Response.GetResponseStream()\n $reader = New-Object System.Io.StreamReader($result);\n $responseBody = $reader.ReadToEnd();\n Write-Host \"error $($responseBody)\"\n throw \"Error occurred: $responseBody\"\n}\n\nSet-OctopusVariable -name \"ReleaseNotes\" -value $releaseNotes\n" + "Octopus.Action.Script.ScriptBody": "#TFS\n$instance = $OctopusParameters[\"tfsInstance\"]\n$collection = $OctopusParameters[\"tfsCollection\"]\n$project = $OctopusParameters[\"tfsProject\"]\n$PAT = $OctopusParameters[\"tfsPat\"]\n$pathquery = $OctopusParameters[\"tfsPathQuery\"]\n\n#Octopus\n$octopusAPIKey = $OctopusParameters['octopusAPIKey']\n$baseUri = $OctopusParameters['Octopus.Web.BaseUrl']\n$octopusProjectId = $OctopusParameters['Octopus.Project.Id']\n$thisReleaseNumber = $OctopusParameters['Octopus.Release.Number']\n\nwrite-host \"Instance: $($instance)\"\nwrite-host \"collection: $($collection)\"\nwrite-host \"project: $($project)\"\nwrite-host \"baseUri: $($baseUri)\"\nwrite-host \"projectId: $($projectId)\"\nwrite-host \"thisReleaseNumber: $($thisReleaseNumber)\"\nwrite-host \"TFS path: $($pathquery)\"\n\n#Create HEADERS\n$bytes = [System.Text.Encoding]::ASCII.GetBytes($PAT)\n$base64 = [System.Convert]::ToBase64String($bytes)\n$basicAuthValue = \"Basic $base64\"\n$headers = @{ }\n$headers.Add(\"Authorization\", $basicAuthValue)\n$headers.Add(\"Accept\",\"application/json\")\n$headers.Add(\"Content-Type\",\"application/json\")\n\n$reqheaders = @{\"X-Octopus-ApiKey\" = $octopusAPIKey }\n$putReqHeaders = @{\"X-HTTP-Method-Override\" = \"PUT\"; \"X-Octopus-ApiKey\" = $octopusAPIKey }\n\nfunction Test-SpacesApi {\n\tWrite-Verbose \"Checking API compatibility\";\n\t$rootDocument = Invoke-WebRequest \"$baseUri/api\" -Headers $reqheaders -UseBasicParsing | ConvertFrom-Json;\n if($rootDocument.Links -ne $null -and $rootDocument.Links.Spaces -ne $null) {\n \tWrite-Verbose \"Spaces API found\"\n \treturn $true;\n }\n Write-Verbose \"Pre-spaces API found\"\n return $false;\n}\n\nif(Test-SpacesApi) {\n\t$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\t$baseApiUrl = \"/api/$spaceId\" ;\n} else {\n\t$baseApiUrl = \"/api\" ;\n}\n\n# Get the current release\n$releaseUri = \"$baseUri$baseApiUrl/projects/$octopusProjectId/releases/$thisReleaseNumber\"\nwrite-host \"Release uri $($releaseUri)\"\ntry {\n $currentRelease = Invoke-RestMethod $releaseUri -Headers $reqheaders -UseBasicParsing \n} catch {\n if ($_.Exception.Response.StatusCode.Value__ -ne 404) {\n $result = $_.Exception.Response.GetResponseStream()\n $reader = New-Object System.Io.StreamReader($result);\n $responseBody = $reader.ReadToEnd();\n throw \"Error occurred: $responseBody\"\n }\n}\n\nif(![string]::IsNullOrWhiteSpace($currentRelease.ReleaseNotes)){\n\twrite-host \"Release notes already filled in. $($currentRelease.ReleaseNotes)\"\n Set-OctopusVariable -name \"ReleaseNotes\" -value $releaseNotes\n\texit;\n}\n\n\n#Get projectid\n$url = \"http://$($instance)/tfs/$($collection)/$($projectId)/_apis/projects/$($project)?&includeCapabilities=false&includeHistory=false&api-version=2.2\"\nwrite-host \"Invoking url= $($url)\"\n$projectresponse = Invoke-RestMethod $url -Method GET -Headers $headers\n\n$projectid = $projectresponse.id\nwrite-host \"projectid $($projectid)\"\n\n#Get the ID of the query to execute\n$queryResult = Invoke-RestMethod \"http://$($instance)/tfs/$($collection)/$($projectId)/_apis/$($pathquery)?$depth=1&api-version=2.2\" -Method GET -Headers $headers\nwrite-host \"queryResult $($queryResult)\"\n\n#https://{instance}/DefaultCollection/[{project}/]_apis/wit/wiql/{id}?api-version={version}\n$queryResult = Invoke-RestMethod \"http://$($instance)/tfs/$($collection)/$($projectId)/_apis/wit/wiql/$($queryResult.Id)?api-version=2.2\" -Method GET -Headers $headers\n\nWrite-Host \"Found $($queryResult.workItems.Count) number of workitems for query: ReleaseNotes$($releaseTag)\"\n\n$releaseNotes = \"**Work Items:**\"\n\n\nif($queryResult.workItems.Count -eq 0)\n{\n\tWrite-Host \"No work items for release\"\n\t$releaseNotes = \"`n no new work items\"\n}\nelse\n{\n\t#Create a list of ids\n\t$ids = [string]::Join(\"%2C\", ($queryResult.workItems.id))\n\n\t#Get all the work items\n\t$workItems = Invoke-RestMethod \"http://$($instance)/tfs/$($collection)/_apis/wit/workItems?ids=$($ids)&fields=System.Title\" -Method GET -Headers $headers\n\n\tforeach($workItem in $workItems.value)\n\t{\n\t\t#Add line for each work item\n\t\t$releaseNotes = $releaseNotes + \"`n* [$($workItem.id)] (http://$($instance)/tfs/$($collection)/9981e67f-b27c-4628-b5cf-fba1d327aa07/_workitems/edit/$($workItem.id)) : $($workItem.fields.'System.Title')\"\n\t}\n\n}\n\n\n\n# Update the release notes for the current release\n$currentRelease.ReleaseNotes = $releaseNotes \nwrite-host \"Release notes $($currentRelease.ReleaseNotes)\"\nWrite-Host \"Updating release notes for $thisReleaseNumber`:`n`n\"\ntry {\n $releaseUri = \"$baseUri$baseApiUrl/releases/$($currentRelease.Id)\"\n write-host \"Release uri $($releaseUri)\"\n $currentReleaseBody = $currentRelease | ConvertTo-Json\n write-host \"Current release body $($currentReleaseBody)\"\n $result = Invoke-RestMethod $releaseUri -Method Post -Headers $putReqHeaders -Body $currentReleaseBody -UseBasicParsing\n\twrite-host \"result $($result)\"\n} catch {\n $result = $_.Exception.Response.GetResponseStream()\n $reader = New-Object System.Io.StreamReader($result);\n $responseBody = $reader.ReadToEnd();\n Write-Host \"error $($responseBody)\"\n throw \"Error occurred: $responseBody\"\n}\n\nSet-OctopusVariable -name \"ReleaseNotes\" -value $releaseNotes\n" }, "Parameters": [ { diff --git a/step-templates/octopus-wait-for-deployment-target-registration.json b/step-templates/octopus-wait-for-deployment-target-registration.json index 4f966b9c4..7cd2750a8 100644 --- a/step-templates/octopus-wait-for-deployment-target-registration.json +++ b/step-templates/octopus-wait-for-deployment-target-registration.json @@ -8,7 +8,7 @@ "Properties": { "Octopus.Action.Script.Syntax": "PowerShell", "Octopus.Action.Script.ScriptSource": "Inline", - "Octopus.Action.Script.ScriptBody": "# Running outside octopus\nparam(\n [string]$odEnv,\n [string]$odName,\n [string]$odRole,\n [int]$odTimeout,\n [string]$odUrl,\n [string]$odApiKey,\n [switch]$whatIf\n) \n\n$ErrorActionPreference = \"Stop\" \n\nfunction Get-Param($Name, [switch]$Required, $Default) {\n $result = $null\n\n if ($OctopusParameters -ne $null) {\n $result = $OctopusParameters[$Name]\n }\n\n if ($result -eq $null) {\n $variable = Get-Variable $Name -EA SilentlyContinue \n if ($variable -ne $null) {\n $result = $variable.Value\n }\n }\n\n if (!$result -or $result -eq $null) {\n if ($Default) {\n $result = $Default\n } elseif ($Required) {\n throw \"Missing parameter value $Name\"\n }\n }\n\n return $result\n}\n\n& {\n param(\n [string]$odEnv,\n [string]$odName,\n [string]$odRole,\n [int]$odTimeout,\n [string]$odUrl,\n [string]$odApiKey\n )\n\n # If Octopus Deploy's URL/API Key are not provided as params, attempt to retrieve them from Environment Variables\n if (!$odUrl) {\n if ([Environment]::GetEnvironmentVariable(\"OD_API_URL\", \"Machine\")) {\n $odUrl = [Environment]::GetEnvironmentVariable(\"OD_API_URL\", \"Machine\")\n }\n }\n \n if (!$odUrl) { throw \"Octopus Deploy API URL was not available/provided.\" }\n\n if (!$odApiKey) {\n if ([Environment]::GetEnvironmentVariable(\"OD_API_KEY\", \"Machine\")) {\n $odApiKey = [Environment]::GetEnvironmentVariable(\"OD_API_KEY\", \"Machine\")\n }\n } \n \n if (!$odApiKey) { throw \"Octopus Deploy API key was not available/provided.\" }\n\n $header = @{ \"X-Octopus-ApiKey\" = $odApiKey }\n \n Write-Verbose \"Checking API compatibility\";\n $rootDocument = Invoke-WebRequest \"$odUrl/api/\" -Header $header -UseBasicParsing | ConvertFrom-Json;\n if($rootDocument.Links -ne $null -and $rootDocument.Links.Spaces -ne $null) {\n Write-Verbose \"Spaces API found\"\n $hasSpacesApi = $true;\n } else {\n Write-Verbose \"Pre-spaces API found\"\n $hasSpacesApi = $false;\n }\n \n if($hasSpacesApi) {\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 recieved 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 $baseApiUrl = \"/api/$spaceId\" ;\n } else {\n $baseApiUrl = \"/api\" ;\n } \n \n $environments = (Invoke-WebRequest \"$odUrl$baseApiUrl/environments/all\" -Headers $header -UseBasicParsing).content | ConvertFrom-Json\n $environment = $environments | Where-Object { $_.Name -contains $odEnv }\n if (@($environment).Count -eq 0) { throw \"Could not find environment with the name '$odEnv'\" }\n \n $timeout = new-timespan -Seconds $odTimeout\n $sw = [diagnostics.stopwatch]::StartNew()\n\n Write-Output (\"------------------------------\")\n Write-Output (\"Checking the Deployment Target's registration status:\")\n Write-Output (\"------------------------------\")\n\n while ($true)\n {\n if ($sw.elapsed -gt $timeout) { throw \"Timed out waiting for the Deployment Target to register\" }\n \n $machines = ((Invoke-WebRequest ($odUrl + $environment.Links.Self + \"/machines\") -Headers $header -UseBasicParsing).content | ConvertFrom-Json).items\n if ($odName) { $machines = $machines | Where-Object { $_.Name -like \"*$odName*\" } }\n if ($odRole) { $machines = $machines | Where-Object { $_.Roles -like \"*$odRole*\" } }\n if (@($machines).Count -gt 0) { break }\n\n Write-Output (\"$(Get-Date) | Waiting for Deployment Target to register with the name '$odName' and role '$odRole'\")\n\n Sleep -Seconds 5\n }\n \n Write-Output (\"$(Get-Date) | Deployment Target registered with the name '$odName' and role '$odRole'!\")\n } `\n (Get-Param 'odEnv' -Required) `\n (Get-Param 'odName' -Required) `\n (Get-Param 'odRole') `\n (Get-Param 'odTimeout' -Required) `\n (Get-Param 'odUrl') `\n (Get-Param 'odApiKey')" + "Octopus.Action.Script.ScriptBody": "# Running outside octopus\nparam(\n [string]$odEnv,\n [string]$odName,\n [string]$odRole,\n [int]$odTimeout,\n [string]$odUrl,\n [string]$odApiKey,\n [switch]$whatIf\n) \n\n$ErrorActionPreference = \"Stop\" \n\nfunction Get-Param($Name, [switch]$Required, $Default) {\n $result = $null\n\n if ($OctopusParameters -ne $null) {\n $result = $OctopusParameters[$Name]\n }\n\n if ($result -eq $null) {\n $variable = Get-Variable $Name -EA SilentlyContinue \n if ($variable -ne $null) {\n $result = $variable.Value\n }\n }\n\n if (!$result -or $result -eq $null) {\n if ($Default) {\n $result = $Default\n } elseif ($Required) {\n throw \"Missing parameter value $Name\"\n }\n }\n\n return $result\n}\n\n& {\n param(\n [string]$odEnv,\n [string]$odName,\n [string]$odRole,\n [int]$odTimeout,\n [string]$odUrl,\n [string]$odApiKey\n )\n\n # If Octopus Deploy's URL/API Key are not provided as params, attempt to retrieve them from Environment Variables\n if (!$odUrl) {\n if ([Environment]::GetEnvironmentVariable(\"OD_API_URL\", \"Machine\")) {\n $odUrl = [Environment]::GetEnvironmentVariable(\"OD_API_URL\", \"Machine\")\n }\n }\n \n if (!$odUrl) { throw \"Octopus Deploy API URL was not available/provided.\" }\n\n if (!$odApiKey) {\n if ([Environment]::GetEnvironmentVariable(\"OD_API_KEY\", \"Machine\")) {\n $odApiKey = [Environment]::GetEnvironmentVariable(\"OD_API_KEY\", \"Machine\")\n }\n } \n \n if (!$odApiKey) { throw \"Octopus Deploy API key was not available/provided.\" }\n\n $header = @{ \"X-Octopus-ApiKey\" = $odApiKey }\n \n Write-Verbose \"Checking API compatibility\";\n $rootDocument = Invoke-WebRequest \"$odUrl/api/\" -Header $header -UseBasicParsing | ConvertFrom-Json;\n if($rootDocument.Links -ne $null -and $rootDocument.Links.Spaces -ne $null) {\n Write-Verbose \"Spaces API found\"\n $hasSpacesApi = $true;\n } else {\n Write-Verbose \"Pre-spaces API found\"\n $hasSpacesApi = $false;\n }\n \n if($hasSpacesApi) {\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 $baseApiUrl = \"/api/$spaceId\" ;\n } else {\n $baseApiUrl = \"/api\" ;\n } \n \n $environments = (Invoke-WebRequest \"$odUrl$baseApiUrl/environments/all\" -Headers $header -UseBasicParsing).content | ConvertFrom-Json\n $environment = $environments | Where-Object { $_.Name -contains $odEnv }\n if (@($environment).Count -eq 0) { throw \"Could not find environment with the name '$odEnv'\" }\n \n $timeout = new-timespan -Seconds $odTimeout\n $sw = [diagnostics.stopwatch]::StartNew()\n\n Write-Output (\"------------------------------\")\n Write-Output (\"Checking the Deployment Target's registration status:\")\n Write-Output (\"------------------------------\")\n\n while ($true)\n {\n if ($sw.elapsed -gt $timeout) { throw \"Timed out waiting for the Deployment Target to register\" }\n \n $machines = ((Invoke-WebRequest ($odUrl + $environment.Links.Self + \"/machines\") -Headers $header -UseBasicParsing).content | ConvertFrom-Json).items\n if ($odName) { $machines = $machines | Where-Object { $_.Name -like \"*$odName*\" } }\n if ($odRole) { $machines = $machines | Where-Object { $_.Roles -like \"*$odRole*\" } }\n if (@($machines).Count -gt 0) { break }\n\n Write-Output (\"$(Get-Date) | Waiting for Deployment Target to register with the name '$odName' and role '$odRole'\")\n\n Sleep -Seconds 5\n }\n \n Write-Output (\"$(Get-Date) | Deployment Target registered with the name '$odName' and role '$odRole'!\")\n } `\n (Get-Param 'odEnv' -Required) `\n (Get-Param 'odName' -Required) `\n (Get-Param 'odRole') `\n (Get-Param 'odTimeout' -Required) `\n (Get-Param 'odUrl') `\n (Get-Param 'odApiKey')" }, "Parameters": [ { diff --git a/step-templates/save-octopus-output-variable-with-scoping.json b/step-templates/save-octopus-output-variable-with-scoping.json index 287b9469d..ab5e027e4 100644 --- a/step-templates/save-octopus-output-variable-with-scoping.json +++ b/step-templates/save-octopus-output-variable-with-scoping.json @@ -10,7 +10,7 @@ "Octopus.Action.Script.Syntax": "PowerShell", "Octopus.Action.Script.ScriptSource": "Inline", "Octopus.Action.RunOnServer": "true", - "Octopus.Action.Script.ScriptBody": "$ErrorActionPreference = 'Stop'\n\n$StepTemplate_BaseUrl = $OctopusParameters['Octopus.Web.BaseUrl'].Trim('/')\nif ([string]::IsNullOrWhiteSpace($StepTemplate_ApiKey)) {\n throw \"The step parameter 'API Key' was not found. This step requires an API Key to function, please provide one and try again.\"\n}\n\nfunction Invoke-OctopusApi {\n param(\n [Parameter(Position = 0, Mandatory)]$Uri,\n [ValidateSet(\"Get\", \"Put\")]$Method = 'Get',\n $Body\n )\n $requestParameters = @{\n Uri = ('{0}/{1}' -f $StepTemplate_BaseUrl, $Uri.TrimStart('/'))\n Method = $Method\n Headers = @{ \"X-Octopus-ApiKey\" = $StepTemplate_ApiKey }\n UseBasicParsing = $true\n }\n if ($null -ne $Body) { $requestParameters.Add('Body', ($Body | ConvertTo-Json -Depth 10)) }\n Write-Verbose \"$($Method.ToUpperInvariant()) $($requestParameters.Uri)\" \n Invoke-WebRequest @requestParameters | % Content | ConvertFrom-Json | Write-Output\n}\n\nfunction Test-SpacesApi {\n\tWrite-Verbose \"Checking API compatibility\";\n\t$rootDocument = Invoke-OctopusApi 'api/';\n if($rootDocument.Links -ne $null -and $rootDocument.Links.Spaces -ne $null) {\n \tWrite-Verbose \"Spaces API found\"\n \treturn $true;\n }\n Write-Verbose \"Pre-spaces API found\"\n return $false;\n}\n\nif(Test-SpacesApi) {\n\t$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 recieved 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\t$baseApiUrl = \"api/$spaceId\" ;\n} else {\n\t$baseApiUrl = \"api\" ;\n}\n\nfunction Check-Scope {\n\tparam(\n \t[Parameter(Position = 0, Mandatory)][string]$ScopeName,\n [Parameter(Position = 1, Mandatory)][array]$ScopeValues,\n [Parameter(Position = 2)][array]$ExistingScopeValue,\n [Parameter(Position = 3)][string]$LookingForScopeValue\n )\n \n if ($LookingForScopeValue) {\n \t\n \tWrite-Host \"Checking $ScopeName Scope\"\n \n $scopes = Create-Scope $ScopeName $ScopeValues $LookingForScopeValue\n \n if (-not ($ExistingScopeValue -and (Compare-Object $ExistingScopeValue $scopes) -eq $null)) {\n \tWrite-Host \"$ScopeName scope does not match\"\n \treturn $false\n }\n Write-Host \"$ScopeName scope matches\"\n } else {\n \tif ($ExistingScopeValue) {\n \tWrite-Host \"$ScopeName scope does not match\"\n \treturn $false\n }\n }\n \n return $true\n}\n\nfunction Create-Scope {\n\tparam(\n \t[Parameter(Position = 0, Mandatory)][string]$ScopeName,\n [Parameter(Position = 1, Mandatory)][array]$ScopeValues,\n [Parameter(Position = 2)][string]$ScopeValue\n )\n \n $scopes = @()\n \n foreach ($scope in $ScopeValue.Split($StepTemplate_ScopeDelimiter)) {\n \tif ($ScopeName -eq \"TenantTag\") {\n \t\t$value = $ScopeValues | Where { $_.Id -eq $scope } | Select -First 1\n \t}\n else {\n \t\t$value = $ScopeValues | Where { $_.Name -eq $scope } | Select -First 1\n \t}\n \t$scopes += $value.Id\n }\n \n return $scopes\n}\n\n$outputVariableKey = \"Octopus.Action[${StepTemplate_DeploymentStep}].Output.${StepTemplate_VariableName}\"\nif (!$OctopusParameters.ContainsKey($outputVariableKey)) {\n throw \"Variable '$StepTemplate_VariableName' has not been output from '$StepTemplate_DeploymentStep'\"\n}\n$isSensitive = [System.Convert]::ToBoolean($StepTemplate_IsSensitive)\n$variableType = if ($isSensitive) { \"Sensitive\" } else { \"String\" }\n\n$variableValue = $OctopusParameters[$outputVariableKey]\nWrite-Host \"Name: $StepTemplate_VariableName\"\nWrite-Host \"Type: $variableType\"\nWrite-Host \"Value: $(if ($isSensitive) { \"********\" } else { $variableValue })\"\nWrite-Host ' '\n\nWrite-Host \"Retrieving $StepTemplate_VariableSetType variable set...\"\nif ($StepTemplate_VariableSetType -eq 'project') {\n $variableSet = Invoke-OctopusApi \"$baseApiUrl/projects/all\" | ? Name -eq $StepTemplate_VariableSetName | % { Invoke-OctopusApi $_.Links.Variables }\n}\nif ($StepTemplate_VariableSetType -eq 'library') {\n $variableSet = Invoke-OctopusApi \"$baseApiUrl/libraryvariablesets/all?ContentType=Variables\" | ? Name -eq $StepTemplate_VariableSetName | % { Invoke-OctopusApi $_.Links.Variables }\n}\nif ($null -eq $variableSet) {\n throw \"Unable to find $StepTemplate_VariableSetType variable set '$StepTemplate_VariableSetName'\"\n}\n\n$variableExists = $false\n\n$variableSet.Variables | ? Name -eq $StepTemplate_TargetName | % {\n\tif (-not (Check-Scope 'Environment' $variableSet.ScopeValues.Environments $_.Scope.Environment $StepTemplate_EnvironmentScope)) {\n \treturn\n }\n\n\tif (-not (Check-Scope 'Machine' $variableSet.ScopeValues.Machines $_.Scope.Machine $StepTemplate_MachineScope)) {\n \treturn\n }\n \n if (-not (Check-Scope 'Role' $variableSet.ScopeValues.Roles $_.Scope.Role $StepTemplate_RoleScope)) {\n \treturn\n }\n \n if (-not (Check-Scope 'Action' $variableSet.ScopeValues.Actions $_.Scope.Action $StepTemplate_ActionScope)) {\n \treturn\n }\n \n if (-not (Check-Scope 'Channel' $variableSet.ScopeValues.Channels $_.Scope.Channel $StepTemplate_ChannelScope)) {\n \treturn\n }\n \n if (-not (Check-Scope 'TenantTag' $variableSet.ScopeValues.TenantTags $_.Scope.TenantTag $StepTemplate_TenantTagScope)) {\n \treturn\n }\n\n Write-Host \"Updating existing variable...\"\n Write-Host \"Existing value:\"\n\tWrite-Host \"$(if ($isSensitive) { \"********\" } else { $_.Value })\"\n $_.Value = $variableValue\n $_.Type = $variableType\n $_.IsSensitive = $isSensitive\n $variableExists = $true\n}\n\nif (!$variableExists) {\n Write-Host \"Creating new variable...\"\n \n $variable = @{\n Name = $StepTemplate_TargetName\n Value = $variableValue\n Type = $variableType\n IsSensitive = $isSensitive\n Scope = @{}\n }\n \n if ($StepTemplate_EnvironmentScope) {\n \t$variable.Scope['Environment'] = (Create-Scope 'Environment' $variableSet.ScopeValues.Environments $StepTemplate_EnvironmentScope)\n }\n if ($StepTemplate_RoleScope) {\n \t$variable.Scope['Role'] = (Create-Scope 'Role' $variableSet.ScopeValues.Roles $StepTemplate_RoleScope)\n }\n if ($StepTemplate_MachineScope) {\n \t$variable.Scope['Machine'] = (Create-Scope 'Machine' $variableSet.ScopeValues.Machines $StepTemplate_MachineScope)\n }\n if ($StepTemplate_ActionScope) {\n \t$variable.Scope['Action'] = (Create-Scope 'Action' $variableSet.ScopeValues.Actions $StepTemplate_ActionScope)\n }\n if ($StepTemplate_ChannelScope) {\n \t$variable.Scope['Channel'] = (Create-Scope 'Channel' $variableSet.ScopeValues.Channels $StepTemplate_ChannelScope)\n }\n if ($StepTemplate_TenantTagScope) {\n $variable.Scope['TenantTag'] = (Create-Scope 'TenantTag' $variableSet.ScopeValues.TenantTags $StepTemplate_TenantTagScope)\n }\n \n $variableSet.Variables += $variable\n}\n\nWrite-Host \"Saving updated variable set...\"\nInvoke-OctopusApi $variableSet.Links.Self -Method Put -Body $variableSet | Out-Null\n" + "Octopus.Action.Script.ScriptBody": "$ErrorActionPreference = 'Stop'\n\n$StepTemplate_BaseUrl = $OctopusParameters['Octopus.Web.BaseUrl'].Trim('/')\nif ([string]::IsNullOrWhiteSpace($StepTemplate_ApiKey)) {\n throw \"The step parameter 'API Key' was not found. This step requires an API Key to function, please provide one and try again.\"\n}\n\nfunction Invoke-OctopusApi {\n param(\n [Parameter(Position = 0, Mandatory)]$Uri,\n [ValidateSet(\"Get\", \"Put\")]$Method = 'Get',\n $Body\n )\n $requestParameters = @{\n Uri = ('{0}/{1}' -f $StepTemplate_BaseUrl, $Uri.TrimStart('/'))\n Method = $Method\n Headers = @{ \"X-Octopus-ApiKey\" = $StepTemplate_ApiKey }\n UseBasicParsing = $true\n }\n if ($null -ne $Body) { $requestParameters.Add('Body', ($Body | ConvertTo-Json -Depth 10)) }\n Write-Verbose \"$($Method.ToUpperInvariant()) $($requestParameters.Uri)\" \n Invoke-WebRequest @requestParameters | % Content | ConvertFrom-Json | Write-Output\n}\n\nfunction Test-SpacesApi {\n\tWrite-Verbose \"Checking API compatibility\";\n\t$rootDocument = Invoke-OctopusApi 'api/';\n if($rootDocument.Links -ne $null -and $rootDocument.Links.Spaces -ne $null) {\n \tWrite-Verbose \"Spaces API found\"\n \treturn $true;\n }\n Write-Verbose \"Pre-spaces API found\"\n return $false;\n}\n\nif(Test-SpacesApi) {\n\t$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\t$baseApiUrl = \"api/$spaceId\" ;\n} else {\n\t$baseApiUrl = \"api\" ;\n}\n\nfunction Check-Scope {\n\tparam(\n \t[Parameter(Position = 0, Mandatory)][string]$ScopeName,\n [Parameter(Position = 1, Mandatory)][array]$ScopeValues,\n [Parameter(Position = 2)][array]$ExistingScopeValue,\n [Parameter(Position = 3)][string]$LookingForScopeValue\n )\n \n if ($LookingForScopeValue) {\n \t\n \tWrite-Host \"Checking $ScopeName Scope\"\n \n $scopes = Create-Scope $ScopeName $ScopeValues $LookingForScopeValue\n \n if (-not ($ExistingScopeValue -and (Compare-Object $ExistingScopeValue $scopes) -eq $null)) {\n \tWrite-Host \"$ScopeName scope does not match\"\n \treturn $false\n }\n Write-Host \"$ScopeName scope matches\"\n } else {\n \tif ($ExistingScopeValue) {\n \tWrite-Host \"$ScopeName scope does not match\"\n \treturn $false\n }\n }\n \n return $true\n}\n\nfunction Create-Scope {\n\tparam(\n \t[Parameter(Position = 0, Mandatory)][string]$ScopeName,\n [Parameter(Position = 1, Mandatory)][array]$ScopeValues,\n [Parameter(Position = 2)][string]$ScopeValue\n )\n \n $scopes = @()\n \n foreach ($scope in $ScopeValue.Split($StepTemplate_ScopeDelimiter)) {\n \tif ($ScopeName -eq \"TenantTag\") {\n \t\t$value = $ScopeValues | Where { $_.Id -eq $scope } | Select -First 1\n \t}\n else {\n \t\t$value = $ScopeValues | Where { $_.Name -eq $scope } | Select -First 1\n \t}\n \t$scopes += $value.Id\n }\n \n return $scopes\n}\n\n$outputVariableKey = \"Octopus.Action[${StepTemplate_DeploymentStep}].Output.${StepTemplate_VariableName}\"\nif (!$OctopusParameters.ContainsKey($outputVariableKey)) {\n throw \"Variable '$StepTemplate_VariableName' has not been output from '$StepTemplate_DeploymentStep'\"\n}\n$isSensitive = [System.Convert]::ToBoolean($StepTemplate_IsSensitive)\n$variableType = if ($isSensitive) { \"Sensitive\" } else { \"String\" }\n\n$variableValue = $OctopusParameters[$outputVariableKey]\nWrite-Host \"Name: $StepTemplate_VariableName\"\nWrite-Host \"Type: $variableType\"\nWrite-Host \"Value: $(if ($isSensitive) { \"********\" } else { $variableValue })\"\nWrite-Host ' '\n\nWrite-Host \"Retrieving $StepTemplate_VariableSetType variable set...\"\nif ($StepTemplate_VariableSetType -eq 'project') {\n $variableSet = Invoke-OctopusApi \"$baseApiUrl/projects/all\" | ? Name -eq $StepTemplate_VariableSetName | % { Invoke-OctopusApi $_.Links.Variables }\n}\nif ($StepTemplate_VariableSetType -eq 'library') {\n $variableSet = Invoke-OctopusApi \"$baseApiUrl/libraryvariablesets/all?ContentType=Variables\" | ? Name -eq $StepTemplate_VariableSetName | % { Invoke-OctopusApi $_.Links.Variables }\n}\nif ($null -eq $variableSet) {\n throw \"Unable to find $StepTemplate_VariableSetType variable set '$StepTemplate_VariableSetName'\"\n}\n\n$variableExists = $false\n\n$variableSet.Variables | ? Name -eq $StepTemplate_TargetName | % {\n\tif (-not (Check-Scope 'Environment' $variableSet.ScopeValues.Environments $_.Scope.Environment $StepTemplate_EnvironmentScope)) {\n \treturn\n }\n\n\tif (-not (Check-Scope 'Machine' $variableSet.ScopeValues.Machines $_.Scope.Machine $StepTemplate_MachineScope)) {\n \treturn\n }\n \n if (-not (Check-Scope 'Role' $variableSet.ScopeValues.Roles $_.Scope.Role $StepTemplate_RoleScope)) {\n \treturn\n }\n \n if (-not (Check-Scope 'Action' $variableSet.ScopeValues.Actions $_.Scope.Action $StepTemplate_ActionScope)) {\n \treturn\n }\n \n if (-not (Check-Scope 'Channel' $variableSet.ScopeValues.Channels $_.Scope.Channel $StepTemplate_ChannelScope)) {\n \treturn\n }\n \n if (-not (Check-Scope 'TenantTag' $variableSet.ScopeValues.TenantTags $_.Scope.TenantTag $StepTemplate_TenantTagScope)) {\n \treturn\n }\n\n Write-Host \"Updating existing variable...\"\n Write-Host \"Existing value:\"\n\tWrite-Host \"$(if ($isSensitive) { \"********\" } else { $_.Value })\"\n $_.Value = $variableValue\n $_.Type = $variableType\n $_.IsSensitive = $isSensitive\n $variableExists = $true\n}\n\nif (!$variableExists) {\n Write-Host \"Creating new variable...\"\n \n $variable = @{\n Name = $StepTemplate_TargetName\n Value = $variableValue\n Type = $variableType\n IsSensitive = $isSensitive\n Scope = @{}\n }\n \n if ($StepTemplate_EnvironmentScope) {\n \t$variable.Scope['Environment'] = (Create-Scope 'Environment' $variableSet.ScopeValues.Environments $StepTemplate_EnvironmentScope)\n }\n if ($StepTemplate_RoleScope) {\n \t$variable.Scope['Role'] = (Create-Scope 'Role' $variableSet.ScopeValues.Roles $StepTemplate_RoleScope)\n }\n if ($StepTemplate_MachineScope) {\n \t$variable.Scope['Machine'] = (Create-Scope 'Machine' $variableSet.ScopeValues.Machines $StepTemplate_MachineScope)\n }\n if ($StepTemplate_ActionScope) {\n \t$variable.Scope['Action'] = (Create-Scope 'Action' $variableSet.ScopeValues.Actions $StepTemplate_ActionScope)\n }\n if ($StepTemplate_ChannelScope) {\n \t$variable.Scope['Channel'] = (Create-Scope 'Channel' $variableSet.ScopeValues.Channels $StepTemplate_ChannelScope)\n }\n if ($StepTemplate_TenantTagScope) {\n $variable.Scope['TenantTag'] = (Create-Scope 'TenantTag' $variableSet.ScopeValues.TenantTags $StepTemplate_TenantTagScope)\n }\n \n $variableSet.Variables += $variable\n}\n\nWrite-Host \"Saving updated variable set...\"\nInvoke-OctopusApi $variableSet.Links.Self -Method Put -Body $variableSet | Out-Null\n" }, "Parameters": [ { diff --git a/step-templates/save-octopus-output-variable.json b/step-templates/save-octopus-output-variable.json index d0eb31780..84b60c952 100644 --- a/step-templates/save-octopus-output-variable.json +++ b/step-templates/save-octopus-output-variable.json @@ -8,7 +8,7 @@ "Octopus.Action.Script.Syntax": "PowerShell", "Octopus.Action.Script.ScriptSource": "Inline", "Octopus.Action.RunOnServer": "true", - "Octopus.Action.Script.ScriptBody": "$ErrorActionPreference = 'Stop'\n\n$StepTemplate_BaseUrl = $OctopusParameters['Octopus.Web.BaseUrl'].Trim('/')\nif ([string]::IsNullOrWhiteSpace($StepTemplate_ApiKey)) {\n throw \"The step parameter 'API Key' was not found. This step requires an API Key to function, please provide one and try again.\"\n}\n\nfunction Invoke-OctopusApi {\n param(\n [Parameter(Position = 0, Mandatory)]$Uri,\n [ValidateSet(\"Get\", \"Put\")]$Method = 'Get',\n $Body\n )\n $requestParameters = @{\n Uri = ('{0}/{1}' -f $StepTemplate_BaseUrl, $Uri.TrimStart('/'))\n Method = $Method\n Headers = @{ \"X-Octopus-ApiKey\" = $StepTemplate_ApiKey }\n UseBasicParsing = $true\n }\n if ($null -ne $Body) { $requestParameters.Add('Body', ($Body | ConvertTo-Json -Depth 10)) }\n Write-Verbose \"$($Method.ToUpperInvariant()) $($requestParameters.Uri)\" \n Invoke-WebRequest @requestParameters | % Content | ConvertFrom-Json | Write-Output\n}\n\nfunction Test-SpacesApi {\n\tWrite-Verbose \"Checking API compatibility\";\n\t$rootDocument = Invoke-OctopusApi 'api/';\n if($rootDocument.Links -ne $null -and $rootDocument.Links.Spaces -ne $null) {\n \tWrite-Verbose \"Spaces API found\"\n \treturn $true;\n }\n Write-Verbose \"Pre-spaces API found\"\n return $false;\n}\n\nif(Test-SpacesApi) {\n\t$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 recieved 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\t$baseApiUrl = \"api/$spaceId\" ;\n} else {\n\t$baseApiUrl = \"api\" ;\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 [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$outputVariableKey = \"Octopus.Action[${StepTemplate_DeploymentStep}].Output.${StepTemplate_VariableName}\"\nif (!$OctopusParameters.ContainsKey($outputVariableKey)) {\n throw \"Variable '$StepTemplate_VariableName' has not been output from '$StepTemplate_DeploymentStep'\"\n}\n$isSensitive = [System.Convert]::ToBoolean($StepTemplate_IsSensitive)\n$variableType = if ($isSensitive) { \"Sensitive\" } else { \"String\" }\n\n$variableValue = $OctopusParameters[$outputVariableKey]\nWrite-Host \"Name: $StepTemplate_VariableName\"\nWrite-Host \"Type: $variableType\"\nWrite-Host \"Value: $(if ($isSensitive) { \"********\" } else { $variableValue })\"\nWrite-Host ' '\n\nWrite-Host \"Retrieving $StepTemplate_VariableSetType variable set...\"\nif ($StepTemplate_VariableSetType -eq 'project') {\n $variableSet = Invoke-OctopusApi \"$baseApiUrl/projects/all\" | ? Name -eq $StepTemplate_VariableSetName | % { Invoke-OctopusApi $_.Links.Variables }\n}\nif ($StepTemplate_VariableSetType -eq 'library') {\n $variableSet = Invoke-OctopusApi \"$baseApiUrl/libraryvariablesets/all?ContentType=Variables\" | ? Name -eq $StepTemplate_VariableSetName | % { Invoke-OctopusApi $_.Links.Variables }\n}\nif ($null -eq $variableSet) {\n throw \"Unable to find $StepTemplate_VariableSetType variable set '$StepTemplate_VariableSetName'\"\n}\n\n$variableExists = $false\n$variableSet.Variables | ? Name -eq $StepTemplate_VariableName | % {\n Write-Host \"Updating existing variable...\"\n Write-Verbose \"Existing value: $(if ($isSensitive) { \"********\" } else { $_.Value })\"\n $_.Value = $variableValue\n $_.Type = $variableType\n $_.IsSensitive = $isSensitive\n $_.Scope = Get-OctopusSetting Scope $_.Scope\n $variableExists = $true\n}\nif (!$variableExists) {\n Write-Host \"Creating new variable...\"\n $variableSet.Variables += @{\n Name = $StepTemplate_VariableName\n Value = $variableValue\n Type = $variableType\n IsSensitive = $isSensitive\n Scope = (Get-OctopusSetting Scope @{})\n }\n}\n\nWrite-Host \"Saving updated variable set...\"\nInvoke-OctopusApi $variableSet.Links.Self -Method Put -Body $variableSet | Out-Null", + "Octopus.Action.Script.ScriptBody": "$ErrorActionPreference = 'Stop'\n\n$StepTemplate_BaseUrl = $OctopusParameters['Octopus.Web.BaseUrl'].Trim('/')\nif ([string]::IsNullOrWhiteSpace($StepTemplate_ApiKey)) {\n throw \"The step parameter 'API Key' was not found. This step requires an API Key to function, please provide one and try again.\"\n}\n\nfunction Invoke-OctopusApi {\n param(\n [Parameter(Position = 0, Mandatory)]$Uri,\n [ValidateSet(\"Get\", \"Put\")]$Method = 'Get',\n $Body\n )\n $requestParameters = @{\n Uri = ('{0}/{1}' -f $StepTemplate_BaseUrl, $Uri.TrimStart('/'))\n Method = $Method\n Headers = @{ \"X-Octopus-ApiKey\" = $StepTemplate_ApiKey }\n UseBasicParsing = $true\n }\n if ($null -ne $Body) { $requestParameters.Add('Body', ($Body | ConvertTo-Json -Depth 10)) }\n Write-Verbose \"$($Method.ToUpperInvariant()) $($requestParameters.Uri)\" \n Invoke-WebRequest @requestParameters | % Content | ConvertFrom-Json | Write-Output\n}\n\nfunction Test-SpacesApi {\n\tWrite-Verbose \"Checking API compatibility\";\n\t$rootDocument = Invoke-OctopusApi 'api/';\n if($rootDocument.Links -ne $null -and $rootDocument.Links.Spaces -ne $null) {\n \tWrite-Verbose \"Spaces API found\"\n \treturn $true;\n }\n Write-Verbose \"Pre-spaces API found\"\n return $false;\n}\n\nif(Test-SpacesApi) {\n\t$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\t$baseApiUrl = \"api/$spaceId\" ;\n} else {\n\t$baseApiUrl = \"api\" ;\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 [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$outputVariableKey = \"Octopus.Action[${StepTemplate_DeploymentStep}].Output.${StepTemplate_VariableName}\"\nif (!$OctopusParameters.ContainsKey($outputVariableKey)) {\n throw \"Variable '$StepTemplate_VariableName' has not been output from '$StepTemplate_DeploymentStep'\"\n}\n$isSensitive = [System.Convert]::ToBoolean($StepTemplate_IsSensitive)\n$variableType = if ($isSensitive) { \"Sensitive\" } else { \"String\" }\n\n$variableValue = $OctopusParameters[$outputVariableKey]\nWrite-Host \"Name: $StepTemplate_VariableName\"\nWrite-Host \"Type: $variableType\"\nWrite-Host \"Value: $(if ($isSensitive) { \"********\" } else { $variableValue })\"\nWrite-Host ' '\n\nWrite-Host \"Retrieving $StepTemplate_VariableSetType variable set...\"\nif ($StepTemplate_VariableSetType -eq 'project') {\n $variableSet = Invoke-OctopusApi \"$baseApiUrl/projects/all\" | ? Name -eq $StepTemplate_VariableSetName | % { Invoke-OctopusApi $_.Links.Variables }\n}\nif ($StepTemplate_VariableSetType -eq 'library') {\n $variableSet = Invoke-OctopusApi \"$baseApiUrl/libraryvariablesets/all?ContentType=Variables\" | ? Name -eq $StepTemplate_VariableSetName | % { Invoke-OctopusApi $_.Links.Variables }\n}\nif ($null -eq $variableSet) {\n throw \"Unable to find $StepTemplate_VariableSetType variable set '$StepTemplate_VariableSetName'\"\n}\n\n$variableExists = $false\n$variableSet.Variables | ? Name -eq $StepTemplate_VariableName | % {\n Write-Host \"Updating existing variable...\"\n Write-Verbose \"Existing value: $(if ($isSensitive) { \"********\" } else { $_.Value })\"\n $_.Value = $variableValue\n $_.Type = $variableType\n $_.IsSensitive = $isSensitive\n $_.Scope = Get-OctopusSetting Scope $_.Scope\n $variableExists = $true\n}\nif (!$variableExists) {\n Write-Host \"Creating new variable...\"\n $variableSet.Variables += @{\n Name = $StepTemplate_VariableName\n Value = $variableValue\n Type = $variableType\n IsSensitive = $isSensitive\n Scope = (Get-OctopusSetting Scope @{})\n }\n}\n\nWrite-Host \"Saving updated variable set...\"\nInvoke-OctopusApi $variableSet.Links.Self -Method Put -Body $variableSet | Out-Null", "Octopus.Action.Script.ScriptFileName": null, "Octopus.Action.Package.FeedId": null, "Octopus.Action.Package.PackageId": null From d94d2ae88e54bbd5b1eb17d10f3415a4abab6ec6 Mon Sep 17 00:00:00 2001 From: Jim Burger Date: Fri, 18 Jan 2019 16:23:47 +1000 Subject: [PATCH 15/16] removed unused spaceid input --- step-templates/clone-tenant.json | 8 -------- 1 file changed, 8 deletions(-) diff --git a/step-templates/clone-tenant.json b/step-templates/clone-tenant.json index 2e0e34322..c29e05ade 100644 --- a/step-templates/clone-tenant.json +++ b/step-templates/clone-tenant.json @@ -50,14 +50,6 @@ "DisplaySettings": { "Octopus.ControlType": "Checkbox" } - }, - { - "DisplaySettings": { - "Octopus.ControlType": "SingleLineText" - }, - "Name": "CloneTenantStep_SpaceId", - "Label": "Space Id of Tenant to Clone", - "HelpText": "The Id of the Space that the Tenant to clone resides in. Leave blank if your version of Octopus server doesn't support spaces yet." } ], "$Meta": { From 63947b4a4aab8383792f1983d9a1a69d9105622a Mon Sep 17 00:00:00 2001 From: Jim Burger Date: Mon, 21 Jan 2019 12:31:21 +1000 Subject: [PATCH 16/16] bump for CI