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 @@ + +