Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .github/tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,17 @@ The tests use a set of environemnts to managed by the ALZ team. These are:
- Organisation: microsoft-azure-landing-zones-cd-tests
- GiHub
- Organisation: microsoft-azure-landingzones-cd-tests

## Validation Script

Use the script below to validate deterministic deployment stack naming behavior for Bug #4062:

- Script: `.github/tests/scripts/validate-deployment-stack-name-hash.ps1`
- Run from repo root: `pwsh ./.github/tests/scripts/validate-deployment-stack-name-hash.ps1`

This validation confirms:

1. Legacy names are preserved when `prefix-base` length is within Azure's 64-character limit.
2. Different long names (for example `stage-three` and `stage-four`) produce different final stack names.
3. Final stack names remain within Azure's 64-character limit.
4. Very long prefixes are trimmed safely while preserving the hash suffix when hash mode is required.
Comment on lines +45 to +48
167 changes: 167 additions & 0 deletions .github/tests/scripts/validate-deployment-stack-name-hash.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
param()

function Get-Sha256HashPrefix {
param(
[Parameter(Mandatory = $true)]
[string]$InputString,
[Parameter(Mandatory = $true)]
[int]$Length
)

$sha256 = [System.Security.Cryptography.SHA256]::Create()
try {
$hashBytes = $sha256.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($InputString))
} finally {
$sha256.Dispose()
}

return [System.BitConverter]::ToString($hashBytes).Replace("-", "").Substring(0, $Length).ToLower()
}

function New-RemoteDeterministicDeploymentStackName {
param(
[Parameter(Mandatory = $true)]
[string]$DeploymentPrefix,
[Parameter(Mandatory = $true)]
[string]$OriginalName
)

$normalizedBase = $OriginalName
if ([string]::IsNullOrWhiteSpace($normalizedBase)) {
$normalizedBase = "deployment"
}
$normalizedBase = $normalizedBase.Replace(" ", "-")

$hashLength = 10
$hash = Get-Sha256HashPrefix -InputString "$DeploymentPrefix|$normalizedBase" -Length $hashLength

$legacyName = "$DeploymentPrefix-$normalizedBase"
if ($legacyName.Length -le 64) {
return $legacyName
}

$safePrefix = $DeploymentPrefix
$prefixMaxLength = [Math]::Max(1, 64 - $hashLength - 3)
if ($safePrefix.Length -gt $prefixMaxLength) {
$safePrefix = $safePrefix.Substring(0, $prefixMaxLength)
}

$baseMaxLength = [Math]::Max(1, 64 - $safePrefix.Length - $hashLength - 2)
$safeBase = $normalizedBase
if ($safeBase.Length -gt $baseMaxLength) {
$safeBase = $safeBase.Substring(0, $baseMaxLength)
}

return "$safePrefix-$safeBase-$hash"
}

$localPrefix = "alzrishabh2705-long-management-group-id"
function New-LocalDeterministicDeploymentStackName {
param(
[Parameter(Mandatory = $true)]
[string]$DeploymentPrefix,
[Parameter(Mandatory = $true)]
[string]$OriginalName
)

$normalizedBase = $OriginalName
if ([string]::IsNullOrWhiteSpace($normalizedBase)) {
$normalizedBase = "deployment"
}
$normalizedBase = $normalizedBase.Replace(" ", "-")

if ($normalizedBase.Length -le 64) {
return $normalizedBase
}

$hashLength = 10
$hash = Get-Sha256HashPrefix -InputString "$DeploymentPrefix|$normalizedBase" -Length $hashLength

$safePrefix = $DeploymentPrefix
$prefixMaxLength = [Math]::Max(1, 64 - $hashLength - 3)
if ($safePrefix.Length -gt $prefixMaxLength) {
$safePrefix = $safePrefix.Substring(0, $prefixMaxLength)
}

$baseMaxLength = [Math]::Max(1, 64 - $safePrefix.Length - $hashLength - 2)
$safeBase = $normalizedBase
if ($safeBase.Length -gt $baseMaxLength) {
$safeBase = $safeBase.Substring(0, $baseMaxLength)
}

return "$safePrefix-$safeBase-$hash"
}

