How to manage NuGet package sources in open-source projects

Managing multiple NuGet feeds just became easy

How to manage NuGet package sources in open-source projects

I love contributing to open-source projects. There's something satisfying about building tools that anyone can use and improve.

But every now and then, I hit a peculiar problem that sits right at the intersection of open source ideals. This is one of those problems.

Suppose you've tried contributing to an open-source project that configured private NuGet feeds for internal builds. In that case, you know the tension: External contributors can't access those feeds, but the DevOps pipeline created needs them to restore.

I spent a few walks thinking about this one. How do you keep a project truly open? But at the same time, buildable by anyone while still using private dependencies if run through an internal infrastructure?

The answer turned out to be simpler than I thought. It only took some trial and error to get there.

Here's what I learned.

The problem: When open source meets private feeds

The thing about open-source projects should be straightforward: they're meant to be built by anyone and anywhere. Clone it, restore it, and build it to run it. That should be the promise.

But with recent supply chain attacks, many open-source projects have introduced an internal feed to their DevOps pipelines. These feeds aren't available for public consumption and require authentication nine out of ten times.

That brings a challenge. You can't expect external contributors to have access to those private feeds by default. It totally defeats the whole purpose of being open source.

I encountered this while working on a project that had a private Azure DevOps feed for those internal dependencies. The contributors already did a fantastic job by providing a build automation script (build.ps1). But triggering this script instantly gave me the error message:

The plugin credential provider could not acquire credentials. Authentication may require manual action. Consider re-running t 
he command with --interactive for `dotnet`, /p:NuGetInteractive="true" for MSBuild or removing the -NonInteractive switch for `NuGet`

When I tried to add both NuGet's public feed and the private one in the nuget.config file, I was presented with MSBuild's strict mode:

When using central package management, please map your package sources with package source mapping

The problem was clear: Contributors shouldn't need credentials to build an open-source project, regardless.

Enter package source mapping

The clue was already given. Map the package sources with package source mapping in the nuget.config file. .NET 6 introduced this mapping, telling NuGet: "This package comes from here, that packages come from there."

What you can now do is define the source mapping for each pattern you want it to follow:

<packageSourceMapping>
  <packageSource key="NuGet">
    <package pattern="*" />
  </packageSource>
  <packageSource key="OtherFeed">
    <package pattern="CustomPackage.*" />
  </packageSource>
</packageSourceMapping>

This tells NuGet: "Hey, if you find a package with CustomPackage in the name, you need to fetch it from there. Everything else can go through NuGet's feed."

But did it solve the two issues? Not fully, because there was another problem. The private feed.

The solution: Split your configs

This is where it gets interesting. What if we could simply use different NuGet configs for different scenarios?

Most CI/CD system runners have predefined variables. These are populated when a task runs. For example, Azure DevOps has a known variable TF_BUILD that is set when a build task runs a script.

Knowing this allows you to split your nuget.config files in two: one for the public builds and the other for private builds:

<packageSources>
  <add key="NuGet" value="https://api.nuget.org/v3/index.json" />
  <add key="OtherFeed" value="https://otherfeed.com/v3/index.json" />
</packageSources>
<packageSourceMapping>
  <packageSource key="NuGet">
    <package pattern="*" />
  </packageSource>
  <packageSource key="OtherFeed">
    <package pattern="CustomPackage.*" />
  </packageSource>
</packageSourceMapping>

Then, you can create a nuget.internal.config file used only for internal builds:

<packageSources>
  <add key="Packages" value="https://pkgs.dev.azure.com/<organizationName>/<artifactFeedName>/..." />
</packageSources>

In this case, you don't have to set the packageSourceMapping element. It's a single source, so MSBuild doesn't require mappings.

Luckily, as mentioned earlier, the project already had a build automation script available. This is where the actual magic happens. During a pipeline run, you can easily swap to the internal config:

function Restore-NuGet
{
    [CmdletBinding()]
    param (
        [string]$SolutionPath,
        [switch]$UseInternal
    )

    $nugetConfig = Join-Path (Split-Path $SolutionPath -Parent) "nuget.config"

    if ($UseInternal)
    {
        Write-Verbose "Using internal NuGet configuration"
        $nugetConfig = Join-Path (Split-Path $SolutionPath -Parent) "nuget.internal.config"
    }

    $dotNetPath = (Get-Command "dotnet" -CommandType Application -ErrorAction SilentlyContinue).Source

    if (!$dotNetPath)
    {
        throw "dotnet CLI not found. Please ensure that .NET SDK is installed and 'dotnet' is in the system PATH."
    }

    & dotnet restore $SolutionPath --configfile $nugetConfig
}

Whenever you call the function, you can use the $env:TF_BUILD variable to determine if it is run through Azure DevOps, or not:

Restore-Nuget -SolutionPath $solutionPath -UseInternal:([bool]$env:TF_BUILD)

This results in local builds and community PRs always using the public config with source mappings, whereas internal builds swap to the private feed.

The key insight

So what's the real lesson here? Package source mapping is helpful in itself, but it's not a one-size-fits-all solution. Each open-source project lives in its own bubble where you need to balance between:

  1. Security
  2. Accessibility
  3. Flexibility

Each point gets covered if you split your NuGet configs into your build context.

What you should do

If you ever encounter the above issue in similar projects, start the discussion and:

  1. Start mapping your package explicitly using the packageSourceMapping element. Use a (*) wildcard pattern for your primary source
  2. If you want to capture errors early on, set the RestorePackageDownloadTrustedSources property to error in your builds
  3. When you want contributions, consider splitting your configs if you require internal builds

The beauty of this approach is that it's transparent and doesn't require additional work from contributors. They can just clone, build, and everything should work as expected. That's the kind of simplicity I love. One straightforward solution that lets everything else fade to the background.