Created
March 11, 2025 12:05
-
-
Save chrisfcarroll/527c24a58fa240322c8ce918e56dee75 to your computer and use it in GitHub Desktop.
Dev Azure Release Pipeline Web.*.config Variable Transform PowerShell Script
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# ----------------------------------------------------- | |
# VARIABLE SUBSTITUTION | |
# ----------------------------------------------------- | |
# Replace strings like "$(VariableName)" in config files | |
# with the variable value found in the pipeline Environment | |
# ----------------------------------------------------- | |
# For TESTing: to load all the functions definitions but not | |
# run the main script, set $NoRun=$true before calling the script | |
# ----------------------------------------------------- | |
# FILES TO BE PROCESSED: | |
# The first zipfile found in $(Release.PrimaryArtifactSourceAlias) | |
$script:zipfile= ( Get-ChildItem "$(Release.PrimaryArtifactSourceAlias)\*.zip" -recurse -ErrorAction Stop | | |
Select-Object -First 1 ) | |
# will be expanded and searched for files matching the pattern: | |
$script:includeFiles= "Web.config","Web.$env:RELEASE_ENVIRONMENTNAME.config" | |
# ----------------------------------------------------- | |
# NB: Pipeline variable names with "." in them get turned into | |
# environment variable names with "_" in them. In the TextTransform | |
# step, we replace **both** forms of the variable name. | |
# ----------------------------------------------------- | |
# Editing this script: | |
# Make sure you know both pipeline variable substitution and | |
# powershell variable substitution and, when to escape `$`() | |
function GetProcessableVariablesFromEnvironment { | |
Invoke-Expression "Get-ChildItem 'env:*'" | |
} | |
function ShowDebuggingInformation { | |
"_____________" | |
"Variables To Inject:" | |
GetProcessableVariablesFromEnvironment | | |
ForEach-Object { $_.Name + "=" + $_.Value } | |
"_____________" | |
Get-ChildItem "$(Release.PrimaryArtifactSourceAlias)\*.zip" -recurse | |
"_____________" | |
"Zipped Artifact to Expand: $($zipfile.FullName)" | |
$zipfile | |
"_____________" | |
"Environment:RELEASE_ENVIRONMENTNAME:" | |
$env:RELEASE_ENVIRONMENTNAME | Sort-Object | |
"_____________" | |
"ConfigFiles To Edit:" | |
$configFiles | ForEach-Object { $_.FullName } | |
"_____________" | |
} | |
function TextTransform( [string]$content, $replacements, $fileName) { | |
$newContent=$content | |
foreach($nameValue in $replacements ) { | |
if($null -eq $nameValue.Name){ | |
Write-Debug "TextTransform:`$replacements contained a null name" | |
continue | |
} | |
$search1= "`$(" + $nameValue.Name + ")" | |
$search2= "`$(" + $nameValue.Name.Replace("_",".") + ")" | |
# $searchDebug= "_Someuniquedebuggablestring_" | |
if( $content -notlike "*$search1*" -and $content -notlike "*$search2*" ){ | |
Write-Debug "$fileName`: doesnt contain $search2" | |
continue | |
} | |
Write-Host "$fileName`: replacing $search2 with $($nameValue.Value)" | |
$newContent = $newContent -replace [regex]::Escape($search1), $nameValue.Value | |
$newContent = $newContent -replace [regex]::Escape($search2), $nameValue.Value | |
} | |
Write-Verbose $newContent | |
return $newContent | |
} | |
function TransformFiles($files) { | |
foreach($file in $files){ | |
$content= Get-Content $file -Raw | |
$replacements= GetProcessableVariablesFromEnvironment | |
Write-Host "Transforming $file ..." | |
$newContent= TextTransform $content $replacements $file | |
if($newContent.Trim() -eq $content.Trim()){ | |
Write-Host "... nothing changed." | |
$file | |
} | |
else{ | |
Write-Host "... saving changes to $($file.FullName)" | |
} | |
Set-Content -Path $file.FullName -Value $newContent.Trim() -Encoding UTF8 | |
} | |
} | |
function ExpandArtifactZipToTempExpandedFolder($zipFile, $target="TempExpanded") { | |
if($target -match ":|%"){ | |
throw "Target path must be a subfolder of the current directory" | |
} | |
Remove-Item -Recurse $target -ErrorAction SilentlyContinue | |
MkDir $target | |
Expand-Archive $zipfile -DestinationPath $target -Force | |
} | |
function Get-ConfigFilesInExpandedFolder($expandedFolder="TempExpanded"){ | |
Get-ChildItem $expandedFolder -Recurse -Include $includeFiles | |
} | |
function BackupOriginalZipFile($zipfile) { | |
$backupfileName= $zipfile.FullName -replace ".zip$",".bak.zip" | |
if(Test-Path $backupfileName){ | |
Rename-Item $backupfileName ($backupfileName.FullName -replace ".zip$",".bak.zip") -Force | |
} | |
Rename-Item $zipfile $backupfileName | |
} | |
function BackupAndReCreateArtifactZipFile($zipfile) { | |
BackupOriginalZipFile $zipfile | |
Compress-Archive -Path * -DestinationPath $zipfile -Force | |
} | |
# ----------------------------------------------------- | |
if($NoRun){ return } | |
# ----------------------------------------------------- | |
# And now, run the script | |
# | |
ExpandArtifactZipToTempExpandedFolder $zipFile | |
$script:configFiles= Get-ConfigFilesInExpandedFolder | |
ShowDebuggingInformation | |
if($configFiles.Count -eq 0){ | |
Write-Debug "No files to process" | |
return | |
} | |
Push-Location TempExpanded | |
try | |
{ | |
TransformFiles $configFiles | |
BackupAndReCreateArtifactZipFile $zipfile | |
} | |
finally { Pop-Location } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using namespace System.Diagnostics | |
using namespace System.Collections | |
using namespace System.Collections.Generic | |
using namespace System.IO.Compression | |
param ( [switch]$NoRun ) | |
function Assert( [bool]$condition, [string]$message = "Assertion failed" ){ | |
if( -not $condition ) { | |
Get-PSCallStack | ForEach-Object { Write-Host $_.FunctionName $_.Location $_.Arguments } | |
throw $message | |
} | |
} | |
function CreateExampleConfigs { | |
Write-Warning "Creating test files ExampleConfigs\Web.config and Web.UnitTest.config..." | |
Write-Host "Creating test files ExampleConfigs\Web.config and Web.UnitTest.config..." | |
mkdir ExampleConfigs -ErrorAction SilentlyContinue | |
@' | |
<?xml version="1.0" encoding="utf-8"?> | |
<configuration> | |
<someSetting name="someSetting" from="Web.config" to="OriginalValueHere" subject="Test variable substitution" /> | |
<appSettings> | |
<add key="TestKey" value="TestValue" /> | |
</appSettings> | |
</configuration> | |
'@ | Out-File .\ExampleConfigs\Web.config | |
@' | |
<?xml version="1.0" encoding="utf-8"?> | |
<configuration> | |
<someSetting name="someSetting" from="Web.UnitTest.config" to="$(SomeSetting.EmailTo)" subject="Test variable substitution" /> | |
<appSettings> | |
<add key="TestKey" value="TestValue" /> | |
</appSettings> | |
</configuration> | |
'@ | Out-File .\ExampleConfigs\Web.UnitTest.config | |
} | |
function Setup { | |
$env:SomeSetting_EmailTo = "[email protected]" | |
$env:RELEASE_ENVIRONMENTNAME = "UnitTest" | |
if (-not (Test-Path ExampleConfigs\Web.config) -or -not (Test-Path ExampleConfigs\Web.UnitTest.config)) { | |
CreateExampleConfigs | |
} | |
Remove-Item TempExpanded -Recurse -ErrorAction SilentlyContinue | |
$primaryArtifactSourceAlias = "TestArtifactSourceAlias" | |
function Release.PrimaryArtifactSourceAlias { $primaryArtifactSourceAlias } | |
if (Test-Path $primaryArtifactSourceAlias\Drop.bak.zip) { | |
Remove-Item $primaryArtifactSourceAlias\Drop.zip -ErrorAction SilentlyContinue | |
Rename-Item $primaryArtifactSourceAlias\Drop.bak.zip Drop.zip | |
} | |
else { | |
mkdir $primaryArtifactSourceAlias -ErrorAction SilentlyContinue | |
Compress-Archive ExampleConfigs $primaryArtifactSourceAlias\drop.zip -Force | |
} | |
} | |
function TestTextTransform { | |
$content = " to=""`$(SomeSetting.EmailTo)"" subject=""ELMAH [TEST]""" | |
$replacements = @( @{"SomeSetting_EmailTo"="Replaced"}.GetEnumerator() ) | |
$expected = " to=""Replaced"" subject=""ELMAH [TEST]""" | |
$actual = TextTransform $content $replacements "[TestTextTransform]" | |
Assert ($actual -eq $expected) "Failed matching case Replace: Expected: $expected, Actual: $actual" | |
"TestTextTransform:With Exact Case Match:Passed" | |
$replacementsUpperCase = @( @{"SomeSetting_EmailTo"="Replaced"}.GetEnumerator() ) | |
$actual2 = TextTransform $content $replacementsUpperCase "[TestTextTransform Case Insensitive]" | |
Assert ($actual2 -eq $expected) "Failed Case Insensitive Replace: Expected: $expected, Actual: $actual2" | |
"TestTextTransform:Case Insensitive:Passed" | |
} | |
function TestTransformFile { | |
#Arrange | |
mkdir ExampleConfigsCopy -ErrorAction SilentlyContinue | |
try{ | |
#Arrange | |
Copy-Item ExampleConfigs\Web.config , .\ExampleConfigs\Web.UnitTest.config ExampleConfigsCopy | |
$files= Get-ChildItem ExampleConfigsCopy\* | |
$original0 = Get-Content ExampleConfigsCopy\Web.UnitTest.config -Raw | |
# Act | |
TransformFiles $files | |
# Assert | |
$actual0 = Get-Content ExampleConfigsCopy\Web.UnitTest.config -Raw | |
Assert ($original0 -ne $actual0) "Original: $original0, Actual: $actual0" | |
Assert ($actual0 -match $env:SomeSetting_EmailTo) "$files should contain `$env:SomeSetting_EmailTo = $env:SomeSetting_EmailTo but doesn't" | |
$appSettingsNode= (Select-Xml -Path $files[0] -XPath "/configuration/appSettings"). | |
Node | |
Assert ($null -ne $appSettingsNode) "Failed to parse appSettings note in $files[0] after transform, got null for /configuration/appSettings" | |
"TestTranformFiles: Passed" | |
} | |
finally { Remove-Item .\ExampleConfigsCopy -Recurse -ErrorAction SilentlyContinue } | |
} | |
function TestExpandAndRecreateArtifactZipFile { | |
$artifactZipPath = "$primaryArtifactSourceAlias\drop.zip" | |
$backupfileName = $artifactZipPath -replace ".zip$",".bak.zip" | |
ExpandArtifactZipToTempExpandedFolder $artifactZipPath | |
Push-Location TempExpanded | |
try | |
{ | |
BackupAndReCreateArtifactZipFile $zipfile | |
} | |
finally { Pop-Location } | |
Assert (Test-Path $artifactZipPath) "Expected replacement zip file $artifactZipPath not found" | |
Assert (Test-Path $backupfileName) "Expected Backup file $backupfileName not found" | |
"TestExpandAndRecreateArtifactZipFile:Zip file and bak.zip file exist:Passed" | |
"TestExpandAndRecreateArtifactZipFile:Compare: $artifactZipPath, $backupfileName" | |
# Resolve-Path $artifactZipPath | |
# Resolve-Path $backupfileName | |
# Get-Location | |
$zip1 = [ZipFile]::OpenRead( (Resolve-Path $artifactZipPath) ) | |
$zip2 = [ZipFile]::OpenRead( (Resolve-Path $backupfileName) ) | |
$toTuple = [Func[ZipArchiveEntry, ZipArchiveEntry, [Tuple[ZipArchiveEntry,ZipArchiveEntry]]]]{ | |
param ($entry1, $entry2) | |
return [tuple]::Create( $entry1, $entry2 ) | |
} | |
$dirEntries= [System.Linq.Enumerable]::Zip($zip1.Entries, $zip2.Entries, $toTuple ) | |
$zip1.Dispose() | |
$zip2.Dispose() | |
$dirEntries | ForEach-Object { | |
$entry1 = [ZipArchiveEntry]$_.Item1 | |
$entry2 = [ZipArchiveEntry]$_.Item2 | |
Assert ($entry1.FullName -eq $entry2.FullName) "Files in $artifactZipPath and $primaryArtifactSourceAlias\drop.bak.zip do not match" | |
Assert ($entry1.Length -eq $entry2.Length) "Files in $artifactZipPath and $primaryArtifactSourceAlias\drop.bak.zip do not match" | |
} | |
"TestExpandAndRecreateArtifactZipFile:Zip file and bak.zip file have same directory entries:Passed" | |
} | |
function TestGetConfigFilesToEdit { | |
"Expected:" | |
Get-ChildItem TempExpanded -Recurse -Include "Web.config","Web.$env:RELEASE_ENVIRONMENTNAME.config" | |
$actual = Get-ConfigFilesInExpandedFolder | |
Assert ($null -ne $actual) "Expected `$actual to be set, but found null" | |
Assert ($actual.Count -eq 2) "Expected to transform exactly 2 config files, but found:`n----------`n$($actual)`n---------" | |
$configFilesToTransformIncludesWebConfig = $actual -match "TempExpanded(\\|/).+(\\|/)Web.config$" | |
Assert $configFilesToTransformIncludesWebConfig.Count "Expected to transform web.config file but found:`n----------`n$($actual)`n---------" | |
$configFilesToTransformIncludesWebTestConfig = $actual -match "TempExpanded(\\|/).+(\\|/)Web.$env:RELEASE_ENVIRONMENTNAME.config$" | |
Assert $configFilesToTransformIncludesWebTestConfig.Count "Expected to transform web.$env:RELEASE_ENVIRONMENTNAME.config file but found:`n----------`n$($actual)`n---------" | |
"TestGetConfigFilesToEdit: Passed" | |
} | |
# ----------------------------------------------------- | |
# | |
if($NoRun){ return } | |
# ----------------------------------------------------- | |
$DebugPreferenceWas = $DebugPreference | |
$DebugPreference = "Continue" | |
try{ | |
. Setup | |
$noRun=$true ; . .\DevAzureReleaseVariableTransform.ps1 | |
ShowDebuggingInformation | |
TestTextTransform | |
TestTransformFile | |
TestExpandAndRecreateArtifactZipFile | |
TestGetConfigFilesToEdit | |
} | |
finally { $DebugPreference = $DebugPreferenceWas } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Usage
Contents
A powershell script for variable substitution in Dev Azure release pipelines. It will:
$(VariableName)
or$(VariableDottedName)
with the value of the environment variableSetting.Name
for an environment variable calledSetting_Name
No clean-up is done.
Testing
Test the transforms locally by running the .Test.ps1 test file