From be23b53784b6c120b73f0b94f67491448e305acb Mon Sep 17 00:00:00 2001 From: Travis Bader Date: Wed, 20 May 2026 20:36:55 -0600 Subject: [PATCH] Make nuget-config replace only packageSources Previously the convention replaced the entire file, which destroyed repo-specific packageSourceMapping entries. The new behavior uses XmlDocument to surgically replace only the packageSources element, leaving all other sections (packageSourceMapping, activePackageSource, etc.) intact. Also adds and the Faithlife Azure feed to the published template so repos start with the correct baseline. --- conventions/nuget-config/README.md | 8 +- conventions/nuget-config/convention.Tests.ps1 | 33 ++++++-- conventions/nuget-config/convention.ps1 | 75 +++++++++++++++++-- conventions/nuget-config/files/nuget.config | 2 + 4 files changed, 100 insertions(+), 18 deletions(-) diff --git a/conventions/nuget-config/README.md b/conventions/nuget-config/README.md index 20a3494..056a954 100644 --- a/conventions/nuget-config/README.md +++ b/conventions/nuget-config/README.md @@ -1,12 +1,14 @@ # nuget-config -Creates or replaces the repository-root `nuget.config` from [files/nuget.config](./files/nuget.config). +Creates or updates the repository-root `nuget.config` to ensure it uses the standard package sources. ## Behavior -The published `nuget.config` matches the RepoConventions version except that it omits the `protocolVersion` attribute. +When no `nuget.config` exists, the convention creates one from [files/nuget.config](./files/nuget.config), which configures `nuget.org` and the Faithlife Azure Artifacts feed as the standard package sources. -If the repository already has a root `nuget.config` and it does not match exactly, the convention replaces it with the published file. +When a `nuget.config` already exists, the convention replaces only the `` element to match the published template. All other sections — including `` and `` — are preserved. This allows each repository to maintain its own package source mappings, which vary by the private packages each project consumes. + +If the file exists but is not valid XML, or does not contain a `` element, the convention throws an error. ## Example diff --git a/conventions/nuget-config/convention.Tests.ps1 b/conventions/nuget-config/convention.Tests.ps1 index b92906a..bd9701a 100644 --- a/conventions/nuget-config/convention.Tests.ps1 +++ b/conventions/nuget-config/convention.Tests.ps1 @@ -95,11 +95,11 @@ Describe 'nuget-config convention' { } } - It 'replaces an existing different nuget.config' { + It 'replaces packageSources and preserves all other sections' { $testDirectory = New-TemporaryDirectory try { - # Arrange a repository with a committed divergent NuGet config. + # Arrange a config like LogosCompilerService— custom mappings, old protocolVersion. Initialize-TestRepository -Path $testDirectory $nuGetConfigPath = Join-Path $testDirectory 'nuget.config' $existingContent = @" @@ -107,7 +107,19 @@ Describe 'nuget-config convention' { + + + + + + + + + + + + "@ [System.IO.File]::WriteAllText($nuGetConfigPath, $existingContent, $utf8) @@ -121,15 +133,20 @@ Describe 'nuget-config convention' { Pop-Location } - # Apply the convention and collect the modified config state. + # Apply the convention and inspect the result. $output = InvokeNuGetConfigConvention -TestDirectory $testDirectory $status = @(Get-GitStatusLines -TestDirectory $testDirectory) - - # Assert the config was replaced with the published file. - (Get-Content -LiteralPath $nuGetConfigPath -Raw) | Should -Be (Get-Content -LiteralPath $expectedNuGetConfigPath -Raw) - $status.Count | Should -Be 1 + $updatedDoc = [System.Xml.XmlDocument]::new() + $updatedDoc.LoadXml((Get-Content -LiteralPath $nuGetConfigPath -Raw)) + + # Assert packageSources updated (clear added, no protocolVersion) and mapping preserved. + ($null -ne $updatedDoc.DocumentElement.SelectSingleNode('packageSources/clear')) | Should -Be $true + ($null -eq $updatedDoc.DocumentElement.SelectSingleNode('packageSources/add[@key="nuget.org"]/@protocolVersion')) | Should -Be $true + $azurePatterns = @($updatedDoc.DocumentElement.SelectNodes('packageSourceMapping/packageSource[@key="Faithlife Azure"]/package') | ForEach-Object { $_.GetAttribute('pattern') }) + $azurePatterns | Should -Contain 'OrdersApi.*' + $azurePatterns | Should -Contain 'RoyaltyApi.*' $status[0] | Should -Match '^ M nuget\.config$' - (@($output | ForEach-Object { $_.ToString() }) -contains "Replaced '$nuGetConfigPath' with the published NuGet config.") | Should -Be $true + (@($output | ForEach-Object { $_.ToString() }) -contains "Updated package sources in '$nuGetConfigPath'.") | Should -Be $true } finally { Remove-Item -LiteralPath $testDirectory -Recurse -Force diff --git a/conventions/nuget-config/convention.ps1 b/conventions/nuget-config/convention.ps1 index 62af6f8..8bdf1c2 100644 --- a/conventions/nuget-config/convention.ps1 +++ b/conventions/nuget-config/convention.ps1 @@ -56,16 +56,77 @@ if ($existingNuGetConfigItem.Name -cne 'nuget.config') { $existingNuGetConfigItem = Get-Item -LiteralPath $targetNuGetConfigPath } -# Exit when the target already matches the published template. -if (Test-FileContentMatches -ExpectedPath $sourceNuGetConfigPath -ActualPath $targetNuGetConfigPath) { +# Load the published package sources and the existing NuGet config as XML documents. +$sourceDoc = [System.Xml.XmlDocument]::new() +$sourceDoc.Load($sourceNuGetConfigPath) +$sourcePackageSources = $sourceDoc.DocumentElement.SelectSingleNode('packageSources') + +$targetContent = [System.IO.File]::ReadAllText($targetNuGetConfigPath) +$targetDoc = [System.Xml.XmlDocument]::new() + +try { + $targetDoc.LoadXml($targetContent) +} +catch { + throw "Cannot update '$targetNuGetConfigPath' because it is not valid XML: $_" +} + +$targetPackageSources = $targetDoc.DocumentElement.SelectSingleNode('packageSources') + +if ($null -eq $targetPackageSources) { + throw "Cannot update '$targetNuGetConfigPath' because it does not contain a element." +} + +# Serialize a packageSources node to a canonical string for comparison. +function ConvertPackageSourcesToString { + param( + [Parameter(Mandatory = $true)] + [System.Xml.XmlNode] $Node + ) + + $stringWriter = [System.IO.StringWriter]::new() + $settings = [System.Xml.XmlWriterSettings]::new() + $settings.Indent = $true + $settings.IndentChars = ' ' + $settings.OmitXmlDeclaration = $true + $settings.NewLineChars = "`n" + $settings.NewLineHandling = [System.Xml.NewLineHandling]::Replace + + $writer = [System.Xml.XmlWriter]::Create($stringWriter, $settings) + $Node.WriteTo($writer) + $writer.Flush() + return $stringWriter.ToString() +} + +# Exit when the existing packageSources already matches the published template. +$sourcePackageSourcesText = ConvertPackageSourcesToString -Node $sourcePackageSources +$targetPackageSourcesText = ConvertPackageSourcesToString -Node $targetPackageSources + +if ($sourcePackageSourcesText -ceq $targetPackageSourcesText) { return } -# Replace stale NuGet config content with the published template. -$copyResult = Copy-FileIfDifferent -SourcePath $sourceNuGetConfigPath -DestinationPath $targetNuGetConfigPath +# Replace only the packageSources element, preserving all other sections. +$importedPackageSources = $targetDoc.ImportNode($sourcePackageSources, $true) +$targetDoc.DocumentElement.ReplaceChild($importedPackageSources, $targetPackageSources) | Out-Null + +$xmlSettings = [System.Xml.XmlWriterSettings]::new() +$xmlSettings.Indent = $true +$xmlSettings.IndentChars = ' ' +$xmlSettings.Encoding = $utf8 +$xmlSettings.NewLineChars = "`n" +$xmlSettings.NewLineHandling = [System.Xml.NewLineHandling]::Replace -if (-not $copyResult.Updated) { - throw "Expected '$targetNuGetConfigPath' to be replaced." +# Write the updated document. +$stream = [System.IO.MemoryStream]::new() +$xmlWriter = [System.Xml.XmlWriter]::Create($stream, $xmlSettings) +$targetDoc.Save($xmlWriter) +$xmlWriter.Flush() +$newContent = $utf8.GetString($stream.ToArray()) + +if ($newContent -ceq $targetContent) { + return } -Write-Host "Replaced '$targetNuGetConfigPath' with the published NuGet config." +[System.IO.File]::WriteAllText($targetNuGetConfigPath, $newContent, $utf8) +Write-Host "Updated package sources in '$targetNuGetConfigPath'." diff --git a/conventions/nuget-config/files/nuget.config b/conventions/nuget-config/files/nuget.config index 95e879e..36a5bf7 100644 --- a/conventions/nuget-config/files/nuget.config +++ b/conventions/nuget-config/files/nuget.config @@ -1,6 +1,8 @@ + +