-
Notifications
You must be signed in to change notification settings - Fork 508
Best Practices
This page is a collection of common snippets and practices to apply to Powershell step templates.
The properties defined against a step template have limited ability to define validation rules, so the scripts should validate these parameters before running any other logic.
$global:AzureAppConfigRetrievalMethod = $OctopusParameters["AzFunction.SetAppSettings.FromAzAppConfig.RetrievalMethod"]
if ([string]::IsNullOrWhiteSpace($global:AzureAppConfigRetrievalMethod)) {
throw "Required parameter AzFunction.SetAppSettings.FromAzAppConfig.RetrievalMethod not specified"
}
The script should validate that any required CLI tools are available:
function Test-ForAzCLI() {
$oldPreference = $ErrorActionPreference
$ErrorActionPreference = "Stop"
try {
return Get-Command "az"
}
catch {
return $false
}
finally {
$ErrorActionPreference = $oldPreference
}
}
Octopus prints all text stream to stderr as an error. This is arguably not the correct implementation, but isn't likely to be changed. This function allows a command to be run capturing all the output as a string:
Function Invoke-CustomCommand
{
Param (
$commandPath,
$commandArguments,
$workingDir = (Get-Location),
$path = @()
)
$path += $env:PATH
$newPath = $path -join [IO.Path]::PathSeparator
$pinfo = New-Object System.Diagnostics.ProcessStartInfo
$pinfo.FileName = $commandPath
$pinfo.WorkingDirectory = $workingDir
$pinfo.RedirectStandardError = $true
$pinfo.RedirectStandardOutput = $true
$pinfo.UseShellExecute = $false
$pinfo.Arguments = $commandArguments
$pinfo.EnvironmentVariables["PATH"] = $newPath
$p = New-Object System.Diagnostics.Process
$p.StartInfo = $pinfo
$p.Start() | Out-Null
# Capture output during process execution so we don't hang
# if there is too much output.
# Microsoft documents a C# solution here:
# https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.processstartinfo.redirectstandardoutput?view=net-7.0&redirectedfrom=MSDN#remarks
# This code is based on https://stackoverflow.com/a/74748844
$stdOut = [System.Text.StringBuilder]::new()
$stdErr = [System.Text.StringBuilder]::new()
do
{
if (!$p.StandardOutput.EndOfStream)
{
$stdOut.AppendLine($p.StandardOutput.ReadLine())
}
if (!$p.StandardError.EndOfStream)
{
$stdErr.AppendLine($p.StandardError.ReadLine())
}
Start-Sleep -Milliseconds 10
}
while (-not $p.HasExited)
# Capture any standard output generated between our last poll and process end.
while (!$p.StandardOutput.EndOfStream)
{
$stdOut.AppendLine($p.StandardOutput.ReadLine())
}
# Capture any error output generated between our last poll and process end.
while (!$p.StandardError.EndOfStream)
{
$stdErr.AppendLine($p.StandardError.ReadLine())
}
$p.WaitForExit()
$executionResults = [pscustomobject]@{
StdOut = $stdOut.ToString()
StdErr = $stdErr.ToString()
ExitCode = $p.ExitCode
}
return $executionResults
}
function Write-Results
{
[cmdletbinding()]
param (
[Parameter(Mandatory=$True,ValuefromPipeline=$True)]
$results
)
if (![String]::IsNullOrWhiteSpace($results.StdOut))
{
Write-Verbose $results.StdOut
}
}
Only later versions of PowerShell exposed variables $IsWindows
and $IsLinux
. This polyfill makes those variables available for all scripts:
if ($null -eq $IsWindows) {
Write-Host "Determining Operating System..."
$IsWindows = ([System.Environment]::OSVersion.Platform -eq "Win32NT")
$IsLinux = ([System.Environment]::OSVersion.Platform -eq "Unix")
}
Sometimes you want a trimmed string, or null if the string was null, because $null.Trim()
will throw an error:
function Format-StringAsNullOrTrimmed {
[cmdletbinding()]
param (
[Parameter(ValuefromPipeline=$True)]
$input
)
if ([string]::IsNullOrWhitespace($input)) {
return $null
}
return $input.Trim()
}
Most scripts treat $null
, empty strings, and whitespace strings as falsy. Unless you have a specific need to distinguish between these types of strings, prefer the use of the [string]::IsNullOrWhiteSpace()
check over checks like $myString -eq ""
or $myString -eq $null
.
When a step creates output variables, print them to the log. This makes it easy to copy and paste the correct syntax:
$StepName = $OctopusParameters["Octopus.Step.Name"]
Write-Host "Created output variable: ##{Octopus.Action[$StepName].Output.AppSettingsJson}"