$shortPrefix = "alz-short"
$shortBase = "deploy-stage-one"
$legacyExpected = "$shortPrefix-$shortBase"
$shortNameResult = New-RemoteDeterministicDeploymentStackName -DeploymentPrefix $shortPrefix -OriginalName $shortBase
Write-Host "Legacy-compatible Name: $shortNameResult"

if ($shortNameResult -ne $legacyExpected) {
throw "Validation failed: expected legacy name '$legacyExpected' when length is <= 64."
}

if ($shortNameResult -match "-[a-f0-9]{10}$") {
throw "Validation failed: hash suffix should not be added when legacy name length is <= 64."
}

$prefix = "alzrishabh2705-long-management-group-id"
$nameStageThree = "governance-landingzones-platform-deployment-stage-three"
$nameStageFour = "governance-landingzones-platform-deployment-stage-four"

$stackNameStageThree = New-RemoteDeterministicDeploymentStackName -DeploymentPrefix $prefix -OriginalName $nameStageThree
$stackNameStageFour = New-RemoteDeterministicDeploymentStackName -DeploymentPrefix $prefix -OriginalName $nameStageFour

Write-Host "Stage Three Name: $stackNameStageThree"
Write-Host "Stage Four Name: $stackNameStageFour"

if ($stackNameStageThree -eq $stackNameStageFour) {
throw "Validation failed: stage-three and stage-four produced the same deployment stack name."
}

if ($stackNameStageThree.Length -gt 64 -or $stackNameStageFour.Length -gt 64) {
throw "Validation failed: one or more deployment stack names exceeded the 64 character limit."
}

if ($stackNameStageThree -notmatch "-[a-f0-9]{10}$" -or $stackNameStageFour -notmatch "-[a-f0-9]{10}$") {
throw "Validation failed: long names should use the deterministic hash suffix."
}

$veryLongPrefix = "alzrishabh2705-super-long-management-group-prefix-with-extra-segments-over-limit"
$longPrefixName = New-RemoteDeterministicDeploymentStackName -DeploymentPrefix $veryLongPrefix -OriginalName $nameStageThree
Write-Host "Long Prefix Name: $longPrefixName"

if ($longPrefixName.Length -gt 64) {
throw "Validation failed: long-prefix deployment stack name exceeded 64 characters."
}

if ($longPrefixName -notmatch "-[a-f0-9]{10}$") {
throw "Validation failed: deployment stack name does not end with expected lowercase hash suffix."
}

$localShortName = "platform-stage-one"
$localShortNameResult = New-LocalDeterministicDeploymentStackName -DeploymentPrefix $localPrefix -OriginalName $localShortName
Write-Host "Local Legacy-compatible Name: $localShortNameResult"

if ($localShortNameResult -ne $localShortName) {
throw "Validation failed: local short deployment stack names should remain unchanged."
}

if ($localShortNameResult -match "-[a-f0-9]{10}$") {
throw "Validation failed: local short deployment stack names should not receive a hash suffix."
}

$localLongName = "governance-landingzones-platform-deployment-stage-three-with-extra-segments-over-the-azure-name-limit"
$localLongNameResult = New-LocalDeterministicDeploymentStackName -DeploymentPrefix $localPrefix -OriginalName $localLongName
Write-Host "Local Long Name: $localLongNameResult"

if ($localLongNameResult.Length -gt 64) {
throw "Validation failed: local long deployment stack name exceeded 64 characters."
}

if ($localLongNameResult -notmatch "-[a-f0-9]{10}$") {
throw "Validation failed: local long deployment stack names should use the deterministic hash suffix."
}

