From 60b1a8cc9f773a1ce9a6be3c9f3dd42bd129d01e Mon Sep 17 00:00:00 2001 From: Emrys MacInally Date: Fri, 28 Nov 2025 11:13:44 +0100 Subject: [PATCH 01/14] Add support for retrieving tasks from DeploymentResource in Get-Task function --- OctopusDeploy/Public/Get-Task.ps1 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/OctopusDeploy/Public/Get-Task.ps1 b/OctopusDeploy/Public/Get-Task.ps1 index 835bacf..61a9bc1 100644 --- a/OctopusDeploy/Public/Get-Task.ps1 +++ b/OctopusDeploy/Public/Get-Task.ps1 @@ -163,6 +163,8 @@ $tasks = (Get-RunbookRun -RunbookSnapshot $r).links.task } elseif ($r -is [Octopus.Client.Model.ReleaseResource]) { $tasks = (Get-Deployment -Release $r).links.task + } elseif ($r -is [Octopus.Client.Model.DeploymentResource]) { + $tasks = @($r.TaskId) } foreach ($t in $tasks) { $repo._repository.Tasks.get((($t -split '/')[-1])) From 45a45338b6a69d54dc72efd4c37d46814df40241 Mon Sep 17 00:00:00 2001 From: Emrys MacInally Date: Fri, 28 Nov 2025 17:07:32 +0100 Subject: [PATCH 02/14] Update CHANGELOG and improve Get-CommonTenantVariable function with environment filtering and optional variable set retrieval --- CHANGELOG.md | 6 ++ GettingStarted.md | 6 ++ .../Public/Get-CommonTenantVariable.ps1 | 88 ++++++++++++++++--- OctopusDeploy/Public/Get-Task.ps1 | 16 ++-- 4 files changed, 98 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 57bc794..ad3bcb6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [Unreleased] + +### Improved +- **Get-Task**: Added support for retrieving tasks from `DeploymentResource` objects. +- **Get-CommonTenantVariable**: Added support for filtering by Environment, made VariableSet optional to retrieve all variable sets, and improved output to include scope and variable set name. + ## [2.0.0] ### Added - Configuration as Code Runbook Support 🎉 diff --git a/GettingStarted.md b/GettingStarted.md index 30079f5..c5b22bf 100644 --- a/GettingStarted.md +++ b/GettingStarted.md @@ -789,6 +789,12 @@ These are library variables with tenant-specific values: # Get common tenant variables Get-CommonTenantVariable -VariableSet "Customer Variables" -Tenant "MyTenant" +# Get common tenant variables for a specific environment +Get-CommonTenantVariable -Tenant "MyTenant" -Environment "Production" + +# Get all common tenant variables (across all variable sets) +Get-CommonTenantVariable -Tenant "MyTenant" + # Set a single variable Set-CommonTenantVariable -Tenant "MyTenant" -VariableSet "Customer Variables" -Name "DatabaseServer" -Value "sql.example.com" diff --git a/OctopusDeploy/Public/Get-CommonTenantVariable.ps1 b/OctopusDeploy/Public/Get-CommonTenantVariable.ps1 index 3bc03f9..d23a76c 100644 --- a/OctopusDeploy/Public/Get-CommonTenantVariable.ps1 +++ b/OctopusDeploy/Public/Get-CommonTenantVariable.ps1 @@ -3,20 +3,37 @@ .SYNOPSIS Returns a list of common tenant variables .DESCRIPTION - Returns a list of common tenant variables for a specific tenant and variable set + Returns a list of common tenant variables for a specific tenant and variable set. + This function retrieves variables from Library Variable Sets that are associated with the tenant. + It returns both default values (from the Library Variable Set templates) and tenant-specific overridden values. + .PARAMETER Tenant + The tenant to retrieve variables for. + .PARAMETER VariableSet + The Library Variable Set to filter by. If not provided, all variable sets associated with the tenant are processed. + .PARAMETER Environment + The environment to filter by. If provided, only variables scoped to this environment (or unscoped variables) are returned. .EXAMPLE Get-CommonTenantVariable -Tenant XXROM001 -VariableSet 'Customer Variables' - All variable from tenant "XXROM001" saved in "Customer Variables" + Returns all variables for tenant "XXROM001" from the "Customer Variables" set. + .EXAMPLE + Get-CommonTenantVariable -Tenant XXROM001 + Returns all common variables for tenant "XXROM001" from all associated variable sets. + .EXAMPLE + Get-CommonTenantVariable -Tenant XXROM001 -Environment Production + Returns all common variables for tenant "XXROM001" that are relevant to the Production environment. #> [CmdletBinding()] param ( [parameter(Mandatory = $true)] [TenantSingleTransformation()] [Octopus.Client.Model.TenantResource]$Tenant, - [parameter(Mandatory = $true, - ValueFromPipeline = $true)] + [parameter(Mandatory = $false, + ValueFromPipeline = $true)] [LibraryVariableSetSingleTransformation()] - [Octopus.Client.Model.LibraryVariableSetResource]$VariableSet + [Octopus.Client.Model.LibraryVariableSetResource]$VariableSet, + [parameter(Mandatory = $false)] + [EnvironmentSingleTransformation()] + [Octopus.Client.Model.EnvironmentResource]$Environment ) begin { @@ -29,15 +46,62 @@ } process { # variables types [System.Enum]::GetNames([Octopus.Client.Model.VariableSetContentType]) - $tenantVar = $repo._repository.Tenants.GetVariables($Tenant) - $libVars = $tenantVar.LibraryVariables."$($VariableSet.Id)" - $libVars.Templates | ForEach-Object { $setvar = $libVars.Variables."$($_.id)"; [pscustomobject]@{ - Name = $_.name - Value = if ($_.DefaultValue.IsSensitive) { "*****" }else { if ($setvar.value) { $setvar.value }else { $_.DefaultValue.value } } - IsDefaultValue = if ($setvar.value -or $setvar.SensitiveValue) { $false }else { $true } - } } + $tenantVar = $repo._repository.Tenants.GetVariables($Tenant) + $environments = Get-Environment + # Determine which Variable Sets to process + $VariableSets = @() + if ($PSBoundParameters['VariableSet']) { + $VariableSets = $VariableSet + } + else { + $VariableSets = Get-VariableSet | Where-Object { $_.id -in $tenantVar.LibraryVariables.Keys } + } + $results = @() + foreach ($vSet in $VariableSets) { + # get default variables from each set + $libVars = $tenantVar.LibraryVariables."$($vSet.Id)" + $results += $libVars.Templates | ForEach-Object { $setvar = $libVars.Variables."$($_.id)"; + # only output default values that are not overridden + if ((-not $setvar.Value) -and (-not $setvar.SensitiveValue)) { + [pscustomobject]@{ + VariableSetName = $vSet.Name + Name = $_.name + Value = if ($_.DefaultValue.IsSensitive) { "*****" }else { $_.DefaultValue.value } + IsDefaultValue = $true + Scope = $null + } + } + } + # get all non default variables + $c = [Octopus.Client.Model.TenantVariables.GetCommonVariablesByTenantIdRequest]::new($Tenant.id, $Tenant.SpaceId) + $tenantVars = $repo._repository.TenantVariables.get($c) + + + $vars = $tenantVars.Variables | Where-Object LibraryVariableSetId -EQ $vSet.Id + + $results += $vars | ForEach-Object { + [pscustomobject]@{ + VariableSetName = $vSet.Name + Name = $_.template.name + Value = if ($_.Value.IsSensitive) { '*****' }else { $_.value.value } + IsDefaultValue = $false + Scope = [String[]]($_.scope.EnvironmentIds | ForEach-Object { $environments | Where-Object id -Like $_ }).name + } + } + # if an environment was specified, return all scoped variables for that environment and unscoped variables a far the variable has no environment scope + if ($PSBoundParameters['Environment']) { + $results = $results | Where-Object { ($_.Scope -contains $Environment.Name) -or ($null -eq $_.Scope -and ($results | Where-Object Name -EQ $_.name).count -eq 1) } + } + + + + } + # order results and output + $results = $results | Sort-Object VariableSetName, Name + $results } + end {} } diff --git a/OctopusDeploy/Public/Get-Task.ps1 b/OctopusDeploy/Public/Get-Task.ps1 index 61a9bc1..e9afb0c 100644 --- a/OctopusDeploy/Public/Get-Task.ps1 +++ b/OctopusDeploy/Public/Get-Task.ps1 @@ -93,7 +93,7 @@ catch { $PSCmdlet.ThrowTerminatingError($_) } - } + } process { if ($PSCmdlet.ParameterSetName -eq 'ByID') { $repo._repository.Tasks.Get($TaskID) @@ -103,7 +103,8 @@ function checkpath ($path) { if ($path -eq '/api/tasks') { return '/api/tasks?' - } else { return ($path + "&") } + } + else { return ($path + "&") } } # always add space @@ -148,9 +149,10 @@ } return $results.Items - } catch { + } + catch { - Throw $_ + throw $_ } @@ -161,9 +163,11 @@ foreach ($r in $Regarding) { if ($r -is [Octopus.Client.Model.RunbookSnapshotResource]) { $tasks = (Get-RunbookRun -RunbookSnapshot $r).links.task - } elseif ($r -is [Octopus.Client.Model.ReleaseResource]) { + } + elseif ($r -is [Octopus.Client.Model.ReleaseResource]) { $tasks = (Get-Deployment -Release $r).links.task - } elseif ($r -is [Octopus.Client.Model.DeploymentResource]) { + } + elseif ($r -is [Octopus.Client.Model.DeploymentResource]) { $tasks = @($r.TaskId) } foreach ($t in $tasks) { From ee1001cc275e938ded561f413ea324cebb044330 Mon Sep 17 00:00:00 2001 From: Emrys MacInally Date: Fri, 28 Nov 2025 17:23:44 +0100 Subject: [PATCH 03/14] Fix type declaration for results array in Get-CommonTenantVariable function --- OctopusDeploy/Public/Get-CommonTenantVariable.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OctopusDeploy/Public/Get-CommonTenantVariable.ps1 b/OctopusDeploy/Public/Get-CommonTenantVariable.ps1 index d23a76c..9e35d49 100644 --- a/OctopusDeploy/Public/Get-CommonTenantVariable.ps1 +++ b/OctopusDeploy/Public/Get-CommonTenantVariable.ps1 @@ -91,7 +91,7 @@ } # if an environment was specified, return all scoped variables for that environment and unscoped variables a far the variable has no environment scope if ($PSBoundParameters['Environment']) { - $results = $results | Where-Object { ($_.Scope -contains $Environment.Name) -or ($null -eq $_.Scope -and ($results | Where-Object Name -EQ $_.name).count -eq 1) } + [System.Array]$results = $results | Where-Object { ($_.Scope -contains $Environment.Name) -or ($null -eq $_.Scope -and ($results | Where-Object Name -EQ $_.name).count -eq 1) } } From 8d7955375fc205005f5b48c61650b372fab76346 Mon Sep 17 00:00:00 2001 From: Emrys MacInally Date: Fri, 28 Nov 2025 17:26:39 +0100 Subject: [PATCH 04/14] +semver:minor --- OctopusDeploy/Public/Get-CommonTenantVariable.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OctopusDeploy/Public/Get-CommonTenantVariable.ps1 b/OctopusDeploy/Public/Get-CommonTenantVariable.ps1 index 9e35d49..d9916c9 100644 --- a/OctopusDeploy/Public/Get-CommonTenantVariable.ps1 +++ b/OctopusDeploy/Public/Get-CommonTenantVariable.ps1 @@ -105,4 +105,4 @@ end {} } -#Get-CommonTenantVariable -Tenant XXROM001 -VariableSet "customer variables" + From c057f8a4e5ac3197e134c7e9ea91fb3506c7c85b Mon Sep 17 00:00:00 2001 From: Emrys MacInally Date: Mon, 8 Dec 2025 11:50:22 +0100 Subject: [PATCH 05/14] Refactor Get-CommonTenantVariable and Set-CommonTenantVariable functions for improved variable handling and add new Get-VariableTemplate and New-TenantCommonVariablePayload functions --- .../Private/Get-VariableTemplate.ps1 | 25 +++ .../New-TenantCommonVariablePayload.ps1 | 76 ++++++++++ .../Public/Compare-EnvironmentScope.ps1 | 35 +++++ .../Public/Get-CommonTenantVariable.ps1 | 60 +------- .../Public/GetCommonTenantVariable.ps1 | 96 ++++++++++++ .../Public/Set-CommonTenantVariable.ps1 | 143 +++++++++++++++--- 6 files changed, 361 insertions(+), 74 deletions(-) create mode 100644 OctopusDeploy/Private/Get-VariableTemplate.ps1 create mode 100644 OctopusDeploy/Private/New-TenantCommonVariablePayload.ps1 create mode 100644 OctopusDeploy/Public/Compare-EnvironmentScope.ps1 create mode 100644 OctopusDeploy/Public/GetCommonTenantVariable.ps1 diff --git a/OctopusDeploy/Private/Get-VariableTemplate.ps1 b/OctopusDeploy/Private/Get-VariableTemplate.ps1 new file mode 100644 index 0000000..ef3c51c --- /dev/null +++ b/OctopusDeploy/Private/Get-VariableTemplate.ps1 @@ -0,0 +1,25 @@ +function Get-VariableTemplate { + <# + .SYNOPSIS + Gets a variable template from a variable set by name. + .DESCRIPTION + This function retrieves a specific variable template from a LibraryVariableSetResource based on the provided name. + .PARAMETER VariableSet + The LibraryVariableSetResource to search in. + .PARAMETER Name + The name of the template to find. + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [Octopus.Client.Model.LibraryVariableSetResource]$VariableSet, + + [Parameter(Mandatory = $true)] + [string]$Name + ) + + process { + $template = $VariableSet.Templates | Where-Object Name -EQ $Name + return $template + } +} diff --git a/OctopusDeploy/Private/New-TenantCommonVariablePayload.ps1 b/OctopusDeploy/Private/New-TenantCommonVariablePayload.ps1 new file mode 100644 index 0000000..e8056ba --- /dev/null +++ b/OctopusDeploy/Private/New-TenantCommonVariablePayload.ps1 @@ -0,0 +1,76 @@ +function New-TenantCommonVariablePayload { + <# + .SYNOPSIS + Creates a TenantCommonVariablePayload object. + .DESCRIPTION + Helper function to create the payload for updating tenant common variables. + .PARAMETER LibraryVariableSetId + The ID of the library variable set. + .PARAMETER TemplateId + The ID of the variable template. + .PARAMETER Value + The value of the variable. Can be a string or PropertyValueResource. + .PARAMETER Scope + The scope of the variable. Can be CommonVariableScope, ReferenceCollection, or array of environment IDs. + .PARAMETER VariableId + The ID of the existing variable, if updating. + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string]$LibraryVariableSetId, + + [Parameter(Mandatory = $true)] + [string]$TemplateId, + + [Parameter(Mandatory = $true)] + $Value, + + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [AllowNull()] + $Scope, + + [Parameter(Mandatory = $false)] + [string]$VariableId + ) + + process { + # Handle Value + if ($Value -isnot [Octopus.Client.Model.PropertyValueResource]) { + $Value = [Octopus.Client.Model.PropertyValueResource]::new($Value, $false) + } + + # Handle Scope + if ($Scope -isnot [Octopus.Client.Model.TenantVariables.CommonVariableScope]) { + if ($Scope -is [Octopus.Client.Model.ReferenceCollection]) { + $Scope = [Octopus.Client.Model.TenantVariables.CommonVariableScope]::new($Scope) + } + elseif ($Scope -is [System.Collections.IEnumerable] -and $Scope -isnot [string]) { + $collection = [Octopus.Client.Model.ReferenceCollection]::new($Scope) + $Scope = [Octopus.Client.Model.TenantVariables.CommonVariableScope]::new($collection) + } + else { + # Assume it's a single ID or empty + $collection = [Octopus.Client.Model.ReferenceCollection]::new(@($Scope)) + $Scope = [Octopus.Client.Model.TenantVariables.CommonVariableScope]::new($collection) + } + } + + $payload = [Octopus.Client.Model.TenantVariables.TenantCommonVariablePayload]::new( + $LibraryVariableSetId, + $TemplateId, + $Value, + $Scope + ) + + if (-not [string]::IsNullOrEmpty($VariableId)) { + $payload.Id = $VariableId + } + else { + $payload.Id = [string]::Empty + } + + return $payload + } +} diff --git a/OctopusDeploy/Public/Compare-EnvironmentScope.ps1 b/OctopusDeploy/Public/Compare-EnvironmentScope.ps1 new file mode 100644 index 0000000..77f4afb --- /dev/null +++ b/OctopusDeploy/Public/Compare-EnvironmentScope.ps1 @@ -0,0 +1,35 @@ +function Compare-EnvironmentScope { + param( + [AllowNull()] + [AllowEmptyCollection()] + [string[]]$ExistingScope, + + # allow empty for new scope to represent unscoped + [AllowNull()] + [AllowEmptyCollection()] + [string[]]$NewScope + ) + $existing = if ($ExistingScope) { $ExistingScope | Sort-Object -Unique } else { @() } + $new = if ($NewScope) { $NewScope | Sort-Object -Unique } else { @() } + + # Exact match + if (($existing -join ',') -eq ($new -join ',')) { + return [pscustomobject]@{ Status = 'Equal'; ExistingScope = $existing; NewScope = $null } + } + + $intersection = $existing | Where-Object { $new -contains $_ } + $remaining = $existing | Where-Object { $new -notcontains $_ } + + # No overlap + if (-not $intersection) { + return [pscustomobject]@{ Status = 'Disjoint'; ExistingScope = $existing; NewScope = $new } + } + + # Partial overlap where existing has items not in new (Existing is superset or mixed) + if ($remaining) { + return [pscustomobject]@{ Status = 'Overlap'; ExistingScope = $remaining; NewScope = $new } + } + + # Intersection exists and no remaining items in existing (Existing is subset of New) + return [pscustomobject]@{ Status = 'Contained'; ExistingScope = $null; NewScope = $new } +} diff --git a/OctopusDeploy/Public/Get-CommonTenantVariable.ps1 b/OctopusDeploy/Public/Get-CommonTenantVariable.ps1 index d9916c9..0a985c3 100644 --- a/OctopusDeploy/Public/Get-CommonTenantVariable.ps1 +++ b/OctopusDeploy/Public/Get-CommonTenantVariable.ps1 @@ -45,61 +45,13 @@ } } process { - # variables types [System.Enum]::GetNames([Octopus.Client.Model.VariableSetContentType]) - $tenantVar = $repo._repository.Tenants.GetVariables($Tenant) - - $environments = Get-Environment - # Determine which Variable Sets to process - $VariableSets = @() - if ($PSBoundParameters['VariableSet']) { - $VariableSets = $VariableSet - } - else { - $VariableSets = Get-VariableSet | Where-Object { $_.id -in $tenantVar.LibraryVariables.Keys } - } - $results = @() - foreach ($vSet in $VariableSets) { - # get default variables from each set - $libVars = $tenantVar.LibraryVariables."$($vSet.Id)" - $results += $libVars.Templates | ForEach-Object { $setvar = $libVars.Variables."$($_.id)"; - # only output default values that are not overridden - if ((-not $setvar.Value) -and (-not $setvar.SensitiveValue)) { - [pscustomobject]@{ - VariableSetName = $vSet.Name - Name = $_.name - Value = if ($_.DefaultValue.IsSensitive) { "*****" }else { $_.DefaultValue.value } - IsDefaultValue = $true - Scope = $null + $result = GetCommonTenantVariable @PSBoundParameters + $result | ForEach-Object { + if ($_.IsSensitive) { + $_.Value = "*****" } - } - } - # get all non default variables - $c = [Octopus.Client.Model.TenantVariables.GetCommonVariablesByTenantIdRequest]::new($Tenant.id, $Tenant.SpaceId) - $tenantVars = $repo._repository.TenantVariables.get($c) - - - $vars = $tenantVars.Variables | Where-Object LibraryVariableSetId -EQ $vSet.Id - - $results += $vars | ForEach-Object { - [pscustomobject]@{ - VariableSetName = $vSet.Name - Name = $_.template.name - Value = if ($_.Value.IsSensitive) { '*****' }else { $_.value.value } - IsDefaultValue = $false - Scope = [String[]]($_.scope.EnvironmentIds | ForEach-Object { $environments | Where-Object id -Like $_ }).name - } - } - # if an environment was specified, return all scoped variables for that environment and unscoped variables a far the variable has no environment scope - if ($PSBoundParameters['Environment']) { - [System.Array]$results = $results | Where-Object { ($_.Scope -contains $Environment.Name) -or ($null -eq $_.Scope -and ($results | Where-Object Name -EQ $_.name).count -eq 1) } - } - - - - } - # order results and output - $results = $results | Sort-Object VariableSetName, Name - $results + $_ + } | Select-Object -Property VariableSetName, Name , Value, IsDefaultValue, Scope } end {} diff --git a/OctopusDeploy/Public/GetCommonTenantVariable.ps1 b/OctopusDeploy/Public/GetCommonTenantVariable.ps1 new file mode 100644 index 0000000..975b41c --- /dev/null +++ b/OctopusDeploy/Public/GetCommonTenantVariable.ps1 @@ -0,0 +1,96 @@ +function GetCommonTenantVariable { + + [CmdletBinding()] + param ( + [parameter(Mandatory = $true)] + [TenantSingleTransformation()] + [Octopus.Client.Model.TenantResource]$Tenant, + [parameter(Mandatory = $false, + ValueFromPipeline = $true)] + [LibraryVariableSetSingleTransformation()] + [Octopus.Client.Model.LibraryVariableSetResource]$VariableSet, + [parameter(Mandatory = $false)] + [EnvironmentSingleTransformation()] + [Octopus.Client.Model.EnvironmentResource]$Environment + + ) + begin { + try { + ValidateConnection + } + catch { + $PSCmdlet.ThrowTerminatingError($_) + } + } + process { + # variables types [System.Enum]::GetNames([Octopus.Client.Model.VariableSetContentType]) + $tenantVar = $repo._repository.Tenants.GetVariables($Tenant) + + $environments = Get-Environment + # Determine which Variable Sets to process + $VariableSets = @() + if ($PSBoundParameters['VariableSet']) { + $VariableSets = $VariableSet + } + else { + $VariableSets = Get-VariableSet | Where-Object { $_.id -in $tenantVar.LibraryVariables.Keys } + } + $results = @() + foreach ($vSet in $VariableSets) { + # get default variables from each set + $libVars = $tenantVar.LibraryVariables."$($vSet.Id)" + $results += $libVars.Templates | ForEach-Object { $setvar = $libVars.Variables."$($_.id)"; + # only output default values that are not overridden + if ((-not $setvar.Value) -and (-not $setvar.SensitiveValue)) { + [pscustomobject]@{ + VariableSetName = $vSet.Name + Name = $_.name + Value = $_.DefaultValue.value + IsDefaultValue = $true + Scope = $null + ScopeIds = $null + TemplateID = $_.id + IsSensitive = $_.DefaultValue.IsSensitive + VariableId = $null + LibraryVariableSetId = $vSet.Id + } + } + } + # get all non default variables + $commonTenantVarRequest = [Octopus.Client.Model.TenantVariables.GetCommonVariablesByTenantIdRequest]::new($Tenant.id, $Tenant.SpaceId) + $tenantVars = $repo._repository.TenantVariables.get($commonTenantVarRequest) + + + $vars = $tenantVars.Variables | Where-Object LibraryVariableSetId -EQ $vSet.Id + + $results += $vars | ForEach-Object { + [pscustomobject]@{ + VariableSetName = $vSet.Name + Name = $_.template.name + Value = $_.value.value + IsDefaultValue = $false + Scope = [String[]]($_.scope.EnvironmentIds | ForEach-Object { $environments | Where-Object id -Like $_ }).name + ScopeIds = [String[]]($_.scope.EnvironmentIds) + TemplateID = $_.TemplateId + IsSensitive = $_.Value.IsSensitive + VariableId = $_.Id + LibraryVariableSetId = $_.LibraryVariableSetId + } + } + # if an environment was specified, return all scoped variables for that environment and unscoped variables a far the variable has no environment scope + if ($PSBoundParameters['Environment']) { + [System.Array]$results = $results | Where-Object { ($_.Scope -contains $Environment.Name) -or ($null -eq $_.Scope -and ($results | Where-Object Name -EQ $_.name).count -eq 1) } + } + + + + } + # order results and output + $results = $results | Sort-Object VariableSetName, Name + $results + } + + end {} + +} + diff --git a/OctopusDeploy/Public/Set-CommonTenantVariable.ps1 b/OctopusDeploy/Public/Set-CommonTenantVariable.ps1 index 56a2f9f..d2603c9 100644 --- a/OctopusDeploy/Public/Set-CommonTenantVariable.ps1 +++ b/OctopusDeploy/Public/Set-CommonTenantVariable.ps1 @@ -66,39 +66,137 @@ ParameterSetName = 'Hash')] [parameter(Mandatory = $false, ParameterSetName = 'Value')] - [string[]]$Environment = @() + [EnvironmentTransformation()] + [Octopus.Client.Model.EnvironmentResource[]]$Environment ) + begin { # testing connection to octopus try { ValidateConnection - } catch { + } + catch { $PSCmdlet.ThrowTerminatingError($_) } - # Function to compare environment scopes - # function Test-ScopeMatch($scope1, $scope2) { - # $env1 = @($scope1.EnvironmentIds | Sort-Object) - # $env2 = @($scope2.EnvironmentIds | Sort-Object) - - # if ($env1.Count -ne $env2.Count) { return $false } - - # for ($i = 0; $i -lt $env1.Count; $i++) { - # if ($env1[$i] -ne $env2[$i]) { return $false } - # } - - # return $true - # } } process { try { - # Fix the parameter set name comparison (case sensitive) + # Create the variable hash if using Name/Value parameters if ($PSCmdlet.ParameterSetName -eq "Value") { $VariableHash = @{} $VariableHash[$Name] = $Value } - Write-Verbose "Processing variables: $($VariableHash | ConvertTo-Json -Compress)" + if ($PSBoundParameters['Environment']) { + $envString = $Environment.name -join ', ' + } + Write-Verbose "Processing variables: $($VariableHash | ConvertTo-Json -Compress) $envString" + ################################################################################### + # new implementation + $getCommonTenantVariableSpat = @{ + Tenant = $Tenant + VariableSet = $VariableSet + } + + $currentVariables = GetCommonTenantVariable @getCommonTenantVariableSpat + + + # Check that all the variables are defined in Variable Set + foreach ($h in $VariableHash.GetEnumerator()) { + if ($currentVariables.Name -notcontains $h.Name) { + $message = "Couldn't find {0} in variable set {1}" -f $h.Name, $VariableSet.Name + $err = [System.Management.Automation.ErrorRecord]::new( + [System.Management.Automation.ItemNotFoundException]::new("$message"), + 'NotSpecified', + 'InvalidData', + "$($Variableset.name) / $($h.name)" + ) + $errorDetails = [System.Management.Automation.ErrorDetails]::new("$message") + $errorDetails.RecommendedAction = "Check variable exists in variable set" + $err.ErrorDetails = $errorDetails + $PSCmdlet.ThrowTerminatingError($err) + } + else { + $message = "Found variable {0} in variable set {1}" -f $h.Name, $VariableSet.Name + Write-Verbose $message + } + } + # Create the target scope we want to match + $targetEnvIds = [Octopus.Client.Model.ReferenceCollection]::new($Environment.id) + $targetScope = [Octopus.Client.Model.TenantVariables.CommonVariableScope]::new($targetEnvIds) + + + # Create payloads for all variables + $payloads = @() + foreach ($existingVar in $currentVariables ) { + # Get the variable template + $varTemplate = Get-VariableTemplate -VariableSet $VariableSet -Name $existingVar.Name + + # Determine if this variable is one we want to update and if both are scoped + if ($targetScope.EnvironmentIds.count -and $existingVar.Scope -and $VariableHash.Keys -contains $existingVar.Name) { + Write-Host "Existing scope env Ids: $($existingVar.ScopeIds -join ',')" + Write-Host "Target scope env Ids: $($targetScope.EnvironmentIds -join ',')" + $comparison = Compare-EnvironmentScope -ExistingScope $existingVar.ScopeIds -NewScope $Environment.id + $comparison | Out-String + # update old variable and new variable depending on comparison result + if ($comparison.Status -eq 'Disjoint'){ + 'keeping old variable as-is and adding new variable with updated value and target scope' + }elseif ($comparison.Status -in 'Equal', 'Contained') { + 'updating existing variable with new value' + }elseif ($comparison.Status -eq 'Overlap') { + 'updating existing variable to remove overlapping scope and adding new variable with updated value and target scope' + }else { + throw "Unhandled comparison status: $($comparison.Status)" + } + + } + elseif ($VariableHash.Keys -contains $existingVar.Name -and -not $existingVar.Scope -and $targetScope.EnvironmentIds.count -eq 0) { + "old value for {0} : {1}" -f $existingVar.Name, ($existingVar.Value) + "new value for {0} : {1}" -f $existingVar.Name, $VariableHash[$existingVar.Name] + "IsDefault {0}" -f $existingVar.IsDefaultValue + + # add old variable with updated value to payloads or create new variable if IsDefaultValue + $variableId = if (-not $existingVar.IsDefaultValue) { $existingVar.VariableId } else { $null } + + $newTenantCommonVariablePayloadSpat = @{ + LibraryVariableSetId = $existingVar.LibraryVariableSetId + TemplateId = $existingVar.TemplateId + Value = $VariableHash[$existingVar.Name] + Scope = $targetScope + VariableId = $variableId + } + $payload = New-TenantCommonVariablePayload @newTenantCommonVariablePayloadSpat + + $payloads += $payload + + } + else { + # add old variable as-is to payloads + "Preserving variable {0} as-is with scope {1}" -f $existingVar.Name, ($existingVar.Scope -join ',') + #Todo: check if sensitive .... + $newTenantCommonVariablePayloadSpat = @{ + LibraryVariableSetId = $existingVar.LibraryVariableSetId + TemplateId = $existingVar.TemplateId + Value = $existingVar.Value + Scope = $existingVar.Scope + VariableId = $existingVar.VariableId + } + $payload = New-TenantCommonVariablePayload @newTenantCommonVariablePayloadSpat + + $payloads += $payload + } + } + $payloads + exit + + + + + + ################################################################################### + # old implementation + ################################################################################### # Get variable set resource # $VariableSet = $repo._repository.LibraryVariableSets.FindByName($VariableSet) @@ -111,12 +209,15 @@ if ($VariableSet.Templates.name -notcontains $h.Name) { $message = "Couldn't find {0} in variable set {1}" -f $h.Name, $VariableSet.Name throw $message - } else { + } + else { $message = "Found variable {0} in variable set {1}" -f $h.Name, $VariableSet.Name Write-Verbose $message } } + # + # Create the target scope we want to match (move outside the loop) $targetEnvIds = [Octopus.Client.Model.ReferenceCollection]::new() foreach ($_environment in $Environment) { @@ -176,7 +277,8 @@ if ($targetVariable) { $payload.Id = $targetVariable.Id $action = "updated ID: $($targetVariable.Id)" - } else { + } + else { $payload.Id = [string]::Empty $action = "created new" } @@ -191,7 +293,8 @@ $command = [Octopus.Client.Model.TenantVariables.ModifyCommonVariablesByTenantIdCommand]::new($Tenant.Id, $Tenant.SpaceId, $payloads) $repo._repository.TenantVariables.Modify($command) | Out-Null - } catch { + } + catch { $PSCmdlet.ThrowTerminatingError($_) } } From c6ccb6fe9c6c00749722c14256784091ce9db1f2 Mon Sep 17 00:00:00 2001 From: Emrys MacInally Date: Tue, 9 Dec 2025 16:29:23 +0100 Subject: [PATCH 06/14] Enhance variable handling in New-TenantCommonVariablePayload and Set-CommonTenantVariable functions; add IsSensitive parameter and improve payload creation logic --- .../New-TenantCommonVariablePayload.ps1 | 10 +- .../Public/Compare-EnvironmentScope.ps1 | 4 +- .../Public/GetCommonTenantVariable.ps1 | 5 + .../Public/Set-CommonTenantVariable.ps1 | 322 ++++++++++-------- 4 files changed, 201 insertions(+), 140 deletions(-) diff --git a/OctopusDeploy/Private/New-TenantCommonVariablePayload.ps1 b/OctopusDeploy/Private/New-TenantCommonVariablePayload.ps1 index e8056ba..429862f 100644 --- a/OctopusDeploy/Private/New-TenantCommonVariablePayload.ps1 +++ b/OctopusDeploy/Private/New-TenantCommonVariablePayload.ps1 @@ -26,6 +26,9 @@ [Parameter(Mandatory = $true)] $Value, + [Parameter(Mandatory = $false)] + [bool]$IsSensitive = $false, + [Parameter(Mandatory = $true)] [AllowEmptyString()] [AllowNull()] @@ -38,7 +41,7 @@ process { # Handle Value if ($Value -isnot [Octopus.Client.Model.PropertyValueResource]) { - $Value = [Octopus.Client.Model.PropertyValueResource]::new($Value, $false) + $Value = [Octopus.Client.Model.PropertyValueResource]::new($Value, $IsSensitive) } # Handle Scope @@ -47,7 +50,10 @@ $Scope = [Octopus.Client.Model.TenantVariables.CommonVariableScope]::new($Scope) } elseif ($Scope -is [System.Collections.IEnumerable] -and $Scope -isnot [string]) { - $collection = [Octopus.Client.Model.ReferenceCollection]::new($Scope) + $collection = [Octopus.Client.Model.ReferenceCollection]::new() + foreach ($id in $Scope) { + $collection.Add($id) | Out-Null + } $Scope = [Octopus.Client.Model.TenantVariables.CommonVariableScope]::new($collection) } else { diff --git a/OctopusDeploy/Public/Compare-EnvironmentScope.ps1 b/OctopusDeploy/Public/Compare-EnvironmentScope.ps1 index 77f4afb..7ce7b0b 100644 --- a/OctopusDeploy/Public/Compare-EnvironmentScope.ps1 +++ b/OctopusDeploy/Public/Compare-EnvironmentScope.ps1 @@ -3,7 +3,7 @@ [AllowNull()] [AllowEmptyCollection()] [string[]]$ExistingScope, - + # allow empty for new scope to represent unscoped [AllowNull()] [AllowEmptyCollection()] @@ -19,7 +19,7 @@ $intersection = $existing | Where-Object { $new -contains $_ } $remaining = $existing | Where-Object { $new -notcontains $_ } - + # No overlap if (-not $intersection) { return [pscustomobject]@{ Status = 'Disjoint'; ExistingScope = $existing; NewScope = $new } diff --git a/OctopusDeploy/Public/GetCommonTenantVariable.ps1 b/OctopusDeploy/Public/GetCommonTenantVariable.ps1 index 975b41c..afc1262 100644 --- a/OctopusDeploy/Public/GetCommonTenantVariable.ps1 +++ b/OctopusDeploy/Public/GetCommonTenantVariable.ps1 @@ -46,6 +46,7 @@ VariableSetName = $vSet.Name Name = $_.name Value = $_.DefaultValue.value + ValueObject = $_.DefaultValue IsDefaultValue = $true Scope = $null ScopeIds = $null @@ -53,6 +54,7 @@ IsSensitive = $_.DefaultValue.IsSensitive VariableId = $null LibraryVariableSetId = $vSet.Id + origObject = $_ } } } @@ -68,6 +70,7 @@ VariableSetName = $vSet.Name Name = $_.template.name Value = $_.value.value + ValueObject = $_.value IsDefaultValue = $false Scope = [String[]]($_.scope.EnvironmentIds | ForEach-Object { $environments | Where-Object id -Like $_ }).name ScopeIds = [String[]]($_.scope.EnvironmentIds) @@ -75,6 +78,8 @@ IsSensitive = $_.Value.IsSensitive VariableId = $_.Id LibraryVariableSetId = $_.LibraryVariableSetId + origObject = $_ + } } # if an environment was specified, return all scoped variables for that environment and unscoped variables a far the variable has no environment scope diff --git a/OctopusDeploy/Public/Set-CommonTenantVariable.ps1 b/OctopusDeploy/Public/Set-CommonTenantVariable.ps1 index d2603c9..3e988d0 100644 --- a/OctopusDeploy/Public/Set-CommonTenantVariable.ps1 +++ b/OctopusDeploy/Public/Set-CommonTenantVariable.ps1 @@ -81,6 +81,7 @@ } process { + try { # Create the variable hash if using Name/Value parameters if ($PSCmdlet.ParameterSetName -eq "Value") { @@ -122,177 +123,226 @@ Write-Verbose $message } } + + # Create the target scope we want to match $targetEnvIds = [Octopus.Client.Model.ReferenceCollection]::new($Environment.id) $targetScope = [Octopus.Client.Model.TenantVariables.CommonVariableScope]::new($targetEnvIds) + + # Create payloads for all variables $payloads = @() - foreach ($existingVar in $currentVariables ) { - # Get the variable template - $varTemplate = Get-VariableTemplate -VariableSet $VariableSet -Name $existingVar.Name + + # Variable to preserve (not being updated) + $variableToPreserve = $currentVariables | Where-Object { $_.name -notin $VariableHash.Keys -and -not $_.IsDefaultValue } + foreach ($var in $variableToPreserve) { + $newTenantCommonVariablePayloadSpat = @{ + LibraryVariableSetId = $var.LibraryVariableSetId + TemplateId = $var.TemplateId + Value = $var.ValueObject + Scope = $var.ScopeIds + VariableId = $var.VariableId + } + $payload = New-TenantCommonVariablePayload @newTenantCommonVariablePayloadSpat + $payloads += $payload + } - # Determine if this variable is one we want to update and if both are scoped - if ($targetScope.EnvironmentIds.count -and $existingVar.Scope -and $VariableHash.Keys -contains $existingVar.Name) { - Write-Host "Existing scope env Ids: $($existingVar.ScopeIds -join ',')" - Write-Host "Target scope env Ids: $($targetScope.EnvironmentIds -join ',')" - $comparison = Compare-EnvironmentScope -ExistingScope $existingVar.ScopeIds -NewScope $Environment.id - $comparison | Out-String - # update old variable and new variable depending on comparison result - if ($comparison.Status -eq 'Disjoint'){ - 'keeping old variable as-is and adding new variable with updated value and target scope' - }elseif ($comparison.Status -in 'Equal', 'Contained') { - 'updating existing variable with new value' - }elseif ($comparison.Status -eq 'Overlap') { - 'updating existing variable to remove overlapping scope and adding new variable with updated value and target scope' - }else { - throw "Unhandled comparison status: $($comparison.Status)" - } + $variablesToChange = $currentVariables | Where-Object { $_.name -in $VariableHash.Keys } + - } - elseif ($VariableHash.Keys -contains $existingVar.Name -and -not $existingVar.Scope -and $targetScope.EnvironmentIds.count -eq 0) { - "old value for {0} : {1}" -f $existingVar.Name, ($existingVar.Value) - "new value for {0} : {1}" -f $existingVar.Name, $VariableHash[$existingVar.Name] - "IsDefault {0}" -f $existingVar.IsDefaultValue - - # add old variable with updated value to payloads or create new variable if IsDefaultValue - $variableId = if (-not $existingVar.IsDefaultValue) { $existingVar.VariableId } else { $null } - + if (-not $Environment) { + # We are updating unscoped variables only + # We will preserve all scoped variables as-is + foreach ($var in $variablesToChange | Where-Object { $_.Scope }) { $newTenantCommonVariablePayloadSpat = @{ - LibraryVariableSetId = $existingVar.LibraryVariableSetId - TemplateId = $existingVar.TemplateId - Value = $VariableHash[$existingVar.Name] - Scope = $targetScope - VariableId = $variableId + LibraryVariableSetId = $var.LibraryVariableSetId + TemplateId = $var.TemplateId + Value = $var.ValueObject + Scope = $var.ScopeIds + VariableId = $var.VariableId } $payload = New-TenantCommonVariablePayload @newTenantCommonVariablePayloadSpat - $payloads += $payload - } - else { - # add old variable as-is to payloads - "Preserving variable {0} as-is with scope {1}" -f $existingVar.Name, ($existingVar.Scope -join ',') - #Todo: check if sensitive .... - $newTenantCommonVariablePayloadSpat = @{ - LibraryVariableSetId = $existingVar.LibraryVariableSetId - TemplateId = $existingVar.TemplateId - Value = $existingVar.Value - Scope = $existingVar.Scope - VariableId = $existingVar.VariableId + # update only unscoped variables + foreach ($var in $variablesToChange | Where-Object { -not $_.Scope }) { + if ($VariableHash.Keys -contains $Var.Name) { + $newTenantCommonVariablePayloadSpat = @{ + LibraryVariableSetId = $var.LibraryVariableSetId + TemplateId = $var.TemplateId + Value = $VariableHash[$var.Name] + IsSensitive = $var.IsSensitive + Scope = @() # unscoped + VariableId = if ($var.VariableId) { $var.VariableId } else { $null } + } + $payload = New-TenantCommonVariablePayload @newTenantCommonVariablePayloadSpat + $payloads += $payload } - $payload = New-TenantCommonVariablePayload @newTenantCommonVariablePayloadSpat - - $payloads += $payload } - } - $payloads - exit - - - - - - ################################################################################### - # old implementation - ################################################################################### - # Get variable set resource - # $VariableSet = $repo._repository.LibraryVariableSets.FindByName($VariableSet) + # Execute update using new API with all variables - any excluded variables are deleted + $command = [Octopus.Client.Model.TenantVariables.ModifyCommonVariablesByTenantIdCommand]::new($Tenant.Id, $Tenant.SpaceId, $payloads) + $repo._repository.TenantVariables.Modify($command) | Out-Null + return # exit function as we are done handling unscoped only case - # Get ALL existing common variables for this tenant - $getRequest = [Octopus.Client.Model.TenantVariables.GetCommonVariablesByTenantIdRequest]::new($Tenant.Id, $Tenant.SpaceId) - $allExistingVariables = $repo._repository.TenantVariables.Get($getRequest).Variables - - # Check that all the variables are defined in template - foreach ($h in $VariableHash.GetEnumerator()) { - if ($VariableSet.Templates.name -notcontains $h.Name) { - $message = "Couldn't find {0} in variable set {1}" -f $h.Name, $VariableSet.Name - throw $message - } - else { - $message = "Found variable {0} in variable set {1}" -f $h.Name, $VariableSet.Name - Write-Verbose $message + } + # We are updating scoped variables + # first we preserve all unscoped variables as-is + foreach ($var in $variablesToChange | Where-Object { -not $_.Scope -and -not $_.IsDefaultValue}) { + $newTenantCommonVariablePayloadSpat = @{ + LibraryVariableSetId = $var.LibraryVariableSetId + TemplateId = $var.TemplateId + Value = $var.ValueObject + Scope = $var.ScopeIds + VariableId = $var.VariableId } + $payload = New-TenantCommonVariablePayload @newTenantCommonVariablePayloadSpat + $payloads += $payload } - # + - # Create the target scope we want to match (move outside the loop) - $targetEnvIds = [Octopus.Client.Model.ReferenceCollection]::new() - foreach ($_environment in $Environment) { - $envObj = $repo._repository.Environments.FindByName($_environment) - if (-not $envObj) { - $message = "Couldn't find environment {0}" -f $_environment - throw $message + # convert the value hashtable to an array and add property to remember if already added + $newVariable = $VariableHash.GetEnumerator() | ForEach-Object { + [PSCustomObject]@{ + Name = $_.Name + Value = $_.Value + Added = $false } - $message = "Found environment {0} with ID {1}" -f $envObj.Name, $envObj.Id - Write-Verbose $message - $targetEnvIds.Add($envObj.Id) | Out-Null } - $targetScope = [Octopus.Client.Model.TenantVariables.CommonVariableScope]::new($targetEnvIds) - # Create payloads for all variables - $payloads = @() - - # Add all existing variables (unchanged) to preserve them, except those we're updating - $variablesToUpdate = $VariableHash.Keys - foreach ($existingVar in $allExistingVariables) { - $varTemplate = $VariableSet.Templates | Where-Object Id -EQ $existingVar.TemplateId - $isTargetVariable = $variablesToUpdate -contains $varTemplate.Name #-and (Test-ScopeMatch $existingVar.Scope $targetScope) - - if (-not $isTargetVariable) { - # This is a different variable - preserve it as-is - $payload = [Octopus.Client.Model.TenantVariables.TenantCommonVariablePayload]::new( - $existingVar.LibraryVariableSetId, - $existingVar.TemplateId, - $existingVar.Value, - $existingVar.Scope - ) - $payload.Id = $existingVar.Id - $payloads += $payload - } - } + foreach ($var in $variablesToChange | Where-Object { $_.Scope }) { + # Determine if this variable is one we want to update and if both are scoped + if ($newVariable.name -contains $var.Name) { + if ($newVariable | Where-Object { $_.Name -eq $var.Name -and $_.Added -eq $true } ) { + # var already added. remove scope. if one or more scoping we need to update + # remove target scope from variable scope + + $newVarScope = $var.ScopeIds | Where-Object { $Environment.id -notcontains $_ } + + if ($newVarScope) { + # update existing variable with new scope + $newTenantCommonVariablePayloadSpat = @{ + LibraryVariableSetId = $var.LibraryVariableSetId + TemplateId = $var.TemplateId + Value = $var.ValueObject + Scope = $newVarScope + VariableId = $var.VariableId + } + $payload = New-TenantCommonVariablePayload @newTenantCommonVariablePayloadSpat + $payloads += $payload + + } + continue + } + Write-Host "Existing scope env Ids: $($Var.ScopeIds -join ',')" + Write-Host "Target scope env Ids: $($targetScope.EnvironmentIds -join ',')" + $comparison = Compare-EnvironmentScope -ExistingScope $var.ScopeIds -NewScope $Environment.id + $comparison | Out-String + # update old variable and new variable depending on comparison result + if ($comparison.Status -eq 'Disjoint') { + Write-Verbose 'Keeping old variable as-is and adding new variable with updated value and target scope' - # Now add/update each variable from the hash - foreach ($h in $VariableHash.GetEnumerator()) { - # get the template object. Id is needed to identify and set variable - $varTemplate = $VariableSet.Templates | Where-Object Name -EQ $h.Name + # add old variable as-is to payloads + $newTenantCommonVariablePayloadSpat = @{ + LibraryVariableSetId = $var.LibraryVariableSetId + TemplateId = $var.TemplateId + Value = $var.ValueObject + Scope = $var.ScopeIds + VariableId = $var.VariableId + } + $payload = New-TenantCommonVariablePayload @newTenantCommonVariablePayloadSpat + # check the payload does not already exist before adding to $payloads - # Find the specific variable we want to update (match library set, template, and scope) - $targetVariable = $allExistingVariables | Where-Object { - $_.LibraryVariableSetId -eq $VariableSet.Id -and - $_.TemplateId -eq $varTemplate.Id #-and - #(Test-ScopeMatch $_.Scope $targetScope) - } + + $payloads += $payload + + # add new variable with updated value and target scope + $newTenantCommonVariablePayloadSpat = @{ + LibraryVariableSetId = $var.LibraryVariableSetId + TemplateId = $var.TemplateId + Value = $VariableHash[$var.Name] + IsSensitive = $var.IsSensitive + Scope = $comparison.NewScope + } + $payload = New-TenantCommonVariablePayload @newTenantCommonVariablePayloadSpat + $payloads += $payload + $newVariable | Where-Object { $_.Name -eq $var.Name } | ForEach-Object { $_.Added = $true } + } + elseif ($comparison.Status -in 'Equal', 'Contained') { + Write-Verbose 'Updating existing variable with new value' + $newTenantCommonVariablePayloadSpat = @{ + LibraryVariableSetId = $var.LibraryVariableSetId + TemplateId = $var.TemplateId + Value = $VariableHash[$var.Name] + IsSensitive = $var.IsSensitive + Scope = if (-not $null -eq $comparison.ExistingScope) { $($comparison.ExistingScope) }else { $($comparison.NewScope) } + VariableId = $var.VariableId + } + $payload = New-TenantCommonVariablePayload @newTenantCommonVariablePayloadSpat + + $payloads += $payload + $newVariable | Where-Object { $_.Name -eq $var.Name } | ForEach-Object { $_.Added = $true } + + } + elseif ($comparison.Status -eq 'Overlap') { + Write-Verbose 'Updating existing variable to remove overlapping scope and adding new variable with updated value and target scope' + # update old variable with new scope + $newTenantCommonVariablePayloadSpat = @{ + LibraryVariableSetId = $var.LibraryVariableSetId + TemplateId = $var.TemplateId + Value = $var.ValueObject + Scope = $comparison.ExistingScope + VariableId = $var.VariableId + } + $payload = New-TenantCommonVariablePayload @newTenantCommonVariablePayloadSpat + + $payloads += $payload + + + # add new variable with updated value and target scope + $newTenantCommonVariablePayloadSpat = @{ + LibraryVariableSetId = $var.LibraryVariableSetId + TemplateId = $var.TemplateId + Value = $VariableHash[$var.Name] + IsSensitive = $var.IsSensitive + Scope = $comparison.NewScope + } + $payload = New-TenantCommonVariablePayload @newTenantCommonVariablePayloadSpat + + $payloads += $payload + $newVariable | Where-Object { $_.Name -eq $var.Name } | ForEach-Object { $_.Added = $true } + } + else { + throw "Unhandled comparison status: $($comparison.Status)" + } - # Create payload for this variable - $payload = [Octopus.Client.Model.TenantVariables.TenantCommonVariablePayload]::new( - $VariableSet.Id, - $varTemplate.Id, - [Octopus.Client.Model.PropertyValueResource]::new($h.Value, $false), - $targetScope - ) - - if ($targetVariable) { - $payload.Id = $targetVariable.Id - $action = "updated ID: $($targetVariable.Id)" } - else { - $payload.Id = [string]::Empty - $action = "created new" + } + + # Add any new variables that were not already added + foreach ($nv in $newVariable | Where-Object { $_.Added -eq $false }) { + $varInfo = $currentVariables | Where-Object { $_.Name -eq $nv.Name } | Select-Object -First 1 + $newTenantCommonVariablePayloadSpat = @{ + LibraryVariableSetId = $varInfo.LibraryVariableSetId + TemplateId = $varInfo.TemplateID + Value = $nv.Value + IsSensitive = $varInfo.IsSensitive + Scope = $Environment.Id } - + $payload = New-TenantCommonVariablePayload @newTenantCommonVariablePayloadSpat $payloads += $payload - - $scope = if ($Environment.Count -eq 0) { "unscoped" } else { "scoped to: $($Environment -join ', ')" } - Write-Verbose "Successfully processed '$($h.Name)' = '$($h.Value)' for tenant '$($Tenant.Name)' ($scope, $action)" } - + + $payloads # Execute update using new API with all variables - any excluded variables are deleted $command = [Octopus.Client.Model.TenantVariables.ModifyCommonVariablesByTenantIdCommand]::new($Tenant.Id, $Tenant.SpaceId, $payloads) $repo._repository.TenantVariables.Modify($command) | Out-Null + + + } catch { $PSCmdlet.ThrowTerminatingError($_) From c2f7ab495479d475653bec8bea26aaca42a12fa8 Mon Sep 17 00:00:00 2001 From: Emrys MacInally Date: Tue, 9 Dec 2025 19:37:35 +0100 Subject: [PATCH 07/14] Remove commented-out code for tenant variable payload creation in Set-CommonTenantVariable function --- .../Public/Set-CommonTenantVariable.ps1 | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/OctopusDeploy/Public/Set-CommonTenantVariable.ps1 b/OctopusDeploy/Public/Set-CommonTenantVariable.ps1 index 3e988d0..5adc3bb 100644 --- a/OctopusDeploy/Public/Set-CommonTenantVariable.ps1 +++ b/OctopusDeploy/Public/Set-CommonTenantVariable.ps1 @@ -258,17 +258,17 @@ $payloads += $payload - # add new variable with updated value and target scope - $newTenantCommonVariablePayloadSpat = @{ - LibraryVariableSetId = $var.LibraryVariableSetId - TemplateId = $var.TemplateId - Value = $VariableHash[$var.Name] - IsSensitive = $var.IsSensitive - Scope = $comparison.NewScope - } - $payload = New-TenantCommonVariablePayload @newTenantCommonVariablePayloadSpat - $payloads += $payload - $newVariable | Where-Object { $_.Name -eq $var.Name } | ForEach-Object { $_.Added = $true } + # # add new variable with updated value and target scope + # $newTenantCommonVariablePayloadSpat = @{ + # LibraryVariableSetId = $var.LibraryVariableSetId + # TemplateId = $var.TemplateId + # Value = $VariableHash[$var.Name] + # IsSensitive = $var.IsSensitive + # Scope = $comparison.NewScope + # } + # $payload = New-TenantCommonVariablePayload @newTenantCommonVariablePayloadSpat + # $payloads += $payload + # $newVariable | Where-Object { $_.Name -eq $var.Name } | ForEach-Object { $_.Added = $true } } elseif ($comparison.Status -in 'Equal', 'Contained') { Write-Verbose 'Updating existing variable with new value' @@ -335,7 +335,7 @@ $payloads += $payload } - $payloads + # Execute update using new API with all variables - any excluded variables are deleted $command = [Octopus.Client.Model.TenantVariables.ModifyCommonVariablesByTenantIdCommand]::new($Tenant.Id, $Tenant.SpaceId, $payloads) $repo._repository.TenantVariables.Modify($command) | Out-Null From 4bc8ddc3aa2bbc7e9512d2ba7f4c90c56d0c2d71 Mon Sep 17 00:00:00 2001 From: Marvin Becker Date: Wed, 10 Dec 2025 11:37:31 +0100 Subject: [PATCH 08/14] Add null check for Value parameter in New-TenantCommonVariablePayload function and fix variable name typo in Set-CommonTenantVariable function --- .../New-TenantCommonVariablePayload.ps1 | 3 ++ OctopusDeploy/Public/Invoke-RunbookRun.ps1 | 4 +- .../Public/Set-CommonTenantVariable.ps1 | 50 +++++++++---------- 3 files changed, 30 insertions(+), 27 deletions(-) diff --git a/OctopusDeploy/Private/New-TenantCommonVariablePayload.ps1 b/OctopusDeploy/Private/New-TenantCommonVariablePayload.ps1 index 429862f..fb94768 100644 --- a/OctopusDeploy/Private/New-TenantCommonVariablePayload.ps1 +++ b/OctopusDeploy/Private/New-TenantCommonVariablePayload.ps1 @@ -41,6 +41,9 @@ process { # Handle Value if ($Value -isnot [Octopus.Client.Model.PropertyValueResource]) { + if ([string]::IsNullOrEmpty($Value)) { + return $null + } $Value = [Octopus.Client.Model.PropertyValueResource]::new($Value, $IsSensitive) } diff --git a/OctopusDeploy/Public/Invoke-RunbookRun.ps1 b/OctopusDeploy/Public/Invoke-RunbookRun.ps1 index f474611..57fb3ca 100644 --- a/OctopusDeploy/Public/Invoke-RunbookRun.ps1 +++ b/OctopusDeploy/Public/Invoke-RunbookRun.ps1 @@ -260,7 +260,7 @@ $runbookRun.TenantId = $_tenant.id try { Write-Verbose "Running traditional runbook snapshot for tenant '$($_tenant.Name)'" - return $repo._repository.RunbookRuns.Create($runbookRun) + $repo._repository.RunbookRuns.Create($runbookRun) } catch { $PSCmdlet.WriteError($_) @@ -271,7 +271,7 @@ # Execute runbook without tenant try { Write-Verbose "Running traditional runbook snapshot (untenanted)" - return $repo._repository.RunbookRuns.Create($runbookRun) + $repo._repository.RunbookRuns.Create($runbookRun) } catch { $PSCmdlet.WriteError($_) diff --git a/OctopusDeploy/Public/Set-CommonTenantVariable.ps1 b/OctopusDeploy/Public/Set-CommonTenantVariable.ps1 index 5adc3bb..fe3466d 100644 --- a/OctopusDeploy/Public/Set-CommonTenantVariable.ps1 +++ b/OctopusDeploy/Public/Set-CommonTenantVariable.ps1 @@ -138,14 +138,14 @@ # Variable to preserve (not being updated) $variableToPreserve = $currentVariables | Where-Object { $_.name -notin $VariableHash.Keys -and -not $_.IsDefaultValue } foreach ($var in $variableToPreserve) { - $newTenantCommonVariablePayloadSpat = @{ + $newTenantCommonVariablePayloadSplat = @{ LibraryVariableSetId = $var.LibraryVariableSetId TemplateId = $var.TemplateId Value = $var.ValueObject Scope = $var.ScopeIds VariableId = $var.VariableId } - $payload = New-TenantCommonVariablePayload @newTenantCommonVariablePayloadSpat + $payload = New-TenantCommonVariablePayload @newTenantCommonVariablePayloadSplat $payloads += $payload } @@ -156,20 +156,20 @@ # We are updating unscoped variables only # We will preserve all scoped variables as-is foreach ($var in $variablesToChange | Where-Object { $_.Scope }) { - $newTenantCommonVariablePayloadSpat = @{ + $newTenantCommonVariablePayloadSplat = @{ LibraryVariableSetId = $var.LibraryVariableSetId TemplateId = $var.TemplateId Value = $var.ValueObject Scope = $var.ScopeIds VariableId = $var.VariableId } - $payload = New-TenantCommonVariablePayload @newTenantCommonVariablePayloadSpat + $payload = New-TenantCommonVariablePayload @newTenantCommonVariablePayloadSplat $payloads += $payload } # update only unscoped variables foreach ($var in $variablesToChange | Where-Object { -not $_.Scope }) { if ($VariableHash.Keys -contains $Var.Name) { - $newTenantCommonVariablePayloadSpat = @{ + $newTenantCommonVariablePayloadSplat = @{ LibraryVariableSetId = $var.LibraryVariableSetId TemplateId = $var.TemplateId Value = $VariableHash[$var.Name] @@ -177,7 +177,7 @@ Scope = @() # unscoped VariableId = if ($var.VariableId) { $var.VariableId } else { $null } } - $payload = New-TenantCommonVariablePayload @newTenantCommonVariablePayloadSpat + $payload = New-TenantCommonVariablePayload @newTenantCommonVariablePayloadSplat $payloads += $payload } } @@ -190,14 +190,14 @@ # We are updating scoped variables # first we preserve all unscoped variables as-is foreach ($var in $variablesToChange | Where-Object { -not $_.Scope -and -not $_.IsDefaultValue}) { - $newTenantCommonVariablePayloadSpat = @{ + $newTenantCommonVariablePayloadSplat = @{ LibraryVariableSetId = $var.LibraryVariableSetId TemplateId = $var.TemplateId Value = $var.ValueObject Scope = $var.ScopeIds VariableId = $var.VariableId } - $payload = New-TenantCommonVariablePayload @newTenantCommonVariablePayloadSpat + $payload = New-TenantCommonVariablePayload @newTenantCommonVariablePayloadSplat $payloads += $payload } @@ -223,14 +223,14 @@ if ($newVarScope) { # update existing variable with new scope - $newTenantCommonVariablePayloadSpat = @{ + $newTenantCommonVariablePayloadSplat = @{ LibraryVariableSetId = $var.LibraryVariableSetId TemplateId = $var.TemplateId Value = $var.ValueObject Scope = $newVarScope VariableId = $var.VariableId } - $payload = New-TenantCommonVariablePayload @newTenantCommonVariablePayloadSpat + $payload = New-TenantCommonVariablePayload @newTenantCommonVariablePayloadSplat $payloads += $payload } @@ -245,34 +245,34 @@ Write-Verbose 'Keeping old variable as-is and adding new variable with updated value and target scope' # add old variable as-is to payloads - $newTenantCommonVariablePayloadSpat = @{ + $newTenantCommonVariablePayloadSplat = @{ LibraryVariableSetId = $var.LibraryVariableSetId TemplateId = $var.TemplateId Value = $var.ValueObject Scope = $var.ScopeIds VariableId = $var.VariableId } - $payload = New-TenantCommonVariablePayload @newTenantCommonVariablePayloadSpat + $payload = New-TenantCommonVariablePayload @newTenantCommonVariablePayloadSplat # check the payload does not already exist before adding to $payloads $payloads += $payload # # add new variable with updated value and target scope - # $newTenantCommonVariablePayloadSpat = @{ + # $newTenantCommonVariablePayloadSplat = @{ # LibraryVariableSetId = $var.LibraryVariableSetId # TemplateId = $var.TemplateId # Value = $VariableHash[$var.Name] # IsSensitive = $var.IsSensitive # Scope = $comparison.NewScope # } - # $payload = New-TenantCommonVariablePayload @newTenantCommonVariablePayloadSpat + # $payload = New-TenantCommonVariablePayload @newTenantCommonVariablePayloadSplat # $payloads += $payload # $newVariable | Where-Object { $_.Name -eq $var.Name } | ForEach-Object { $_.Added = $true } } elseif ($comparison.Status -in 'Equal', 'Contained') { Write-Verbose 'Updating existing variable with new value' - $newTenantCommonVariablePayloadSpat = @{ + $newTenantCommonVariablePayloadSplat = @{ LibraryVariableSetId = $var.LibraryVariableSetId TemplateId = $var.TemplateId Value = $VariableHash[$var.Name] @@ -280,38 +280,38 @@ Scope = if (-not $null -eq $comparison.ExistingScope) { $($comparison.ExistingScope) }else { $($comparison.NewScope) } VariableId = $var.VariableId } - $payload = New-TenantCommonVariablePayload @newTenantCommonVariablePayloadSpat + $payload = New-TenantCommonVariablePayload @newTenantCommonVariablePayloadSplat - $payloads += $payload + if ($payload) { $payloads += $payload } $newVariable | Where-Object { $_.Name -eq $var.Name } | ForEach-Object { $_.Added = $true } } elseif ($comparison.Status -eq 'Overlap') { Write-Verbose 'Updating existing variable to remove overlapping scope and adding new variable with updated value and target scope' # update old variable with new scope - $newTenantCommonVariablePayloadSpat = @{ + $newTenantCommonVariablePayloadSplat = @{ LibraryVariableSetId = $var.LibraryVariableSetId TemplateId = $var.TemplateId Value = $var.ValueObject Scope = $comparison.ExistingScope VariableId = $var.VariableId } - $payload = New-TenantCommonVariablePayload @newTenantCommonVariablePayloadSpat + $payload = New-TenantCommonVariablePayload @newTenantCommonVariablePayloadSplat $payloads += $payload # add new variable with updated value and target scope - $newTenantCommonVariablePayloadSpat = @{ + $newTenantCommonVariablePayloadSplat = @{ LibraryVariableSetId = $var.LibraryVariableSetId TemplateId = $var.TemplateId Value = $VariableHash[$var.Name] IsSensitive = $var.IsSensitive Scope = $comparison.NewScope } - $payload = New-TenantCommonVariablePayload @newTenantCommonVariablePayloadSpat + $payload = New-TenantCommonVariablePayload @newTenantCommonVariablePayloadSplat - $payloads += $payload + if ($payload) { $payloads += $payload } $newVariable | Where-Object { $_.Name -eq $var.Name } | ForEach-Object { $_.Added = $true } } else { @@ -324,15 +324,15 @@ # Add any new variables that were not already added foreach ($nv in $newVariable | Where-Object { $_.Added -eq $false }) { $varInfo = $currentVariables | Where-Object { $_.Name -eq $nv.Name } | Select-Object -First 1 - $newTenantCommonVariablePayloadSpat = @{ + $newTenantCommonVariablePayloadSplat = @{ LibraryVariableSetId = $varInfo.LibraryVariableSetId TemplateId = $varInfo.TemplateID Value = $nv.Value IsSensitive = $varInfo.IsSensitive Scope = $Environment.Id } - $payload = New-TenantCommonVariablePayload @newTenantCommonVariablePayloadSpat - $payloads += $payload + $payload = New-TenantCommonVariablePayload @newTenantCommonVariablePayloadSplat + if ($payload) { $payloads += $payload } } From fa0097974d63d7379a6fa50af854f0b32fcb9479 Mon Sep 17 00:00:00 2001 From: Emrys MacInally Date: Wed, 10 Dec 2025 13:06:34 +0100 Subject: [PATCH 09/14] better verbose output in Add-RoleToMachine function --- OctopusDeploy/Public/Add-RoleToMachine.ps1 | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/OctopusDeploy/Public/Add-RoleToMachine.ps1 b/OctopusDeploy/Public/Add-RoleToMachine.ps1 index 684d033..6aa8b26 100644 --- a/OctopusDeploy/Public/Add-RoleToMachine.ps1 +++ b/OctopusDeploy/Public/Add-RoleToMachine.ps1 @@ -57,7 +57,13 @@ if ($pscmdlet.ShouldProcess("$($Machine.name)", "$wiMessages$($role -join ', ')")) { foreach ($_role in $Role) { - $Machine.Roles.Add($_role) + $added = $Machine.Roles.Add($_role) + if ($added){ + Write-Verbose "Added role $_role to machine $($Machine.Name)" + } + else { + Write-Verbose "Role $_role already exists on machine $($Machine.Name)" + } try { # Modify will return an update MachineResource. Only the last one will be returned to the user $lastMachineUpdate = $repo._repository.Machines.Modify($Machine) From 2017e74fcced301abeae71adb072b273404a3dca Mon Sep 17 00:00:00 2001 From: Emrys MacInally Date: Wed, 10 Dec 2025 16:15:16 +0100 Subject: [PATCH 10/14] Refactor variable retrieval in Set-CommonTenantVariable function for improved clarity and performance --- OctopusDeploy/Public/Set-CommonTenantVariable.ps1 | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/OctopusDeploy/Public/Set-CommonTenantVariable.ps1 b/OctopusDeploy/Public/Set-CommonTenantVariable.ps1 index fe3466d..f3ed2df 100644 --- a/OctopusDeploy/Public/Set-CommonTenantVariable.ps1 +++ b/OctopusDeploy/Public/Set-CommonTenantVariable.ps1 @@ -95,13 +95,10 @@ Write-Verbose "Processing variables: $($VariableHash | ConvertTo-Json -Compress) $envString" ################################################################################### # new implementation - $getCommonTenantVariableSpat = @{ - Tenant = $Tenant - VariableSet = $VariableSet - } + - $currentVariables = GetCommonTenantVariable @getCommonTenantVariableSpat - + $currentVariables = GetCommonTenantVariable -Tenant $Tenant + # Check that all the variables are defined in Variable Set foreach ($h in $VariableHash.GetEnumerator()) { @@ -136,7 +133,10 @@ $payloads = @() # Variable to preserve (not being updated) - $variableToPreserve = $currentVariables | Where-Object { $_.name -notin $VariableHash.Keys -and -not $_.IsDefaultValue } + $variableToPreserve = $currentVariables | Where-Object { ($_.name -notin $VariableHash.Keys -and -not $_.IsDefaultValue -and $_.LibraryVariableSetId -eq $VariableSet.id ) -or + (-not $_.IsDefaultValue -and $_.LibraryVariableSetId -ne $VariableSet.id ) } + + foreach ($var in $variableToPreserve) { $newTenantCommonVariablePayloadSplat = @{ LibraryVariableSetId = $var.LibraryVariableSetId From 2ee9c3de66d5335ab37034b7d5534bcffde5ff91 Mon Sep 17 00:00:00 2001 From: Emrys MacInally Date: Wed, 10 Dec 2025 17:07:02 +0100 Subject: [PATCH 11/14] Remove commented-out code for variable payload creation in Set-CommonTenantVariable function --- OctopusDeploy/Public/Set-CommonTenantVariable.ps1 | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/OctopusDeploy/Public/Set-CommonTenantVariable.ps1 b/OctopusDeploy/Public/Set-CommonTenantVariable.ps1 index f3ed2df..ffd8785 100644 --- a/OctopusDeploy/Public/Set-CommonTenantVariable.ps1 +++ b/OctopusDeploy/Public/Set-CommonTenantVariable.ps1 @@ -258,17 +258,6 @@ $payloads += $payload - # # add new variable with updated value and target scope - # $newTenantCommonVariablePayloadSplat = @{ - # LibraryVariableSetId = $var.LibraryVariableSetId - # TemplateId = $var.TemplateId - # Value = $VariableHash[$var.Name] - # IsSensitive = $var.IsSensitive - # Scope = $comparison.NewScope - # } - # $payload = New-TenantCommonVariablePayload @newTenantCommonVariablePayloadSplat - # $payloads += $payload - # $newVariable | Where-Object { $_.Name -eq $var.Name } | ForEach-Object { $_.Added = $true } } elseif ($comparison.Status -in 'Equal', 'Contained') { Write-Verbose 'Updating existing variable with new value' From 49fdf4b8b7df5d0bec0b78bf7cbdaf985c06e7c7 Mon Sep 17 00:00:00 2001 From: Emrys MacInally Date: Fri, 19 Dec 2025 13:27:00 +0100 Subject: [PATCH 12/14] Enhance Set-CommonTenantVariable function with improved documentation and error handling for scoped variables --- CHANGELOG.md | 142 ++++------------ .../Public/Set-CommonTenantVariable.ps1 | 151 +++++++++++++----- 2 files changed, 144 insertions(+), 149 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad3bcb6..a7454ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,129 +10,47 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Improved - **Get-Task**: Added support for retrieving tasks from `DeploymentResource` objects. - **Get-CommonTenantVariable**: Added support for filtering by Environment, made VariableSet optional to retrieve all variable sets, and improved output to include scope and variable set name. +- **Set-CommonTenantVariable**: Enhanced to handle scoped common tenant variables with environment scoping support. Intelligently handles scope conflicts (disjoint, overlapping, equal, contained), supports setting multiple variables at once via hashtable, includes comprehensive verbose logging, and detailed comment-based help with multiple examples. -## [2.0.0] +## [2.0.0] - Configuration as Code Runbook Support 🎉 -### Added - Configuration as Code Runbook Support 🎉 +### Added +- **Get-Runbook**: Configuration as Code (CaC) runbook support with optional `BranchName` parameter for Git-based projects. Auto-detects default branch when not specified. +- **Invoke-RunbookRun**: Execute CaC runbooks directly from Git branches with new `BranchName` parameter. Enhanced tenant and project type validation. -#### Get-Runbook -- **CaC Runbook Support**: Added ability to retrieve runbooks from Configuration as Code (CaC) projects stored in Git -- **BranchName Parameter**: New optional parameter to specify Git branch (supports both canonical names like `refs/heads/main` and short names like `main`) -- **Name Parameter**: New optional parameter for filtering runbooks by exact name match across all parameter sets -- **Auto-detection**: Automatically detects and uses the default branch for CaC projects when no branch is specified -- **Enhanced Examples**: Added 8+ comprehensive examples demonstrating CaC and traditional runbook retrieval - -#### Invoke-RunbookRun -- **CaC Runbook Execution**: Added support for running runbooks directly from Git branches for CaC projects -- **BranchName Parameter**: New optional parameter to specify which Git branch to run from (defaults to project's default branch) -- **Separate Parameter Sets**: Introduced distinct parameter sets (`Runbook` for CaC, `Snapshot` for traditional) for clearer intent -- **Automatic Validation**: Validates project type (CaC vs traditional) and provides clear error messages when wrong parameter set is used -- **Enhanced Tenant Validation**: Improved validation of tenant connections to project/environment before execution -- **Better Error Messages**: Custom error objects with appropriate categories for clearer troubleshooting - -#### Documentation -- **Complete Help Rewrite**: Comprehensive comment-based help for both `Get-Runbook` and `Invoke-RunbookRun` -- **Real-world Examples**: Added practical examples covering CaC branches, traditional snapshots, tenanted/untenanted scenarios -- **Parameter Clarity**: Enhanced parameter descriptions explaining when and how to use each parameter -- **Notes Section**: Added detailed notes about CaC detection, branch handling, and tenant modes -- **README & Getting Started**: Updated with extensive CaC runbook examples and usage patterns - -#### Infrastructure -- **GitHub Actions Workflow**: Added `release-with-github-publish.yaml` for automated releases -- **Utility Scripts**: Added helper scripts for tenant management and runbook operations -- **Build Enhancements**: Updated build process for better CI/CD integration - -### Changed - -#### Get-Runbook - BREAKING CHANGES -- **Parameter Name**: Renamed `RunbookID` parameter to `Id` for consistency with other functions -- **Name Matching**: Changed `-Name` parameter from wildcard support to exact match only -- **Default Behavior**: When called without parameters, now returns only non-CaC runbooks with a warning (previously returned all runbooks but couldn't access CaC runbooks) -- **Parameter Sets**: Simplified parameter sets - removed `byName` set, consolidated into `default`, `byProject`, and `byID` -- **Project Parameter**: Changed from accepting array (`ProjectResource[]`) to single object (`ProjectResource`) for clarity - -#### Invoke-RunbookRun - BREAKING CHANGES -- **Default Parameter Set**: Changed from `default` to `Runbook` for better clarity -- **Parameter Separation**: `Runbook` parameter now exclusively for CaC projects; must use `RunbookSnapshot` for traditional projects -- **Auto-resolution Removed**: No longer automatically resolves published snapshots when using `Runbook` parameter -- **Error Handling**: Changed from throwing terminating errors to writing non-terminating errors for better pipeline handling - -### Improved - -#### Error Handling -- Replaced `throw` statements with `Get-CustomError` and `WriteError` for non-terminating errors -- Added specific exception types (ArgumentException, OctopusResourceNotFoundException, InvalidOperationException) -- Improved error categories (InvalidData, InvalidOperation) for better error handling in scripts - -#### Validation -- Enhanced tenant deployment mode validation (Tenanted/Untenanted/TenantedOrUntenanted) -- Added validation for tenant connections to project/environment combinations -- Improved CaC project detection with edge case handling - -#### User Experience -- Added verbose logging throughout execution flow for better debugging -- Clear warning messages when CaC runbooks won't be returned -- Helpful error messages suggesting correct parameter usage -- Better ShouldProcess messages showing branch information - -### Fixed -- **Logic Error in Invoke-RunbookRun**: Corrected inverted CaC validation logic (was treating CaC projects as traditional) -- **Branch Handling**: Fixed default branch selection when multiple branches exist -- **Edge Case**: Properly handles CaC projects where individual runbooks may not be version controlled - -### Removed -- **Get-Runbook**: Removed `byName` parameter set (functionality merged into other parameter sets) -- **Get-Runbook**: Removed wildcard support from `-Name` parameter -- **Documentation**: Removed incorrect/broken `.LINK` references from comment-based help - -### Dependencies -- **Octopus.Client**: Updated to newer .NET Framework version for better CaC API support +### Changed - BREAKING +- **Get-Runbook**: + - Renamed parameter `RunbookID` → `Id` + - Changed `-Name` from wildcard to exact match only + - Simplified parameter sets (removed `byName`) + - `Project` parameter now accepts single object instead of array +- **Invoke-RunbookRun**: + - Separate parameter sets for CaC (`Runbook`) vs traditional (`RunbookSnapshot`) projects + - No longer auto-resolves published snapshots + - Changed to non-terminating errors for better pipeline handling ### Migration Guide - -#### Updating Get-Runbook Calls -```powershell -# Before -Get-Runbook -RunbookID "Runbooks-123" # Parameter renamed -Get-Runbook -Name "Deploy*" # Wildcards no longer supported -Get-Runbook # Didn't warn about missing CaC runbooks - -# After -Get-Runbook -Id "Runbooks-123" # Use -Id instead -Get-Runbook -Name "Deploy Application" # Exact match only -Get-Runbook -Project "MyProject" # Add -Project to get CaC runbooks -``` - -#### Updating Invoke-RunbookRun Calls ```powershell -# Before (auto-resolved to published snapshot) -Invoke-RunbookRun -Runbook "MyRunbook" -Environment Production +# Get-Runbook +Get-Runbook -RunbookID "Runbooks-123" # Before +Get-Runbook -Id "Runbooks-123" # After -# After (explicit parameter sets) -# For CaC runbooks: -Invoke-RunbookRun -Runbook "MyRunbook" -Environment Production -BranchName "main" +# Invoke-RunbookRun (CaC projects) +Invoke-RunbookRun -Runbook $runbook -Environment Production -BranchName "main" -# For traditional runbooks: -$snapshot = Get-RunbookSnapshot -Runbook "MyRunbook" -Latest +# Invoke-RunbookRun (Traditional projects) +$snapshot = Get-RunbookSnapshot -Runbook $runbook -Latest Invoke-RunbookRun -RunbookSnapshot $snapshot -Environment Production ``` -### Technical Details - -#### API Methods Used -- `$repo._repository.Projects.GetAllRunbooks($Project, $BranchCanonicalName)` - Retrieve CaC runbooks from specific branch -- `$repo._repository.Runbooks.Run($project, $branch, $slug, $parameters)` - Execute CaC runbook from Git -- `$repo._repository.RunbookRuns.Create($runbookRun)` - Execute traditional snapshot-based runbook - -#### Branch Resolution Logic -1. Retrieves all branches via `Get-GitBranch` -2. If `BranchName` specified, matches by name or canonical name -3. If no branch specified, automatically uses default branch -4. Falls back to non-versioned mode for traditional projects +### Improved +- Enhanced error handling with specific exception types and non-terminating errors +- Comprehensive verbose logging for debugging +- Updated comment-based help with extensive examples ---- +### Fixed +- Corrected inverted CaC validation logic in Invoke-RunbookRun +- Fixed default branch selection for multiple branches -**Semver Tag**: `+semver:major` -**Related Issues**: DNA-337 -**Breaking Changes**: Yes - see migration guide above +**Related Issues**: DNA-337 diff --git a/OctopusDeploy/Public/Set-CommonTenantVariable.ps1 b/OctopusDeploy/Public/Set-CommonTenantVariable.ps1 index ffd8785..adde6dc 100644 --- a/OctopusDeploy/Public/Set-CommonTenantVariable.ps1 +++ b/OctopusDeploy/Public/Set-CommonTenantVariable.ps1 @@ -1,35 +1,65 @@ function Set-CommonTenantVariable { <# .SYNOPSIS - Sets or resets a common variable for a tenant, optionally scoped to specific environments. + Sets or resets common tenant variables with support for environment scoping. .DESCRIPTION - This function allows you to set or reset a common variable for a specified tenant in Octopus Deploy. - You can provide a single variable name and value, or a hashtable of multiple variables to set at once. - Additionally, you can scope the variable to specific environments if needed. + This function manages common tenant variables in Octopus Deploy, allowing you to set or reset variable values + with optional environment scoping. The function intelligently handles scope conflicts by comparing existing and + new scopes, preserving non-overlapping values while updating target environments. + + Key features: + - Set single or multiple variables at once using a hashtable + - Scope variables to specific environments or leave unscoped + - Automatically handles scope conflicts (disjoint, overlapping, equal, contained) + - Reset variables to default by providing an empty string value + - Preserves all other tenant variables not being modified + + When updating scoped variables, the function compares the existing scope with the target scope: + - Disjoint: Keeps existing scoped value and adds new value with target scope + - Equal/Contained: Updates the existing variable with the new value + - Overlap: Splits the variable - preserves non-overlapping environments with old value, updates target environments with new value .PARAMETER Tenant - The tenant to modify. + The tenant to modify. Accepts tenant name, ID, or TenantResource object. .PARAMETER VariableSet - The variable set to modify. + The library variable set containing the common variables. Accepts variable set name, ID, or LibraryVariableSetResource object. .PARAMETER Name - The name of the variable to modify. + The name of the variable to modify. Used when setting a single variable. .PARAMETER Value - The new value for the variable. + The new value for the variable. Use an empty string ('') to reset the variable to its default value. .PARAMETER VariableHash - A hashtable of variable names and values to set multiple variables at once. + A hashtable of variable names and values to set multiple variables in a single operation. + Example: @{Port = "1111"; IP = "1.2.3.4"} .PARAMETER Environment An array of environment names to scope the variable to. If not provided, the variable will be unscoped. + Accepts environment names, IDs, or EnvironmentResource objects. + .EXAMPLE + Set-CommonTenantVariable -Tenant 'Acme Corp' -VariableSet 'Customer Variables' -Name 'Password' -Value 'P@ssw0rd' + + Sets the unscoped variable 'Password' to 'P@ssw0rd' for tenant 'Acme Corp'. .EXAMPLE - Set-CommonTenantVariable -Tenant Tenant -VariableSet 'Customer Variables' -Name 'Password' -Value '123' - Sets the variable to 123 + Set-CommonTenantVariable -Tenant 'Acme Corp' -VariableSet 'Customer Variables' -Name 'Password' -Value '' + + Resets the 'Password' variable back to its default value. .EXAMPLE - Set-CommonTenantVariable -Tenant Tenant -VariableSet 'Customer Variables' -Name 'Password' -Value '' - Resets the variable back to default + Set-CommonTenantVariable -Tenant 'Acme Corp' -VariableSet 'Customer Variables' -VariableHash @{Port = "1111"; IP = "1.2.3.4"} + + Sets multiple unscoped variables at once using a hashtable. .EXAMPLE - Set-CommonTenantVariable -Tenant Tenant -VariableSet 'Customer Variables' -VariableHash @{Port = "1111"; IP = "1.2.3.4"} - Sets multiple variables by passing a hashtable + Set-CommonTenantVariable -Tenant 'Acme Corp' -VariableSet 'Customer Variables' -Name 'DatabaseType' -Value 'PostgreSQL' -Environment 'Production' + + Sets the 'DatabaseType' variable to 'PostgreSQL' scoped to the Production environment only. .EXAMPLE - Set-CommonTenantVariable -Tenant $tenant -VariableSet $variableSet -Name "DatabaseType" -Value "PostgreSQL" -Verbose -Environment $environment - Sets the variable scoped to a specific environment + Set-CommonTenantVariable -Tenant 'Acme Corp' -VariableSet 'Customer Variables' -Name 'ConnectionString' -Value 'Server=new-server' -Environment 'Test','QA','Production' + + Sets the 'ConnectionString' variable scoped to multiple environments. If the variable already has values in other environments, + those will be preserved while the Test, QA, and Production environments will be updated with the new value. + .EXAMPLE + Set-CommonTenantVariable -Tenant 'Acme Corp' -VariableSet 'Customer Variables' -VariableHash @{Port = "5432"; DatabaseType = "PostgreSQL"} -Environment 'Development' + + Sets multiple variables scoped to a specific environment using a hashtable. + .NOTES + This function uses the Octopus Deploy Client API to modify tenant variables. All changes are atomic - + either all variables are updated successfully or none are changed. #> [CmdletBinding()] param ( @@ -82,6 +112,9 @@ } process { + + + try { # Create the variable hash if using Name/Value parameters if ($PSCmdlet.ParameterSetName -eq "Value") { @@ -90,15 +123,52 @@ } if ($PSBoundParameters['Environment']) { - $envString = $Environment.name -join ', ' + $envString = " scoped to environments: $($Environment.name -join ', ')" + Write-Verbose "Processing variables for tenant '$($Tenant.Name)' in variable set '$($VariableSet.Name)'$envString" } - Write-Verbose "Processing variables: $($VariableHash | ConvertTo-Json -Compress) $envString" - ################################################################################### - # new implementation - + else { + Write-Verbose "Processing unscoped variables for tenant '$($Tenant.Name)' in variable set '$($VariableSet.Name)'" + } + Write-Verbose "Variables to update: $($VariableHash.Keys -join ', ')" + + # check the tenant has variables from the variable set passed in as a parameter $currentVariables = GetCommonTenantVariable -Tenant $Tenant - + if ($currentVariables.LibraryVariableSetId -notcontains $VariableSet.Id) { + $message = "Tenant $($Tenant.Name) does not have any variables from variable set `"$($VariableSet.Name)`"" + $err = [System.Management.Automation.ErrorRecord]::new( + [System.Management.Automation.ItemNotFoundException]::new("$message"), + 'NotSpecified', + 'InvalidData', + "$($Tenant.name) / $($Variableset.name)" + ) + $errorDetails = [System.Management.Automation.ErrorDetails]::new("$message") + $errorDetails.RecommendedAction = "Ensure tenant has variables from variable set" + $err.ErrorDetails = $errorDetails + $PSCmdlet.ThrowTerminatingError($err) + } + + # Check that the variables to set exist in the variable set + $currentVarNames = ($currentVariables | Where-Object { $_.LibraryVariableSetId -eq $VariableSet.id }).Name + $missingVars = @() + foreach ($varName in $VariableHash.Keys) { + if ($currentVarNames -notcontains $varName) { + $missingVars += $varName + } + } + if ($missingVars) { + $message = "The following variables were not found in variable set {0}: {1}" -f $VariableSet.Name, ($missingVars -join ', ') + $err = [System.Management.Automation.ErrorRecord]::new( + [System.Management.Automation.ItemNotFoundException]::new("$message"), + 'NotSpecified', + 'InvalidData', + "$($Variableset.name) / $($missingVars -join ', ')" + ) + $errorDetails = [System.Management.Automation.ErrorDetails]::new("$message") + $errorDetails.RecommendedAction = "Check variable names exist in variable set" + $err.ErrorDetails = $errorDetails + $PSCmdlet.ThrowTerminatingError($err) + } # Check that all the variables are defined in Variable Set foreach ($h in $VariableHash.GetEnumerator()) { @@ -133,10 +203,12 @@ $payloads = @() # Variable to preserve (not being updated) - $variableToPreserve = $currentVariables | Where-Object { ($_.name -notin $VariableHash.Keys -and -not $_.IsDefaultValue -and $_.LibraryVariableSetId -eq $VariableSet.id ) -or - (-not $_.IsDefaultValue -and $_.LibraryVariableSetId -ne $VariableSet.id ) } + $variableToPreserve = $currentVariables | Where-Object { ($_.name.ToLower() -notin $VariableHash.Keys.ToLower() -and -not $_.IsDefaultValue -and $_.LibraryVariableSetId -eq $VariableSet.id ) -or + (-not $_.IsDefaultValue -and $_.LibraryVariableSetId -ne $VariableSet.id ) } - + if ($variableToPreserve) { + Write-Verbose "Preserving $($variableToPreserve.Count) existing variable(s)" + } foreach ($var in $variableToPreserve) { $newTenantCommonVariablePayloadSplat = @{ LibraryVariableSetId = $var.LibraryVariableSetId @@ -153,6 +225,7 @@ if (-not $Environment) { + Write-Verbose "Updating unscoped variables only" # We are updating unscoped variables only # We will preserve all scoped variables as-is foreach ($var in $variablesToChange | Where-Object { $_.Scope }) { @@ -182,14 +255,17 @@ } } # Execute update using new API with all variables - any excluded variables are deleted + Write-Verbose "Applying $($payloads.Count) variable payload(s) to tenant '$($Tenant.Name)'" $command = [Octopus.Client.Model.TenantVariables.ModifyCommonVariablesByTenantIdCommand]::new($Tenant.Id, $Tenant.SpaceId, $payloads) $repo._repository.TenantVariables.Modify($command) | Out-Null + Write-Verbose "Successfully updated unscoped variables for tenant '$($Tenant.Name)'" return # exit function as we are done handling unscoped only case } # We are updating scoped variables + Write-Verbose "Updating scoped variables" # first we preserve all unscoped variables as-is - foreach ($var in $variablesToChange | Where-Object { -not $_.Scope -and -not $_.IsDefaultValue}) { + foreach ($var in $variablesToChange | Where-Object { -not $_.Scope -and -not $_.IsDefaultValue }) { $newTenantCommonVariablePayloadSplat = @{ LibraryVariableSetId = $var.LibraryVariableSetId TemplateId = $var.TemplateId @@ -236,13 +312,11 @@ } continue } - Write-Host "Existing scope env Ids: $($Var.ScopeIds -join ',')" - Write-Host "Target scope env Ids: $($targetScope.EnvironmentIds -join ',')" $comparison = Compare-EnvironmentScope -ExistingScope $var.ScopeIds -NewScope $Environment.id - $comparison | Out-String + Write-Verbose "Scope comparison for variable '$($var.Name)': $($comparison.Status)" # update old variable and new variable depending on comparison result if ($comparison.Status -eq 'Disjoint') { - Write-Verbose 'Keeping old variable as-is and adding new variable with updated value and target scope' + Write-Verbose "Variable '$($var.Name)': Keeping existing scope and adding new scoped value" # add old variable as-is to payloads $newTenantCommonVariablePayloadSplat = @{ @@ -260,7 +334,7 @@ } elseif ($comparison.Status -in 'Equal', 'Contained') { - Write-Verbose 'Updating existing variable with new value' + Write-Verbose "Variable '$($var.Name)': Updating existing scoped variable with new value" $newTenantCommonVariablePayloadSplat = @{ LibraryVariableSetId = $var.LibraryVariableSetId TemplateId = $var.TemplateId @@ -276,7 +350,7 @@ } elseif ($comparison.Status -eq 'Overlap') { - Write-Verbose 'Updating existing variable to remove overlapping scope and adding new variable with updated value and target scope' + Write-Verbose "Variable '$($var.Name)': Splitting overlapping scope - preserving non-overlapping environments and updating target environments" # update old variable with new scope $newTenantCommonVariablePayloadSplat = @{ LibraryVariableSetId = $var.LibraryVariableSetId @@ -289,6 +363,11 @@ $payloads += $payload + # there seems to be and issue with setting sensitive variables to empty string in overlapping scope scenario + # workaround is to set a value to something else first then set to empty string in a second call + if ($var.IsSensitive -and [string]::IsNullOrEmpty($VariableHash[$var.Name])) { + Set-CommonTenantVariable @PSBoundParameters -Value 'TemporaryValueForSensitiveVariable' -Verbose:$false + } # add new variable with updated value and target scope $newTenantCommonVariablePayloadSplat = @{ @@ -326,12 +405,10 @@ # Execute update using new API with all variables - any excluded variables are deleted + Write-Verbose "Applying $($payloads.Count) variable payload(s) to tenant '$($Tenant.Name)'" $command = [Octopus.Client.Model.TenantVariables.ModifyCommonVariablesByTenantIdCommand]::new($Tenant.Id, $Tenant.SpaceId, $payloads) $repo._repository.TenantVariables.Modify($command) | Out-Null - - - - + Write-Verbose "Successfully updated scoped variables for tenant '$($Tenant.Name)'" } catch { $PSCmdlet.ThrowTerminatingError($_) From fd3bdc304c8b28d2d1018b00c747600c230e4e73 Mon Sep 17 00:00:00 2001 From: Emrys MacInally Date: Fri, 19 Dec 2025 13:38:46 +0100 Subject: [PATCH 13/14] Update release workflow to automate CHANGELOG updates and extract current version details --- .github/workflows/release.yaml | 66 ++++++++++++++++++++++++++++++++-- 1 file changed, 63 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 5ac5085..b00938d 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -29,6 +29,59 @@ jobs: uses: gittools/actions/gitversion/execute@v0 with: useConfigFile: true + - name: Update CHANGELOG + shell: pwsh + run: | + $version = "${{ steps.gitversion.outputs.semVer }}" + $date = Get-Date -Format "yyyy-MM-dd" + $changelogPath = ".\CHANGELOG.md" + + Write-Host "Updating CHANGELOG.md with version $version" + $content = Get-Content $changelogPath -Raw + $content = $content -replace '## \[Unreleased\]', "## [$version] - $date" + Set-Content $changelogPath -Value $content -NoNewline + + Write-Host "CHANGELOG.md updated successfully" + + - name: Extract current version changelog + id: changelog + shell: pwsh + run: | + $version = "${{ steps.gitversion.outputs.semVer }}" + $changelogPath = ".\CHANGELOG.md" + + $content = Get-Content $changelogPath -Raw + + # Extract content between current version and next version/end + $pattern = "(?s)## \[$version\].*?\n(.*?)(?=\n## \[|$)" + if ($content -match $pattern) { + $changelogContent = $matches[1].Trim() + + # Escape for GitHub Actions output + $changelogContent = $changelogContent -replace '%', '%25' + $changelogContent = $changelogContent -replace '\n', '%0A' + $changelogContent = $changelogContent -replace '\r', '%0D' + + # Use new GitHub Actions output syntax + "CHANGELOG_CONTENT< Date: Fri, 19 Dec 2025 14:23:59 +0100 Subject: [PATCH 14/14] Fix sensitive variable handling in Set-CommonTenantVariable function by explicitly passing parameters for temporary value assignment --- OctopusDeploy/Public/Set-CommonTenantVariable.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OctopusDeploy/Public/Set-CommonTenantVariable.ps1 b/OctopusDeploy/Public/Set-CommonTenantVariable.ps1 index adde6dc..2951408 100644 --- a/OctopusDeploy/Public/Set-CommonTenantVariable.ps1 +++ b/OctopusDeploy/Public/Set-CommonTenantVariable.ps1 @@ -366,7 +366,7 @@ # there seems to be and issue with setting sensitive variables to empty string in overlapping scope scenario # workaround is to set a value to something else first then set to empty string in a second call if ($var.IsSensitive -and [string]::IsNullOrEmpty($VariableHash[$var.Name])) { - Set-CommonTenantVariable @PSBoundParameters -Value 'TemporaryValueForSensitiveVariable' -Verbose:$false + Set-CommonTenantVariable -Tenant $Tenant -VariableSet $VariableSet -Name $var.Name -Value 'TemporaryValueForSensitiveVariable' -Environment $Environment -Verbose:$false } # add new variable with updated value and target scope