Artifacts

NuGet Feed Management in Azure DevOps

A comprehensive guide to creating and managing NuGet feeds in Azure DevOps, covering feed creation, upstream sources, authentication, publishing, versioning strategies, feed views, and permissions management.

NuGet Feed Management in Azure DevOps

Overview

Azure Artifacts provides hosted NuGet feeds that let you publish, version, and share .NET packages across your organization without running your own NuGet server. If you are building .NET libraries that multiple teams consume, or if you need to control exactly which packages and versions your developers use, Azure Artifacts feeds are the right solution. They integrate directly with the dotnet CLI, Visual Studio, and Azure Pipelines with zero additional infrastructure.

I have been managing NuGet feeds in Azure DevOps for several years across organizations ranging from small teams to enterprises with hundreds of developers. The initial setup is straightforward, but getting feed views, upstream sources, and permissions right is where most teams struggle. This article covers everything from feed creation through automated publishing pipelines, including the REST API scripts I use to manage feeds programmatically.

Prerequisites

  • An Azure DevOps organization with an active project
  • .NET SDK 6.0 or later installed locally
  • Azure DevOps Personal Access Token (PAT) with Packaging (Read & Write) scope
  • Node.js 18+ for the REST API management scripts
  • Familiarity with NuGet package structure and .csproj files
  • A basic understanding of Azure Pipelines YAML syntax

Creating and Configuring Feeds

Organization-Scoped vs Project-Scoped Feeds

Azure Artifacts offers two feed scopes, and choosing the wrong one causes permissions headaches down the road.

Organization-scoped feeds are visible to every project in your Azure DevOps organization. Use these for shared libraries that multiple teams need -- utility packages, SDK wrappers, internal frameworks. Every developer in the organization can consume packages from these feeds without special permissions.

Project-scoped feeds are limited to a single project. Use these for team-specific packages that should not leak across project boundaries, or when you want tighter control over who can publish.

To create a feed through the Azure DevOps UI, navigate to Artifacts > Create Feed. But I prefer doing it through the REST API because it is repeatable:

// create-feed.js
var https = require("https");

var org = "my-organization";
var project = "my-project";
var pat = process.env.AZURE_DEVOPS_PAT;

var feedDefinition = {
  name: "dotnet-packages",
  description: "Internal .NET packages for the platform team",
  hideDeletedPackageVersions: true,
  upstreamEnabled: true,
  upstreamSources: [
    {
      name: "NuGet Gallery",
      protocol: "nuget",
      location: "https://api.nuget.org/v3/index.json",
      upstreamSourceType: "public"
    }
  ]
};

var body = JSON.stringify(feedDefinition);
var auth = Buffer.from(":" + pat).toString("base64");

var options = {
  hostname: "feeds.dev.azure.com",
  path: "/" + org + "/" + project + "/_apis/packaging/feeds?api-version=7.1",
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Basic " + auth,
    "Content-Length": Buffer.byteLength(body)
  }
};

var req = https.request(options, function(res) {
  var data = "";
  res.on("data", function(chunk) { data += chunk; });
  res.on("end", function() {
    if (res.statusCode === 201) {
      var feed = JSON.parse(data);
      console.log("Feed created: " + feed.name);
      console.log("Feed ID: " + feed.id);
      console.log("Feed URL: " + feed.url);
    } else {
      console.error("Failed to create feed (" + res.statusCode + "):");
      console.error(data);
    }
  });
});

req.on("error", function(err) {
  console.error("Request error:", err.message);
});

req.write(body);
req.end();

Run it:

export AZURE_DEVOPS_PAT="your-pat-here"
node create-feed.js

Output:

Feed created: dotnet-packages
Feed ID: 3a7b1c2d-4e5f-6789-abcd-ef0123456789
Feed URL: https://feeds.dev.azure.com/my-organization/my-project/_apis/packaging/feeds/3a7b1c2d-4e5f-6789-abcd-ef0123456789

Configuring Upstream Sources

Upstream sources are what make Azure Artifacts feeds genuinely useful instead of just another NuGet server. When you enable NuGet Gallery as an upstream, your feed acts as a proxy -- developers point their NuGet client at your Azure Artifacts feed URL only, and it transparently fetches packages from nuget.org on demand. Once fetched, the package is cached in your feed.

This gives you three things:

  1. A single feed URL for all packages, internal and external
  2. An audit trail of which public packages your organization uses
  3. Protection against left-pad incidents -- once a package version is cached, it stays in your feed even if the author deletes it from nuget.org