Write-Host "Validation passed: legacy-compatible naming is preserved and hash-suffix naming is unique and length-safe when required."
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,42 @@ steps:
# Generate deployment stack name
$intRootMgId = $env:MANAGEMENT_GROUP_ID_PREFIX + $env:INTERMEDIATE_ROOT_MANAGEMENT_GROUP_ID + $env:MANAGEMENT_GROUP_ID_POSTFIX
$deploymentPrefix = $intRootMgId
$deploymentNameBase = "$${{ parameters.name }}".Replace(" ", "-")
$deploymentNameMaxLength = 64 - $deploymentPrefix.Length - 1
if ($deploymentNameBase.Length -gt $deploymentNameMaxLength) {
$deploymentNameBase = $deploymentNameBase.Substring(0, $deploymentNameMaxLength)
$deploymentNameOriginalBase = "$${{ parameters.name }}"
if ([string]::IsNullOrWhiteSpace($deploymentNameOriginalBase)) {
$deploymentNameOriginalBase = "deployment"
}
$deploymentNameOriginalBase = $deploymentNameOriginalBase.Replace(" ", "-")

$deploymentNameHashLength = 10
$hashInput = "$deploymentPrefix|$deploymentNameOriginalBase"
$sha256 = [System.Security.Cryptography.SHA256]::Create()
try {
$hashBytes = $sha256.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($hashInput))
} finally {
$sha256.Dispose()
}
$deploymentNameSuffix = [System.BitConverter]::ToString($hashBytes).Replace("-", "").Substring(0, $deploymentNameHashLength).ToLower()

$deploymentNameLegacy = "$deploymentPrefix-$deploymentNameOriginalBase"

if ($deploymentNameLegacy.Length -le 64) {
# Preserve legacy naming to keep backward compatibility when length already fits.
$deploymentName = $deploymentNameLegacy
} else {
# Keep room for "-<base>-<hash>" and trim an oversized prefix safely.
$deploymentPrefixMaxLength = [Math]::Max(1, 64 - $deploymentNameHashLength - 3)
if ($deploymentPrefix.Length -gt $deploymentPrefixMaxLength) {
$deploymentPrefix = $deploymentPrefix.Substring(0, $deploymentPrefixMaxLength)
}

$deploymentNameBaseMaxLength = [Math]::Max(1, 64 - $deploymentPrefix.Length - $deploymentNameHashLength - 2)
$deploymentNameBase = $deploymentNameOriginalBase
if ($deploymentNameBase.Length -gt $deploymentNameBaseMaxLength) {
$deploymentNameBase = $deploymentNameBase.Substring(0, $deploymentNameBaseMaxLength)
}

$deploymentName = "$deploymentPrefix-$deploymentNameBase-$deploymentNameSuffix"
}
$deploymentName = "$deploymentPrefix-$deploymentNameBase"

$modeText = if ($whatIfEnabled) { "What-If" } else { "Deployment Stack" }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,42 @@ runs:
# Generate deployment stack name
$intRootMgId = $env:MANAGEMENT_GROUP_ID_PREFIX + $env:INTERMEDIATE_ROOT_MANAGEMENT_GROUP_ID + $env:MANAGEMENT_GROUP_ID_POSTFIX
$deploymentPrefix = $intRootMgId
$deploymentNameBase = ($env:NAME).Replace(" ", "-")
$deploymentNameMaxLength = 64 - $deploymentPrefix.Length - 1
if ($deploymentNameBase.Length -gt $deploymentNameMaxLength) {
$deploymentNameBase = $deploymentNameBase.Substring(0, $deploymentNameMaxLength)
$deploymentNameOriginalBase = $env:NAME
if ([string]::IsNullOrWhiteSpace($deploymentNameOriginalBase)) {
$deploymentNameOriginalBase = "deployment"
}
$deploymentNameOriginalBase = $deploymentNameOriginalBase.Replace(" ", "-")

$deploymentNameHashLength = 10
$hashInput = "$deploymentPrefix|$deploymentNameOriginalBase"
$sha256 = [System.Security.Cryptography.SHA256]::Create()
try {
$hashBytes = $sha256.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($hashInput))
} finally {
$sha256.Dispose()
}
$deploymentNameSuffix = [System.BitConverter]::ToString($hashBytes).Replace("-", "").Substring(0, $deploymentNameHashLength).ToLower()

