diff --git a/step-templates/tests/upgrade-octopus-server.ScriptBody.Tests.ps1 b/step-templates/tests/upgrade-octopus-server.ScriptBody.Tests.ps1 new file mode 100644 index 000000000..ab8019688 --- /dev/null +++ b/step-templates/tests/upgrade-octopus-server.ScriptBody.Tests.ps1 @@ -0,0 +1,263 @@ +$ErrorActionPreference = "Stop"; +Set-StrictMode -Version "Latest"; + +$scriptPath = [System.IO.Path]::Combine($PSScriptRoot, "..", "upgrade-octopus-server.ScriptBody.ps1") +$scriptBody = [System.IO.File]::ReadAllText($scriptPath) +$scriptBodyWithoutEntryPoint = $scriptBody -replace '(?m)^StartInstallationOfOctopusServer\s*$', '' + +# These stubs make the revised internal seams mockable while the tests are red against master. +function NewOctopusTempFile { [System.IO.Path]::GetTempFileName() } +function InvokeOctopusDownloadRequest { throw "The ranged downloader is not implemented." } + +. ([scriptblock]::Create($scriptBodyWithoutEntryPoint)) + +function New-FakeDownloadResponse { + param( + [System.Net.HttpStatusCode] $StatusCode, + [long] $ContentLength, + [string] $ContentRange, + [byte[]] $Content = @() + ) + + $response = [pscustomobject]@{ + StatusCode = $StatusCode + ContentLength = $ContentLength + Headers = @{ "Content-Range" = $ContentRange } + Stream = New-Object System.IO.MemoryStream -ArgumentList @(,$Content) + } + Add-Member -InputObject $response -MemberType ScriptMethod -Name GetResponseStream -Value { $this.Stream } + Add-Member -InputObject $response -MemberType ScriptMethod -Name Dispose -Value { $this.Stream.Dispose() } + return $response +} + +function msiexec {} + +Describe "Upgrade Octopus Server ranged download" { + BeforeEach { + $script:DownloadedFile = $null + $script:Requests = @() + + Mock NewOctopusTempFile { + $script:DownloadedFile = [System.IO.Path]::GetTempFileName() + return $script:DownloadedFile + } + Mock New-Object { throw "The legacy WebClient downloader was invoked." } ` + -ParameterFilter { $TypeName -eq "System.Net.WebClient" } + Mock Get-AuthenticodeSignature { [pscustomobject]@{ Status = "Valid" } } + Mock Write-Host {} + Mock Start-Sleep {} + } + + AfterEach { + if ($script:DownloadedFile -and (Test-Path -LiteralPath $script:DownloadedFile)) { + Remove-Item -LiteralPath $script:DownloadedFile -Force + } + } + + It "downloads consecutive ranges and returns one validated path" { + Mock InvokeOctopusDownloadRequest { + $script:Requests += "$Method`:$RangeStart-$RangeEnd" + if ($Method -eq "HEAD") { + return New-FakeDownloadResponse -StatusCode OK -ContentLength 7 + } + + $length = [int]($RangeEnd - $RangeStart + 1) + return New-FakeDownloadResponse -StatusCode PartialContent ` + -ContentLength $length ` + -ContentRange "bytes $RangeStart-$RangeEnd/7" ` + -Content ([byte[]](1..$length)) + } + + $result = @(DownloadOctopusServer "2026.3.15954" 3 3) + + $result.Count | Should Be 1 + $result[0] | Should Be $script:DownloadedFile + (Get-Item -LiteralPath $result[0]).Length | Should Be 7 + $script:Requests -join "," | Should Be "HEAD:-,GET:0-2,GET:3-5,GET:6-6" + Assert-MockCalled Get-AuthenticodeSignature -Times 1 + Assert-MockCalled Write-Host -Times 1 -ParameterFilter { $Object -eq "Download completed" } + } + + It "retries a failed range from the last completed byte" { + $script:RangeAttempts = 0 + Mock InvokeOctopusDownloadRequest { + if ($Method -eq "HEAD") { + return New-FakeDownloadResponse -StatusCode OK -ContentLength 4 + } + + $script:RangeAttempts++ + $script:Requests += "$RangeStart-$RangeEnd" + if ($script:RangeAttempts -eq 1) { throw "connection reset" } + return New-FakeDownloadResponse -StatusCode PartialContent ` + -ContentLength 4 -ContentRange "bytes 0-3/4" -Content ([byte[]](1, 2, 3, 4)) + } + Mock Write-Warning {} + + $result = DownloadOctopusServer "2026.3.15954" 4 3 + + $result | Should Be $script:DownloadedFile + $script:Requests -join "," | Should Be "0-3,0-3" + Assert-MockCalled Write-Warning -Times 1 + } + + It "removes the partial file after exhausting the retry limit" { + Mock InvokeOctopusDownloadRequest { + if ($Method -eq "HEAD") { + return New-FakeDownloadResponse -StatusCode OK -ContentLength 4 + } + throw "connection reset" + } + + { DownloadOctopusServer "2026.3.15954" 4 3 } | Should Throw "connection reset" + Test-Path -LiteralPath $script:DownloadedFile | Should Be $false + Assert-MockCalled InvokeOctopusDownloadRequest -Times 3 -ParameterFilter { $Method -eq "GET" } + } + + It "rejects an invalid range response and removes the file" { + Mock InvokeOctopusDownloadRequest { + if ($Method -eq "HEAD") { + return New-FakeDownloadResponse -StatusCode OK -ContentLength 3 + } + return New-FakeDownloadResponse -StatusCode OK -ContentLength 3 ` + -ContentRange $null -Content ([byte[]](1, 2, 3)) + } + + { DownloadOctopusServer "2026.3.15954" 3 1 } | Should Throw "invalid response" + Test-Path -LiteralPath $script:DownloadedFile | Should Be $false + } + + It "rejects a range with an unexpected byte count" { + Mock InvokeOctopusDownloadRequest { + if ($Method -eq "HEAD") { + return New-FakeDownloadResponse -StatusCode OK -ContentLength 3 + } + return New-FakeDownloadResponse -StatusCode PartialContent -ContentLength 2 ` + -ContentRange "bytes 0-2/3" -Content ([byte[]](1, 2)) + } + + { DownloadOctopusServer "2026.3.15954" 3 1 } | Should Throw "unexpected file length" + Test-Path -LiteralPath $script:DownloadedFile | Should Be $false + } + + It "rejects an installer without a valid Authenticode signature" { + Mock InvokeOctopusDownloadRequest { + if ($Method -eq "HEAD") { + return New-FakeDownloadResponse -StatusCode OK -ContentLength 3 + } + return New-FakeDownloadResponse -StatusCode PartialContent -ContentLength 3 ` + -ContentRange "bytes 0-2/3" -Content ([byte[]](1, 2, 3)) + } + Mock Get-AuthenticodeSignature { [pscustomobject]@{ Status = "NotSigned" } } + + { DownloadOctopusServer "2026.3.15954" 3 1 } | Should Throw "Authenticode" + Test-Path -LiteralPath $script:DownloadedFile | Should Be $false + } + + It "does not stop or install when the download terminates" { + Mock DownloadOctopusServer { throw "download failed" } + Mock StopOctopusServer {} + Mock InstallOctopusServer {} + Mock StartOctopusServer {} + + { InstallSetVersionOnOctopusServer "2025.4.10250" "2026.3.15954" } | Should Throw "download failed" + + Assert-MockCalled StopOctopusServer -Times 0 + Assert-MockCalled InstallOctopusServer -Times 0 + Assert-MockCalled StartOctopusServer -Times 0 + } +} + +Describe "Upgrade Octopus Server installation safety" { + It "waits for msiexec with one quoted MSI path" { + Mock msiexec {} + Mock Start-Process { [pscustomobject]@{ ExitCode = 0 } } + + InstallOctopusServer "C:\Temp\Octopus Server.msi" + + Assert-MockCalled msiexec -Times 0 + Assert-MockCalled Start-Process -Times 1 -ParameterFilter { + $expectedArguments = @("/i", '"C:\Temp\Octopus Server.msi"', "/quiet", "/norestart") + $argumentDifference = Compare-Object $ArgumentList $expectedArguments -SyncWindow 0 + ($FilePath -eq "msiexec.exe") -and $Wait -and $PassThru -and ($null -eq $argumentDifference) + } + } + + It "terminates with the msiexec exit code when installation fails" { + Mock msiexec {} + Mock Start-Process { [pscustomobject]@{ ExitCode = 1603 } } + + { InstallOctopusServer "C:\Temp\Octopus.msi" } | Should Throw "1603" + } + + It "stops, installs, starts, and cleans up in order on success" { + $script:Operations = @() + Mock DownloadOctopusServer { "C:\Temp\Octopus.msi" } + Mock StopOctopusServer { $script:Operations += "stop" } + Mock InstallOctopusServer { $script:Operations += "install" } + Mock StartOctopusServer { $script:Operations += "start" } + Mock RemoveTempFile { $script:Operations += "cleanup" } + + InstallSetVersionOnOctopusServer "2025.4.10250" "2026.3.15954" + + $script:Operations -join "," | Should Be "stop,install,start,cleanup" + } + + It "starts the server and cleans up when installation fails" { + $script:Operations = @() + Mock DownloadOctopusServer { "C:\Temp\Octopus.msi" } + Mock StopOctopusServer { $script:Operations += "stop" } + Mock InstallOctopusServer { $script:Operations += "install"; throw "installation failed" } + Mock StartOctopusServer { $script:Operations += "start" } + Mock RemoveTempFile { $script:Operations += "cleanup" } + + { InstallSetVersionOnOctopusServer "2025.4.10250" "2026.3.15954" } | Should Throw "installation failed" + + $script:Operations -join "," | Should Be "stop,install,start,cleanup" + } + + It "cleans up without installing or starting when server stop fails" { + $script:Operations = @() + Mock DownloadOctopusServer { "C:\Temp\Octopus.msi" } + Mock StopOctopusServer { $script:Operations += "stop"; throw "stop failed" } + Mock InstallOctopusServer { $script:Operations += "install" } + Mock StartOctopusServer { $script:Operations += "start" } + Mock RemoveTempFile { $script:Operations += "cleanup" } + + { InstallSetVersionOnOctopusServer "2025.4.10250" "2026.3.15954" } | Should Throw "stop failed" + + $script:Operations -join "," | Should Be "stop,cleanup" + } + + It "preserves the installation failure and reports a recovery failure" { + Mock DownloadOctopusServer { "C:\Temp\Octopus.msi" } + Mock StopOctopusServer {} + Mock InstallOctopusServer { throw "installation failed" } + Mock StartOctopusServer { throw "recovery failed" } + Mock RemoveTempFile {} + Mock Write-Warning {} + + { InstallSetVersionOnOctopusServer "2025.4.10250" "2026.3.15954" } | Should Throw "installation failed" + + Assert-MockCalled Write-Warning -Times 1 -ParameterFilter { $Message -like "*recovery failed*" } + Assert-MockCalled RemoveTempFile -Times 1 + } +} + +Describe "Upgrade Octopus Server template contract" { + BeforeAll { + $templatePath = [System.IO.Path]::Combine($PSScriptRoot, "..", "upgrade-octopus-server.json") + $template = Get-Content -LiteralPath $templatePath -Raw | ConvertFrom-Json + } + + It "preserves the template identity and parameters" { + $template.Id | Should Be "4b3a1f09-1827-41bb-88a4-894c6317922b" + @($template.Parameters.Name) -join "," | Should Be "InstanceName,InstallPath,SpecificOctopusServerVersionToInstall" + } + + It "publishes version 11 with current modification metadata" { + $template.Version | Should Be 11 + $template.'$Meta'.OctopusVersion | Should Be "2026.3.15954" + $template.LastModifiedBy | Should Be "bcullman" + $template.LastModifiedDate | Should Not BeNullOrEmpty + } +} diff --git a/step-templates/upgrade-octopus-server.json b/step-templates/upgrade-octopus-server.json index 728393e77..209a80f44 100644 --- a/step-templates/upgrade-octopus-server.json +++ b/step-templates/upgrade-octopus-server.json @@ -3,13 +3,13 @@ "Name": "Upgrade Octopus Server", "Description": "This step downloads the latest version of Octopus Server and upgrades an existing instance. Run this step on a tentacle that has privileges to install software and start/stop services on the target server.\n\n**Run this after a database backup step**\n\nTo Use:\n- Install a tentacle on the Octopus Server machine with privileges to install software and start/stop services\n- Add that tentacle to an environment and with a unique role\n- Setup a project for the upgrade process\n- Add a database backup step for your Octopus Server database\n- Add this step, selecting it to run on just the role previously configured\n- Create a release\n- Deploy that release whenever an upgrade is needed\n\nNB: The deployment will show as \"Timed Out\" when the server comes back online", "ActionType": "Octopus.Script", - "Version": 10, + "Version": 11, "CommunityActionTemplateId": null, "Properties": { "Octopus.Action.Script.Syntax": "PowerShell", "Octopus.Action.Script.ScriptSource": "Inline", "Octopus.Action.RunOnServer": "false", - "Octopus.Action.Script.ScriptBody": "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12\n\nFunction GetLatestVersionOrSpecificVersionOfOctopusServer() \n{\n if([string]::IsNullOrEmpty($SpecificOctopusServerVersionToInstall))\n {\n Write-Host \"No specific version has been selected, getting latest version from octopusdeploy.com\"\n $versions = Invoke-WebRequest https://octopusdeploy.com/download/upgrade/v3 -UseBasicParsing | ConvertFrom-Json\n $version = $versions[-1].Version\n return $version\n }\n else \n {\n \tWrite-Host \"Specific version has been selected\"\n return $SpecificOctopusServerVersionToInstall\n }\n}\n\nFunction GetCurrentlyInstalledVersionOfOctopusServer() \n{\n $InstalledVersion = (get-item \"$InstallPath\\Octopus.Server.exe\").VersionInfo.fileversion\n return $installedVersion\n}\n\nFunction DownloadOctopusServer([string] $versionNumber) \n{ \n Write-Host \"Downloading Octopus Server version $versionNumber\"\n $tempFile = [System.IO.Path]::GetTempFileName()\n \ttry\n {\n (New-Object System.Net.WebClient).DownloadFile(\"https://download.octopusdeploy.com/octopus/Octopus.$versionNumber-x64.msi\", $tempFile)\n }\n catch\n {\n Write-Host \"Exception occurred\"\n echo $_.Exception|format-list -force\n }\n Write-Host \"Download completed\"\n return $tempFile\n}\n\nFunction StopOctopusServer() \n{\n Write-Host \"Stopping Server\"\n . \"$InstallPath\\Octopus.Server.exe\" service --stop --console --instance $InstanceName\n}\n\nFunction InstallOctopusServer([object] $tempFile)\n{\n Write-Host \"Installing ...\"\n msiexec /i $tempFile /quiet | Out-Null\n}\n\nFunction RemoveTempFile([object] $temporaryFile)\n{ \n Write-Host \"Deleting downloaded installer\"\n Remove-Item $temporaryFile\n}\n\nFunction StartOctopusServer()\n{\n Write-Host \"Starting Octopus Server\"\n . \"$InstallPath\\Octopus.Server.exe\" service --start --console --instance $InstanceName\n}\n\nFunction InstallSetVersionOnOctopusServer([string] $currentlyInstalledVersion, [string] $selectedVersionToInstall){\n\n Write-Host \"Currently installed version: $currentlyInstalledVersion\"\n Write-Host \"Selected version to install: $selectedVersionToInstall\" \n\n $tempFile = DownloadOctopusServer $selectedVersionToInstall\n StopOctopusServer\n InstallOctopusServer $tempFile\n RemoveTempFile $tempFile\n StartOctopusServer\n}\n\nFunction StartInstallationOfOctopusServer () {\n\n ## Get the current state of Octopus Server\n $selectedVersionToInstall = GetLatestVersionOrSpecificVersionOfOctopusServer \n $currentlyInstalledVersion = GetCurrentlyInstalledVersionOfOctopusServer\n\n\n if([version]$selectedVersionToInstall -eq [version]$currentlyInstalledVersion)\n {\n Write-host \"Octopus Server has already been installed with version $currentlyInstalledVersion\" \n }\n else \n {\n InstallSetVersionOnOctopusServer $currentlyInstalledVersion $selectedVersionToInstall\n }\n}\n\nStartInstallationOfOctopusServer" + "Octopus.Action.Script.ScriptBody": "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12\n\nFunction GetLatestVersionOrSpecificVersionOfOctopusServer()\n{\n if ([string]::IsNullOrEmpty($SpecificOctopusServerVersionToInstall))\n {\n Write-Host \"No specific version has been selected, getting latest version from octopusdeploy.com\"\n $versions = Invoke-WebRequest https://octopusdeploy.com/download/upgrade/v3 -UseBasicParsing | ConvertFrom-Json\n return $versions[-1].Version\n }\n Write-Host \"Specific version has been selected\"\n return $SpecificOctopusServerVersionToInstall\n}\nFunction GetCurrentlyInstalledVersionOfOctopusServer()\n{\n return (Get-Item \"$InstallPath\\Octopus.Server.exe\").VersionInfo.FileVersion\n}\nFunction NewOctopusTempFile()\n{\n return [System.IO.Path]::GetTempFileName()\n}\nFunction InvokeOctopusDownloadRequest($url, $method, $rangeStart, $rangeEnd)\n{\n $request = [Net.HttpWebRequest]::Create($url)\n $request.Method = $method\n if ($method -eq \"GET\") { $request.AddRange([long]$rangeStart, [long]$rangeEnd) }\n return $request.GetResponse()\n}\n\nFunction DownloadOctopusServer([string] $versionNumber, [long] $chunkSize = 32MB, [int] $maximumAttempts = 3)\n{\n $url = \"https://download.octopusdeploy.com/octopus/Octopus.$versionNumber-x64.msi\"\n $tempFile = NewOctopusTempFile\n Write-Host \"Downloading Octopus Server version $versionNumber\"\n try\n {\n $response = InvokeOctopusDownloadRequest $url \"HEAD\" $null $null\n try { [long] $expectedLength = $response.ContentLength }\n finally { $response.Dispose() }\n if ($expectedLength -le 0) { throw \"The download did not report a valid content length.\" }\n\n while ((Get-Item -LiteralPath $tempFile).Length -lt $expectedLength)\n {\n [long] $rangeStart = (Get-Item -LiteralPath $tempFile).Length\n [long] $rangeEnd = [Math]::Min($rangeStart + $chunkSize - 1, $expectedLength - 1)\n for ($attempt = 1; $attempt -le $maximumAttempts; $attempt++)\n {\n $response = $null\n $output = $null\n try\n {\n $response = InvokeOctopusDownloadRequest $url \"GET\" $rangeStart $rangeEnd\n $expectedRange = \"bytes $rangeStart-$rangeEnd/$expectedLength\"\n if ($response.StatusCode -ne [Net.HttpStatusCode]::PartialContent -or\n $response.Headers[\"Content-Range\"] -ne $expectedRange)\n {\n throw \"The server returned an invalid response for range $rangeStart-$rangeEnd.\"\n }\n $output = [IO.File]::Open($tempFile, \"Append\", \"Write\", \"None\")\n $response.GetResponseStream().CopyTo($output)\n $output.Dispose()\n $output = $null\n if ((Get-Item -LiteralPath $tempFile).Length -ne ($rangeEnd + 1))\n {\n throw \"The downloaded range ended at an unexpected file length.\"\n }\n break\n }\n catch\n {\n if ($attempt -eq $maximumAttempts) { throw }\n Write-Warning \"Range download failed; retrying from the last completed byte.\"\n Start-Sleep -Seconds $attempt\n $rangeStart = (Get-Item -LiteralPath $tempFile).Length\n }\n finally\n {\n if ($output) { $output.Dispose() }\n if ($response) { $response.Dispose() }\n }\n }\n $downloadedLength = (Get-Item -LiteralPath $tempFile).Length\n $percentComplete = [Math]::Round(($downloadedLength * 100) / $expectedLength, 2)\n Write-Host \"Downloaded $downloadedLength of $expectedLength bytes ($percentComplete%).\"\n }\n if ((Get-Item -LiteralPath $tempFile).Length -ne $expectedLength)\n {\n throw \"The Octopus Server download has an unexpected file length.\"\n }\n if ((Get-AuthenticodeSignature -LiteralPath $tempFile).Status -ne \"Valid\")\n {\n throw \"The Octopus Server installer does not have a valid Authenticode signature.\"\n }\n }\n catch\n {\n Remove-Item -LiteralPath $tempFile -Force -ErrorAction SilentlyContinue\n throw\n }\n Write-Host \"Download completed\"\n return $tempFile\n}\n\nFunction StopOctopusServer()\n{\n Write-Host \"Stopping Server\"\n . \"$InstallPath\\Octopus.Server.exe\" service --stop --console --instance $InstanceName\n}\n\nFunction InstallOctopusServer([string] $tempFile)\n{\n Write-Host \"Installing ...\"\n $arguments = @(\"/i\", \"`\"$tempFile`\"\", \"/quiet\", \"/norestart\")\n $installer = Start-Process \"msiexec.exe\" -ArgumentList $arguments -Wait -PassThru\n if ($installer.ExitCode -ne 0)\n {\n throw \"Octopus Server installation failed with msiexec exit code $($installer.ExitCode).\"\n }\n}\n\nFunction RemoveTempFile([string] $temporaryFile)\n{\n Write-Host \"Deleting downloaded installer\"\n Remove-Item -LiteralPath $temporaryFile -Force\n}\n\nFunction StartOctopusServer()\n{\n Write-Host \"Starting Octopus Server\"\n . \"$InstallPath\\Octopus.Server.exe\" service --start --console --instance $InstanceName\n}\n\nFunction InstallSetVersionOnOctopusServer([string] $currentlyInstalledVersion, [string] $selectedVersionToInstall)\n{\n Write-Host \"Currently installed version: $currentlyInstalledVersion\"\n Write-Host \"Selected version to install: $selectedVersionToInstall\"\n $tempFile = DownloadOctopusServer $selectedVersionToInstall\n $serverStopped = $false\n $operationError = $null\n $recoveryError = $null\n try\n {\n StopOctopusServer\n $serverStopped = $true\n InstallOctopusServer $tempFile\n }\n catch { $operationError = $_ }\n finally\n {\n if ($serverStopped)\n {\n try { StartOctopusServer }\n catch\n {\n $recoveryError = $_\n Write-Warning \"Failed to restart Octopus Server: $($_.Exception.Message)\"\n }\n }\n RemoveTempFile $tempFile\n }\n if ($operationError) { throw $operationError }\n if ($recoveryError) { throw $recoveryError }\n}\n\nFunction StartInstallationOfOctopusServer()\n{\n $selectedVersionToInstall = GetLatestVersionOrSpecificVersionOfOctopusServer\n $currentlyInstalledVersion = GetCurrentlyInstalledVersionOfOctopusServer\n if ([version]$selectedVersionToInstall -eq [version]$currentlyInstalledVersion)\n {\n Write-Host \"Octopus Server has already been installed with version $currentlyInstalledVersion\"\n }\n else\n {\n InstallSetVersionOnOctopusServer $currentlyInstalledVersion $selectedVersionToInstall\n }\n}\n\nStartInstallationOfOctopusServer\n" }, "Parameters": [ { @@ -46,12 +46,13 @@ "Links": {} } ], - "LastModifiedBy": "Verzada", + "LastModifiedBy": "bcullman", + "LastModifiedDate": "2026-10-17T00:16:50.000Z", "Website": "/step-templates/4b3a1f09-1827-41bb-88a4-894c6317922b", "Logo": "iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAMAAACahl6sAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAC1QTFRFT6Tl////L5Pg8vj9Y67omsvwPJrisdfzfbzs5fL7y+T32Ov5isLucLXqvt31CJPHWwAABMJJREFUeNrs3deW4jAMAFDF3U75/89dlp0ZhiU4blJEjvQ8hYubLJsA00UCBCIQgQhEIAIRiEAEIhCBCEQgAhGIQAQiEIEIhD8kJm+t+QprfdKfB9HbYpx6CWfspj8HMi+gMgHL/AmQA8W3JTKH+ALFvzCeL0RbpyoCPE9IJeNOSQwh5Z3qd6yRGWQ2qi2cZQWxqj1WzQYSjeoJmJlAklOd4VlArOqPhQEkqBERToeMcfRJBkC0Uep8CfBpjz4JsHJ0zF3dkEWNje0kiB/sUC6eApndaIiCMyAa1PiwJ0AWhRGJHJJQHG2dC7h1rNbO1QOxSA7lNCkkKrQIpJCAB1GREILYIC1NAiwbpKFJgGWDNExcwGstfExcZBCHC6nOglshHtmhViLIig1RNBCN7qjtW8C0Z1UvJcC1Z9XmwMBzzvobmgAyEzgq91dtEEsBsQSQQAFZCSBAATEEEApHZbrVBIkkEIUPSVeB+KtALA0kXQUSrwKZBCIQBnk8Y4i5CsReBeKvkqLM+BCSDWJlrZFvGk9SRTHshkgjZCGAaArIxm3H3grhVzFlW2msfl1ca79UJ1bofYvsDHHlNdTZnlh5MghuPd5NdBDUNZHyCkfktIh03XzALGRPlBDPac7qgWjHZzWcmF5zmmkhidMQ6boKiDXcDTUEaylZqCGJ0Vjvu/fLJtHqhSANEvqb2OYqkOUqEHuVMbJcZdZCGiPhKhC4yjqiIjEE7XThMp8fAWII3mY3kUIQD+AMKQTzPiBhgQ63HlT/KSvgtoi0dq5mCPah1UIE0eh3sT0NhOByvKeAkFzi8PgQomumFhsyOxpIzZN4gLOj5plVwNpR0b2AuePWKBEHQu24pSsJA+LVCeHHQxZ1SiyDIdqok8IOhSSnTottHEQTdyt4ettAj4KkzA4dMikk2Dht2S5ptm1vswnPDxn0YyDZ5oDM3iToo2T5voWaYe+Q+vdjH80QyAzZhCgcDtLMI1Tmtz9w++XHgziHQHJJu/OZ3bs9Xn8gQ72NcP3dKqEfkp10F51xhoIi2I91R+LurXV/5q7pH+wx061CzO16oSQleMyr8fXvwMA0Pro8432DPD/ySx8XrHfSuDAM8n6UhnjQabaiXf5Bq/lREHvEeNtn1rJ08+C/uXkQZHeguxAPC3UvtcJYUogLzZX5hhZZvS6onG5lxXtzWGaygwb79vT/IXhdlNibwlKYOR6T8xjI7W8n+xV7T+GH4tMzWwR+lZhRkJYSsC0thpmCYqyngOz3rN2FLBZ2wZflBCggUHF0Vnp88JKienzIXLSEZCZqU7IKr/gQW9yx3pzV7Y9kvWZWTRRIqDmTtRUnU7b2lLcTYmoqHqnmiO1poER0SPkAeZMAZxaJx0Y3TCdAclsIqDz03ALcyxfTCZBsthoGXWmigGyVhWPLFJJfuuKQWycoEFdXbH4dJJoJxNR1eD/kshz6yn48cF8yW8sFoitflB1w6Q8n+/15Za7oA17/pYNmYgP5fmWm8L1NOHPWgK8kuFew1/JXtOA0yJCv7ah7X8ObUuT5kObU30+fDZm8+zqP+HTIpK0xQ796b5Kv2hSIQAQiEIEIRCACEYhABCIQgQhEIAIRiEAEIpBf8UeAAQAEjtYmlDTcCgAAAABJRU5ErkJggg==", "$Meta": { - "ExportedAt": "2018-03-09T15:07:55.181Z", - "OctopusVersion": "2018.3.2", + "ExportedAt": "2026-10-17T00:16:50.000Z", + "OctopusVersion": "2026.3.15954", "Type": "ActionTemplate" }, "Category": "octopus"