You can add multiple upstream sources:

// add-upstream.js
var https = require("https");

var org = "my-organization";
var project = "my-project";
var feedId = "dotnet-packages";
var pat = process.env.AZURE_DEVOPS_PAT;
var auth = Buffer.from(":" + pat).toString("base64");

function getFeed(callback) {
  var options = {
    hostname: "feeds.dev.azure.com",
    path: "/" + org + "/" + project + "/_apis/packaging/feeds/" + feedId + "?api-version=7.1",
    method: "GET",
    headers: { "Authorization": "Basic " + auth }
  };

  var req = https.request(options, function(res) {
    var data = "";
    res.on("data", function(chunk) { data += chunk; });
    res.on("end", function() { callback(JSON.parse(data)); });
  });
  req.end();
}

function updateFeed(feed) {
  feed.upstreamSources.push({
    name: "GitHub NuGet",
    protocol: "nuget",
    location: "https://nuget.pkg.github.com/my-org/index.json",
    upstreamSourceType: "public"
  });

  var body = JSON.stringify(feed);
  var options = {
    hostname: "feeds.dev.azure.com",
    path: "/" + org + "/" + project + "/_apis/packaging/feeds/" + feedId + "?api-version=7.1",
    method: "PATCH",
    headers: {
      "Content-Type": "application/json",
      "Authorization": "Basic " + auth,
      "Content-Length": Buffer.byteLength(body)
    }
  };

  var req = https.request(options, function(res) {
    var data = "";
    res.on("data", function(chunk) { data += chunk; });
    res.on("end", function() {
      if (res.statusCode === 200) {
        console.log("Upstream source added successfully");
      } else {
        console.error("Failed (" + res.statusCode + "):", data);
      }
    });
  });

  req.write(body);
  req.end();
}

getFeed(updateFeed);

Authentication and Client Configuration

Authenticating with the dotnet CLI

The dotnet CLI needs credentials to push to or restore from your Azure Artifacts feed. The cleanest approach is using a nuget.config file at your repository root:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <packageSources>
    <clear />
    <add key="dotnet-packages" value="https://pkgs.dev.azure.com/my-organization/my-project/_packaging/dotnet-packages/nuget/v3/index.json" />
  </packageSources>
  <packageSourceCredentials>
    <dotnet-packages>
      <add key="Username" value="azure" />
      <add key="ClearTextPassword" value="%AZURE_DEVOPS_PAT%" />
    </dotnet-packages>
  </packageSourceCredentials>
</configuration>

The <clear /> tag is important -- it removes all default NuGet sources, so the only source is your Azure Artifacts feed. Since your feed has nuget.org as an upstream, you still get all public packages, but they flow through your feed.

For local development, use the Azure Artifacts Credential Provider instead of hardcoding PATs:

# Install the credential provider (one-time setup)
# Windows
iex "& { $(irm https://aka.ms/install-artifacts-credprovider.ps1) }"

# macOS/Linux
sh -c "$(curl -fsSL https://aka.ms/install-artifacts-credprovider.sh)"

# Now dotnet restore will prompt for authentication
dotnet restore --interactive

Visual Studio Configuration

Visual Studio users can add the feed through Tools > NuGet Package Manager > Package Manager Settings > Package Sources. But distributing a nuget.config in your repository root is better because it works for every developer automatically without manual configuration.

Publishing NuGet Packages

From the Command Line

Create a class library, pack it, and push:

# Create a library project
dotnet new classlib -n MyCompany.Utilities -o src/MyCompany.Utilities

# Build and pack
dotnet pack src/MyCompany.Utilities/MyCompany.Utilities.csproj \
  --configuration Release \
  --output ./artifacts

# Push to your feed
dotnet nuget push ./artifacts/MyCompany.Utilities.1.0.0.nupkg \
  --source dotnet-packages \
  --api-key az

The --api-key az value is required but the actual value does not matter when using the credential provider -- it just cannot be empty.

Setting Package Metadata

Configure your .csproj with proper metadata:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <PackageId>MyCompany.Utilities</PackageId>
    <Version>1.2.0</Version>
    <Authors>Platform Team</Authors>
    <Company>MyCompany</Company>
    <Description>Shared utility library for internal services</Description>
    <PackageTags>utilities;helpers;internal</PackageTags>
    <RepositoryUrl>https://dev.azure.com/my-organization/my-project/_git/utilities</RepositoryUrl>
    <PackageReadmeFile>README.md</PackageReadmeFile>
    <GenerateDocumentationFile>true</GenerateDocumentationFile>
  </PropertyGroup>
  <ItemGroup>
    <None Include="README.md" Pack="true" PackagePath="" />
  </ItemGroup>
