Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Pipeline/Set-DevOpsPipelineVariables.ps1

<#
.SYNOPSIS
Creates or updates a set of pipeline (build definition) variables.
 
.DESCRIPTION
Pipeline variables live on the classic build definition, not on the Pipelines API resource, and a
'PUT' has to send the whole definition back. This reads the current definition, adds or replaces
each named variable in its 'variables' map, and writes the definition back - leaving every other
variable and every other field on the definition untouched.
 
Secret variable values are write-only: the API never returns one, so every call sends the value
the configuration currently holds. There is no way to detect, from here, whether that value
actually changed on the far end.
 
.PARAMETER ApiUri
The base organization API URI, e.g. 'https://dev.azure.com/myorg'.
 
.PARAMETER ProjectName
The Azure DevOps project name.
 
.PARAMETER DefinitionId
The pipeline/build definition id.
 
.PARAMETER Variables
An array of hashtables shaped '@{ Name; Value; IsSecret; AllowOverride }'. 'IsSecret' and
'AllowOverride' default to $false when not present on an entry.
 
.EXAMPLE
Set-DevOpsPipelineVariables -ApiUri 'https://dev.azure.com/myorg' -ProjectName 'MyProject' -DefinitionId 42 `
    -Variables @(@{ Name = 'Environment'; Value = 'Prod' }, @{ Name = 'ApiKey'; Value = 's3cr3t'; IsSecret = $true })
#>

Function Set-DevOpsPipelineVariables
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$ApiUri,
        [Parameter(Mandatory)][string]$ProjectName,
        [Parameter(Mandatory)][int]$DefinitionId,
        [Parameter(Mandatory)][Hashtable[]]$Variables
    )

    $definition = Get-DevOpsBuildDefinition -ApiUri $ApiUri -ProjectName $ProjectName -DefinitionId $DefinitionId

    if ($null -eq $definition)
    {
        Throw "[Set-DevOpsPipelineVariables] Could not read build definition '$DefinitionId' for '$ProjectName'."
    }

    $variablesNode = if ($null -ne $definition.variables) { $definition.variables } else { [PSCustomObject]@{} }

    foreach ($variable in $Variables)
    {
        $name = [string]$variable.Name
        $isSecret = if ($variable.ContainsKey('IsSecret')) { [bool]$variable.IsSecret } else { $false }
        $allowOverride = if ($variable.ContainsKey('AllowOverride')) { [bool]$variable.AllowOverride } else { $false }
        $value = if ($null -eq $variable.Value) { '' } else { [string]$variable.Value }

        $entry = [PSCustomObject]@{
            value         = $value
            isSecret      = $isSecret
            allowOverride = $allowOverride
        }

        if ($variablesNode.PSObject.Properties[$name])
        {
            $variablesNode.PSObject.Properties[$name].Value = $entry
        }
        else
        {
            $variablesNode | Add-Member -MemberType NoteProperty -Name $name -Value $entry -Force
        }
    }

    # A Hashtable accepts a dot-assignment to a key that does not exist yet; a PSCustomObject - the
    # shape a JSON-deserialized API response actually takes - throws unless the NoteProperty is
    # already present, which is only guaranteed when the API included a 'variables' field at all.
    if ($definition -is [System.Collections.IDictionary])
    {
        $definition.variables = $variablesNode
    }
    elseif ($definition.PSObject.Properties['variables'])
    {
        $definition.PSObject.Properties['variables'].Value = $variablesNode
    }
    else
    {
        $definition | Add-Member -MemberType NoteProperty -Name 'variables' -Value $variablesNode -Force
    }

    return Set-DevOpsBuildDefinition -ApiUri $ApiUri -ProjectName $ProjectName -DefinitionId $DefinitionId -Definition $definition
}