Skip to content
Closed
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
8 changes: 5 additions & 3 deletions conventions/nuget-config/README.md
Original file line number Diff line number Diff line change
@@ -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 `<packageSources>` element to match the published template. All other sections — including `<packageSourceMapping>` and `<activePackageSource>` — 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 `<packageSources>` element, the convention throws an error.

## Example

Expand Down
33 changes: 25 additions & 8 deletions conventions/nuget-config/convention.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -95,19 +95,31 @@ 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 = @"
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
<add key="Faithlife Azure" value="https://pkgs.dev.azure.com/Faithlife/Packages/_packaging/main/nuget/v3/index.json" protocolVersion="3" />
</packageSources>
<packageSourceMapping>
<packageSource key="nuget.org">
<package pattern="*" />
</packageSource>
<packageSource key="Faithlife Azure">
<package pattern="Argus.*" />
<package pattern="Faithlife.*" />
<package pattern="OrdersApi.*" />
<package pattern="RoyaltyApi.*" />
</packageSource>
</packageSourceMapping>
</configuration>
"@
[System.IO.File]::WriteAllText($nuGetConfigPath, $existingContent, $utf8)
Expand All @@ -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
Expand Down
75 changes: 68 additions & 7 deletions conventions/nuget-config/convention.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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 <packageSources> 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'."
2 changes: 2 additions & 0 deletions conventions/nuget-config/files/nuget.config
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's recommendation:

Without , NuGet inherits any machine-level or user-level package sources configured in global NuGet.config files. This can cause builds to resolve packages from unexpected sources, making builds non-reproducible across machines. ensures only the explicitly listed sources are used.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hate this because I can't use local package feeds. Strongly opposed.

<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
<add key="Faithlife Azure" value="https://pkgs.dev.azure.com/Faithlife/Packages/_packaging/main/nuget/v3/index.json" />
</packageSources>
</configuration>
Loading