</Project>

Versioning Strategies

I strongly recommend using SemVer 2.0.0 for all internal packages. The version format is MAJOR.MINOR.PATCH-prerelease+metadata:

  • Bump MAJOR when you make breaking API changes
  • Bump MINOR when you add new features that are backward-compatible
  • Bump PATCH for bug fixes

For pre-release packages from feature branches, use the branch name and build number:

# Generate a pre-release version from CI
dotnet pack --configuration Release \
  -p:Version=1.3.0-feature-auth.$(Build.BuildId) \
  --output ./artifacts

This produces a package like MyCompany.Utilities.1.3.0-feature-auth.12345.nupkg. Pre-release versions sort below release versions, so consumers on the stable feed never accidentally pick up a pre-release build.

Feed Views and Package Promotion

Feed views are a release management feature that most teams overlook. A view is a filtered subset of your feed. Every feed comes with three default views:

  • @Local -- all packages published directly to the feed
  • @Prerelease -- packages promoted to the prerelease view
  • @Release -- packages promoted to the release view

The workflow is: publish to the feed (lands in @Local), promote to @Prerelease for testing, then promote to @Release for production consumption. Each view has its own URL, so consuming projects can point at different views:

<!-- nuget.config for a project that only consumes released packages -->
<configuration>
  <packageSources>
    <clear />
    <add key="release-packages"
         value="https://pkgs.dev.azure.com/my-org/my-project/_packaging/dotnet-packages@Release/nuget/v3/index.json" />
  </packageSources>
</configuration>

Notice the @Release in the URL. This feed URL only exposes packages that have been explicitly promoted to the Release view.

To promote a package via the REST API:

// promote-package.js
var https = require("https");

var org = "my-organization";
var project = "my-project";
var feedId = "dotnet-packages";
var pat = process.env.AZURE_DEVOPS_PAT;
var auth = Buffer.from(":" + pat).toString("base64");

var packageName = process.argv[2];
var packageVersion = process.argv[3];
var targetView = process.argv[4] || "Release";

if (!packageName || !packageVersion) {
  console.error("Usage: node promote-package.js <packageName> <version> [view]");
  process.exit(1);
}

var body = JSON.stringify({
  views: { op: "add", path: "/views/-", value: targetView }
});

var path = "/" + org + "/" + project + "/_apis/packaging/feeds/" + feedId +
  "/nuget/packages/" + packageName + "/versions/" + packageVersion +
  "?api-version=7.1";

var options = {
  hostname: "pkgs.dev.azure.com",
  path: path,
  method: "PATCH",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Basic " + auth,
    "Content-Length": Buffer.byteLength(body)
  }
};

var req = https.request(options, function(res) {
  var data = "";
  res.on("data", function(chunk) { data += chunk; });
  res.on("end", function() {
    if (res.statusCode === 200) {
      console.log("Promoted " + packageName + "@" + packageVersion + " to " + targetView);
    } else {
      console.error("Failed (" + res.statusCode + "):", data);
    }
  });
});

req.write(body);
req.end();
node promote-package.js MyCompany.Utilities 1.2.0 Release
# Output: Promoted [email protected] to Release

Managing Feed Permissions

Feed permissions control who can read, publish, and manage packages. Azure Artifacts has four permission levels:

Role Read Publish Unlist/Delete Manage Feed
Reader Yes No No No
Collaborator Yes Yes (upstream only) No No
Contributor Yes Yes Yes No
Owner Yes Yes Yes Yes

By default, every member of the project gets Reader access to project-scoped feeds. The Project Collection Build Service account needs Contributor access to publish from pipelines.

// set-feed-permissions.js
var https = require("https");

var org = "my-organization";
var project = "my-project";
var feedId = "dotnet-packages";
var pat = process.env.AZURE_DEVOPS_PAT;
var auth = Buffer.from(":" + pat).toString("base64");

// Grant Contributor access to the build service
var permissions = [
  {
    identityDescriptor: "Microsoft.TeamFoundation.ServiceIdentity;build:" + project,
    role: "contributor"
  }
];

var body = JSON.stringify(permissions);

