Web.Config Transformation with GitHub Actions using PowerShell
Introduction
While you are dealing with Microsoft .NET framework, managing different configurations for various environments — such as development, testing, and production — is crucial for maintaining application integrity and performance. One widely used method for handling these variations in .NET Framework applications is through web.config transformation. This article discusses how to implement web.config transformations in a CI/CD pipeline using GitHub Actions, focusing on a sample workflow during the release phase.
Quick Overview
As part of this article, I will utilize a script that transforms your web.config file based on your environment.
Why Use the Script Instead of Transformation Extensions?
1. Organizational Security Policies
Many organizations implement strict security policies that prohibit the installation of extensions or third-party libraries on hosted machines. This is often due to concerns about:
- Security Risks: Installing unverified or untrusted extensions can introduce vulnerabilities, leading to potential exploitation by malicious actors.
- Operational Consistency: By avoiding extensions, organizations can maintain consistent environments across their infrastructure, ensuring that all configurations and processes align with internal security standards.
2. Controlled Execution Environment
Using a script-based approach allows for more granular control over the transformation process. The reasons include:
- Reduced Dependencies: By directly referencing the
Microsoft.Web.XmlTransform.dll
and executing transformations in a controlled manner, you minimize reliance on external packages or extensions, reducing the risk of version conflicts and compatibility issues. - Enhanced Debugging: Custom scripts can include logging and error-handling mechanisms that provide better visibility into the transformation process. This can be crucial for troubleshooting and ensuring that the transformations are applied correctly.
- Flexibility: Scripts can be easily modified to include additional logic, error handling, or alternative transformation strategies as the project evolves.
Why Use Web.Config Transformation?
- Environment-Specific Settings: Different environments often require unique settings (like connection strings, API keys, and feature flags). Transformations allow developers to maintain a single source configuration file while providing environment-specific overrides.
- Automated Deployment: Automated transformations ensure that the right configuration is applied during the deployment process, reducing the risk of human error and streamlining the deployment workflow.
- Ease of Management: Using transformation files (like
Web.Test.config
) simplifies managing configurations. Developers can easily see the changes that will be applied to the base configuration, enhancing clarity and maintainability.
How to achieve this?
Now let’s check how to achieve this using GitHub Actions workflows.
As part of my old Microsoft .NET Framework application, I built and packaged it using MSBuild during the build stage. Once your artifact is packaged, you are ready to transform the web.config file. The goal is to merge the changes from Web.Test.config
(based on your environment) back into web.config
.
1. Downloading the NuGet Package
In my release stage, I download the latest Microsoft.Web.Xdt.nupkg
from NuGet. I convert the NuGet package to a .zip
format and extract all the files, which include the important .dll
files that will be used in the next step..
jobs:
download-package:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Download NuGet Package
run: |
wget https://www.nuget.org/api/v2/package/Microsoft.Web.Xdt/7.0.0-preview.22423.2 -O Microsoft.Web.Xdt.nupkg
mv Microsoft.Web.Xdt.nupkg Microsoft.Web.Xdt.zip
mkdir extracted_package
unzip Microsoft.Web.Xdt.zip -d extracted_package/
- name: Upload Extracted Package
uses: actions/upload-artifact@v3
with:
name: extracted-package
path: extracted_package/
2. Downloading the Extracted Package
In the next action block, I download the extracted package:
deploy:
runs-on: [self-hosted, Windows, xxxx]
needs: download-package
if: github.ref == 'refs/heads/master'
environment: Prod
steps:
- name: Download Extracted Package
uses: actions/download-artifact@v3
with:
name: extracted-package
path: ${{ github.workspace }}/extracted_package
3. Applying the Transformation
Next, I apply the transformation to your web.config. For this, I used the following PowerShell script, where you define your environment-specific config file (such as Web.Test.config
) and the actual location of the web.config
file that is going to be changed.
- name: Apply Web.config Transformation
shell: powershell
run: |
$RepoFilePath = "D:\DATA\Web\Host1\Pub\Testfolder\unzipped\Content\D_C\a\projectname\obj\Release\Package\PackageTmp\Web.Test.config"
$originalFilePath = "D:\DATA\Web\Host1\Pub\Web.config"
$dllPath = "${{ github.workspace }}/extracted_package/lib/netstandard2.0/Microsoft.Web.XmlTransform.dll"
Unblock-File -Path $dllPath
Add-Type -Path $dllPath
if (([string]::IsNullOrEmpty($originalFilePath)) -Or (-Not (Test-Path $originalFilePath))) {
throw "Base file not found: $originalFilePath"
}
if (([string]::IsNullOrEmpty($RepoFilePath)) -Or (-Not (Test-Path $RepoFilePath))) {
throw "Transform file not found: $RepoFilePath"
}
$xmlTransformableDoc = New-Object Microsoft.Web.XmlTransform.XmlTransformableDocument
$xmlTransformableDoc.PreserveWhitespace = $true
$xmlTransformableDoc.Load($originalFilePath)
$xmlTransformation = New-Object Microsoft.Web.XmlTransform.XmlTransformation($RepoFilePath)
if ($xmlTransformation.Apply($xmlTransformableDoc) -eq $false) {
throw "error."
}
$xmlTransformableDoc.Save($originalFilePath)
Output
Here is my web.config before transformation in my Test environment. Similar way can be achieved for Production environment as well.
Before Transformation:
After transformation
Meanwhile I have tried other approach as well where you dont have to use that nuget package download step rather you can use this step directly in your main workflow for transformation.
- name: Transform Web.config for ${{ inputs.environment }} Environment
run: |
$transformFile = "D:\DATA\Web\Host1\Pub\Testfolder\unzipped\Content\D_C\a\projectname\obj\Release\Package\PackageTmp\Web.Test.config"
$webConfig = "D:\DATA\Web\Host1\Pub\Web.config"
if (Test-Path -Path $transformFile) {
try {
[xml]$xml = Get-Content $webConfig
} catch {
exit
}
try {
[xml]$transform = Get-Content $transformFile
} catch {
exit
}
$transformAppSettings = $transform.SelectNodes("//appSettings/add")
foreach ($transformNode in $transformAppSettings) {
$key = $transformNode.GetAttribute("key")
$value = $transformNode.GetAttribute("value")
$configNode = $xml.SelectSingleNode("//appSettings/add[@key='$key']")
if ($configNode -ne $null) {
$configNode.SetAttribute("value", $value)
} else {
Write-Host "Adding $key to web.config with value '$value'..."
$newNode = $xml.CreateElement("add")
$newNode.SetAttribute("key", $key)
$newNode.SetAttribute("value", $value)
$xml.SelectSingleNode("//appSettings").AppendChild($newNode)
}
}
try {
$xml.Save($webConfig)
} catch {
Write-Host "Error saving Web.config: $_"
}
} else {
Write-Error "Web.Test.config not found for transformation."
}
shell: powershell
Happy reading, feel free to share your comments.