$deploymentNameLegacy = "$deploymentPrefix-$deploymentNameOriginalBase"

if ($deploymentNameLegacy.Length -le 64) {
# Preserve legacy naming to keep backward compatibility when length already fits.
$deploymentName = $deploymentNameLegacy
} else {
# Keep room for "-<base>-<hash>" and trim an oversized prefix safely.
$deploymentPrefixMaxLength = [Math]::Max(1, 64 - $deploymentNameHashLength - 3)
if ($deploymentPrefix.Length -gt $deploymentPrefixMaxLength) {
$deploymentPrefix = $deploymentPrefix.Substring(0, $deploymentPrefixMaxLength)
}

$deploymentNameBaseMaxLength = [Math]::Max(1, 64 - $deploymentPrefix.Length - $deploymentNameHashLength - 2)
$deploymentNameBase = $deploymentNameOriginalBase
if ($deploymentNameBase.Length -gt $deploymentNameBaseMaxLength) {
$deploymentNameBase = $deploymentNameBase.Substring(0, $deploymentNameBaseMaxLength)
}

$deploymentName = "$deploymentPrefix-$deploymentNameBase-$deploymentNameSuffix"
}
$deploymentName = "$deploymentPrefix-$deploymentNameBase"

$modeText = if ($whatIfEnabled) { "What-If" } else { "Deployment Stack" }

Expand Down
37 changes: 36 additions & 1 deletion alz/local/scripts-bicep/bicep-deploy.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,42 @@ Write-Host "Resource Group Name: $resourceGroupName" -ForegroundColor DarkGray
Write-Host "Location: $location" -ForegroundColor DarkGray
Write-Host "Deployment Type: $deploymentType" -ForegroundColor DarkGray

$deploymentName = $name
$deploymentPrefix = $intRootMgId
$deploymentNameOriginalBase = $name
if ([string]::IsNullOrWhiteSpace($deploymentNameOriginalBase)) {
$deploymentNameOriginalBase = "deployment"
}
$deploymentNameOriginalBase = $deploymentNameOriginalBase.Replace(" ", "-")
$deploymentNameHashLength = 10
$hashInput = "$deploymentPrefix|$deploymentNameOriginalBase"
$sha256 = [System.Security.Cryptography.SHA256]::Create()
try {
$hashBytes = $sha256.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($hashInput))
} finally {
$sha256.Dispose()
}
$deploymentNameSuffix = [System.BitConverter]::ToString($hashBytes).Replace("-", "").Substring(0, $deploymentNameHashLength).ToLower()

$deploymentNameLegacy = $deploymentNameOriginalBase

if ($deploymentNameLegacy.Length -le 64) {
# Preserve legacy local naming to keep backward compatibility when length already fits.
$deploymentName = $deploymentNameLegacy
} else {
# Keep room for "-<base>-<hash>" and trim an oversized prefix safely.
$deploymentPrefixMaxLength = [Math]::Max(1, 64 - $deploymentNameHashLength - 3)
if ($deploymentPrefix.Length -gt $deploymentPrefixMaxLength) {
$deploymentPrefix = $deploymentPrefix.Substring(0, $deploymentPrefixMaxLength)
}

$deploymentNameBaseMaxLength = [Math]::Max(1, 64 - $deploymentPrefix.Length - $deploymentNameHashLength - 2)
$deploymentNameBase = $deploymentNameOriginalBase
if ($deploymentNameBase.Length -gt $deploymentNameBaseMaxLength) {
$deploymentNameBase = $deploymentNameBase.Substring(0, $deploymentNameBaseMaxLength)
}

$deploymentName = "$deploymentPrefix-$deploymentNameBase-$deploymentNameSuffix"
}
Write-Host "Deployment Stack Name: $deploymentName"
Write-Host ""

Expand Down
Loading