var options = {
  hostname: "feeds.dev.azure.com",
  path: "/" + org + "/" + project + "/_apis/packaging/feeds/" + feedId + "/permissions?api-version=7.1",
  method: "PATCH",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Basic " + auth,
    "Content-Length": Buffer.byteLength(body)
  }
};

var req = https.request(options, function(res) {
  var data = "";
  res.on("data", function(chunk) { data += chunk; });
  res.on("end", function() {
    if (res.statusCode === 200) {
      console.log("Permissions updated successfully");
    } else {
      console.error("Failed (" + res.statusCode + "):", data);
    }
  });
});

req.write(body);
req.end();

Complete Working Example

This example puts everything together: a .NET class library project, a NuGet package configuration, a pipeline that builds, packs, publishes, and promotes the package, plus a Node.js management script for feed operations.

The Library Project

// src/MyCompany.Utilities/StringHelpers.cs
namespace MyCompany.Utilities
{
    public static class StringHelpers
    {
        public static string Slugify(string input)
        {
            if (string.IsNullOrWhiteSpace(input)) return string.Empty;

            var slug = input.ToLowerInvariant().Trim();
            slug = System.Text.RegularExpressions.Regex.Replace(slug, @"[^a-z0-9\s-]", "");
            slug = System.Text.RegularExpressions.Regex.Replace(slug, @"\s+", "-");
            slug = System.Text.RegularExpressions.Regex.Replace(slug, @"-+", "-");
            return slug.Trim('-');
        }

        public static string Truncate(string input, int maxLength)
        {
            if (string.IsNullOrEmpty(input) || input.Length <= maxLength) return input;
            return input.Substring(0, maxLength - 3) + "...";
        }
    }
}

The Pipeline YAML

trigger:
  branches:
    include:
      - main
      - feature/*

pool:
  vmImage: ubuntu-latest

variables:
  buildConfiguration: Release
  feedName: dotnet-packages
  ${{ if eq(variables['Build.SourceBranchName'], 'main') }}:
    packageVersion: 1.2.$(Build.BuildId)
  ${{ else }}:
    packageVersion: 1.2.$(Build.BuildId)-$(Build.SourceBranchName)

stages:
  - stage: Build
    jobs:
      - job: BuildAndPack
        steps:
          - task: UseDotNet@2
            inputs:
              packageType: sdk
              version: 8.0.x

          - script: dotnet restore src/MyCompany.Utilities/MyCompany.Utilities.csproj
            displayName: Restore dependencies

          - script: dotnet build src/MyCompany.Utilities/MyCompany.Utilities.csproj --configuration $(buildConfiguration) --no-restore
            displayName: Build project

          - script: dotnet test tests/MyCompany.Utilities.Tests/MyCompany.Utilities.Tests.csproj --configuration $(buildConfiguration) --no-build
            displayName: Run tests

          - script: |
              dotnet pack src/MyCompany.Utilities/MyCompany.Utilities.csproj \
                --configuration $(buildConfiguration) \
                --no-build \
                -p:PackageVersion=$(packageVersion) \
                --output $(Build.ArtifactStagingDirectory)
            displayName: Pack NuGet package

          - task: PublishBuildArtifacts@1
            inputs:
              pathToPublish: $(Build.ArtifactStagingDirectory)
              artifactName: nuget

  - stage: Publish
    dependsOn: Build
    jobs:
      - job: PublishPackage
        steps:
          - task: DownloadBuildArtifacts@1
            inputs:
              buildType: current
              downloadType: single
              artifactName: nuget
              downloadPath: $(System.ArtifactsDirectory)

          - task: NuGetAuthenticate@1
            displayName: Authenticate with Azure Artifacts

          - script: |
              dotnet nuget push $(System.ArtifactsDirectory)/nuget/*.nupkg \
                --source https://pkgs.dev.azure.com/my-organization/my-project/_packaging/$(feedName)/nuget/v3/index.json \
                --api-key az \
                --skip-duplicate
            displayName: Push to Azure Artifacts

  - stage: Promote
    dependsOn: Publish
    condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
    jobs:
      - deployment: PromotePackage
        environment: production
        strategy:
          runOnce:
            deploy:
              steps:
                - task: AzureCLI@2
                  inputs:
                    azureSubscription: my-azure-connection
                    scriptType: bash
                    scriptLocation: inlineScript
                    inlineScript: |
                      az artifacts universal publish \
                        --organization https://dev.azure.com/my-organization \
                        --project my-project \
                        --scope project \
                        --feed $(feedName) \
                        --name MyCompany.Utilities \
                        --version $(packageVersion) \
                        --description "Promoted to Release"
                  displayName: Promote to Release view

Feed Management Script

// feed-manager.js -- Comprehensive feed management utility
var https = require("https");

var org = process.env.AZURE_DEVOPS_ORG || "my-organization";
var project = process.env.AZURE_DEVOPS_PROJECT || "my-project";
var pat = process.env.AZURE_DEVOPS_PAT;

if (!pat) {
  console.error("Error: AZURE_DEVOPS_PAT environment variable is required");
  process.exit(1);
}

var auth = Buffer.from(":" + pat).toString("base64");

function apiRequest(method, hostname, path, body, callback) {
  var options = {
    hostname: hostname,
    path: path,
    method: method,
    headers: {
      "Content-Type": "application/json",
      "Authorization": "Basic " + auth
    }
  };

  if (body) {
    options.headers["Content-Length"] = Buffer.byteLength(body);
  }

  var req = https.request(options, function(res) {
    var data = "";
    res.on("data", function(chunk) { data += chunk; });
    res.on("end", function() {
      callback(null, res.statusCode, data);
    });
  });

  req.on("error", function(err) { callback(err); });
  if (body) req.write(body);
  req.end();
}

function listFeeds() {
  var path = "/" + org + "/" + project + "/_apis/packaging/feeds?api-version=7.1";
  apiRequest("GET", "feeds.dev.azure.com", path, null, function(err, status, data) {
    if (err) return console.error("Error:", err.message);
    var result = JSON.parse(data);
    console.log("Feeds in " + project + ":");
    console.log("---");
    result.value.forEach(function(feed) {
      console.log("  Name: " + feed.name);
      console.log("  ID: " + feed.id);
      console.log("  Packages: " + (feed.packageCount || "unknown"));
      console.log("  Upstream: " + (feed.upstreamEnabled ? "enabled" : "disabled"));
      console.log("---");
    });
  });
}

function listPackages(feedName) {
  var path = "/" + org + "/" + project + "/_apis/packaging/feeds/" + feedName +
    "/packages?api-version=7.1&protocolType=NuGet&$top=50";
  apiRequest("GET", "feeds.dev.azure.com", path, null, function(err, status, data) {
    if (err) return console.error("Error:", err.message);
    var result = JSON.parse(data);
    console.log("NuGet packages in " + feedName + " (" + result.count + " total):");
    result.value.forEach(function(pkg) {
      var versions = pkg.versions.map(function(v) { return v.version; }).join(", ");
      console.log("  " + pkg.name + " [" + versions + "]");
    });
  });
}

function deletePackageVersion(feedName, packageName, version) {
  var path = "/" + org + "/" + project + "/_apis/packaging/feeds/" + feedName +
    "/nuget/packages/" + packageName + "/versions/" + version + "?api-version=7.1";
  var body = JSON.stringify({ listed: false });
  apiRequest("PATCH", "pkgs.dev.azure.com", path, body, function(err, status, data) {
    if (err) return console.error("Error:", err.message);
    if (status === 200) {
      console.log("Unlisted " + packageName + "@" + version);
    } else {
      console.error("Failed (" + status + "):", data);
    }
  });
}

var command = process.argv[2];

switch (command) {
  case "list-feeds":
    listFeeds();
    break;
  case "list-packages":
    listPackages(process.argv[3]);
    break;
  case "unlist":
    deletePackageVersion(process.argv[3], process.argv[4], process.argv[5]);
    break;
  default:
    console.log("Usage:");
    console.log("  node feed-manager.js list-feeds");
    console.log("  node feed-manager.js list-packages <feedName>");
    console.log("  node feed-manager.js unlist <feedName> <packageName> <version>");
}
# List all feeds
node feed-manager.js list-feeds

# Output:
# Feeds in my-project:
# ---
#   Name: dotnet-packages
#   ID: 3a7b1c2d-4e5f-6789-abcd-ef0123456789
#   Packages: 12
#   Upstream: enabled
# ---

# List packages in a feed
node feed-manager.js list-packages dotnet-packages

# Output:
# NuGet packages in dotnet-packages (12 total):
#   MyCompany.Utilities [1.2.0, 1.1.0, 1.0.0]
#   MyCompany.Auth [2.0.1, 2.0.0]

Common Issues and Troubleshooting

1. 401 Unauthorized When Pushing Packages

Error:

error: Response status code does not indicate success: 401 (Unauthorized).

This almost always means your PAT has expired or does not have the Packaging (Read & Write) scope. Generate a new PAT with the correct scope:

  1. Navigate to User Settings > Personal Access Tokens in Azure DevOps
  2. Create a new token with Packaging > Read & Write scope
  3. Update your credentials: dotnet nuget update source dotnet-packages --username azure --password <new-pat>

If using the credential provider, clear the cached credentials:

# Windows
del %LOCALAPPDATA%\MicrosoftCredentialProvider\*

# macOS/Linux
rm -rf ~/.local/share/MicrosoftCredentialProvider

2. Package Not Found Despite Being Published

Error:

error NU1101: Unable to find package MyCompany.Utilities. No packages exist with this id in source(s): dotnet-packages

Three common causes. First, the package is published but your nuget.config points to a feed view (like @Release) and the package has not been promoted to that view yet. Check which view your config references. Second, the NuGet package index takes up to 2 minutes to update after publishing. Wait and retry. Third, you published to the wrong feed -- verify with node feed-manager.js list-packages <feedName>.

3. Upstream Source Conflicts

Error:

error NU1102: Unable to find package Newtonsoft.Json with version (>= 13.0.3)
  - Found 53 version(s) in dotnet-packages but none satisfy: (>= 13.0.3)

Your feed has cached older versions of a public package from the upstream, but the version you need has not been fetched yet. Force a refresh by restoring with --force:

dotnet restore --force

If that does not work, the upstream source might be misconfigured. Verify it in the feed settings.

4. Duplicate Package Version Errors

Error:

error: Response status code does not indicate success: 409 (Conflict).
The feed already contains 'MyCompany.Utilities 1.2.0'.

NuGet feeds are immutable -- you cannot overwrite a published version. This is by design. Either increment the version number or use the --skip-duplicate flag in your pipeline to ignore this error:

dotnet nuget push *.nupkg --source dotnet-packages --api-key az --skip-duplicate

5. Feed Permissions Error in Pipeline

Error:

error: Response status code does not indicate success: 403 (Forbidden).
The current user does not have permission to publish to this feed.

The build service account needs Contributor access on the feed. Navigate to Artifacts > Feed Settings > Permissions and add the Project Collection Build Service identity with the Contributor role. For project-scoped feeds, also add the [ProjectName] Build Service identity.

6. nuget.config Not Being Picked Up

Error:

error: Unable to load the service index for source https://pkgs.dev.azure.com/...

The dotnet CLI searches for nuget.config in the current directory and parent directories up to the drive root. If your config file is in the wrong location, it will not be found. Place it in the repository root next to your .sln file. Verify which config is being used:

dotnet nuget list source

Best Practices

  1. Use a single feed with upstream sources instead of multiple feeds. Having one feed that proxies nuget.org gives you a single source URL, an audit trail, and protection against package deletions upstream. Multiple feeds create confusion about which feed to use.

  2. Always include a nuget.config in your repository root. Do not rely on developer machine configuration. A committed nuget.config with <clear /> ensures everyone uses the same sources and eliminates "works on my machine" issues.

  3. Use the credential provider instead of hardcoded PATs. PATs expire and get committed to source control accidentally. The Azure Artifacts Credential Provider handles authentication seamlessly across local development and CI/CD.

  4. Implement feed views for release management. Publish all packages to @Local, promote tested packages to @Prerelease, and promote production-ready packages to @Release. Point consuming projects at the appropriate view for their stability needs.

  5. Set explicit package metadata in your .csproj. Include PackageId, Version, Authors, Description, RepositoryUrl, and a README. This makes packages discoverable and self-documenting in the feed UI.

  6. Use --skip-duplicate in CI pipelines. Pipeline retries should not fail because a package version already exists. The --skip-duplicate flag makes pushes idempotent.

  7. Pin upstream package versions in production projects. Do not use floating version ranges like * or >= for upstream packages in production code. Lock to specific versions and upgrade deliberately.

  8. Monitor feed storage usage. Azure Artifacts includes 2 GB of free storage per organization. After that, you pay per GB. Set up retention policies to automatically clean up old pre-release versions. Keep release versions indefinitely.

  9. Use organization-scoped feeds for shared libraries, project-scoped for team-internal packages. This balances discoverability with access control. Do not create one feed per team -- it leads to feed sprawl and cross-feed dependency chains.

  10. Automate feed management with the REST API. Use scripts like the ones in this article to create feeds, manage permissions, and promote packages. Manual feed management through the UI does not scale.

References

Powered by Contentful