Azure Artifacts Feed Management Strategies
A practical guide to Azure Artifacts feed management covering feed setup, upstream sources, package promotion through views, permissions, and CI/CD integration.
Azure Artifacts Feed Management Strategies
If your team is still passing around .tgz files or pulling packages from random shared folders, it is time to stop. Azure Artifacts gives you private package registries that integrate directly with Azure DevOps pipelines, support multiple package types, and provide real governance over what your teams consume and publish. This article covers everything you need to set up feeds properly, manage package promotion, configure upstream sources, and build a feed strategy that scales.
What Are Azure Artifacts Feeds
A feed in Azure Artifacts is a private package registry. It works like npmjs.org, nuget.org, or Maven Central, except you control who can publish, who can consume, and what packages are available. Feeds support npm, NuGet, Python (PyPI), Maven, Cargo, and Universal Packages.
Every Azure DevOps organization gets 2 GiB of storage free. After that, you pay per additional GiB. Feeds store packages immutably by default — once you publish version 1.0.0 of a package, that version cannot be overwritten. This is intentional and important for reproducible builds.
Feed Types: Project-Scoped vs Organization-Scoped
Azure Artifacts supports two feed scopes, and the choice matters more than most teams realize.
Project-scoped feeds are tied to a specific Azure DevOps project. Visibility follows project permissions. If a user has access to the project, they can see the feed. The feed URL includes the project name:
https://pkgs.dev.azure.com/{org}/{project}/_packaging/{feed}/npm/registry/
Organization-scoped feeds exist at the organization level and are visible to anyone in the org. The URL is shorter:
https://pkgs.dev.azure.com/{org}/_packaging/{feed}/npm/registry/
Use project-scoped feeds when teams should not see each other's internal packages. Use organization-scoped feeds for shared libraries that multiple projects consume. My recommendation: default to project-scoped and promote shared packages to an org-scoped feed when the need arises.
Creating and Configuring Feeds
You can create feeds through the Azure DevOps UI under Artifacts, or through the REST API. Here is how to create a feed using the Azure CLI:
# Install the Azure DevOps extension if you haven't
az extension add --name azure-devops
# Create an organization-scoped feed
az artifacts feed create \
--name "shared-packages" \
--description "Shared internal packages for all teams" \
--organization "https://dev.azure.com/myorg"
# Create a project-scoped feed
az artifacts feed create \
--name "team-packages" \
--project "WebPlatform" \
--description "Internal packages for the web platform team" \
--organization "https://dev.azure.com/myorg"
You can also create feeds programmatically through the REST API:
curl -X POST \
"https://feeds.dev.azure.com/myorg/_apis/packaging/feeds?api-version=7.1" \
-H "Content-Type: application/json" \
-H "Authorization: Basic $(echo -n ':YOUR_PAT' | base64)" \
-d '{
"name": "shared-packages",
"description": "Organization shared packages",
"hideDeletedPackageVersions": true,
"upstreamEnabled": true
}'
Feed Views: Local, @Prerelease, and @Release
Feed views are the promotion mechanism in Azure Artifacts. Every feed has three built-in views:
- @Local — Every published package lands here automatically. This is the default view and contains everything.
- @Prerelease — Packages promoted here are considered ready for testing but not production.
- @Release — Packages promoted here are production-ready.
Views do not copy packages. They are labels applied to specific package versions. A single version of a package can exist in @Local and also be promoted to @Release. The underlying storage is the same.
Why does this matter? Because downstream consumers can point at a specific view. A production application should only consume packages from the @Release view. A staging environment can consume from @Prerelease. Development teams consume from @Local.
To promote a package to a view using the REST API:
# Promote an npm package to @Release
curl -X PATCH \
"https://pkgs.dev.azure.com/myorg/_apis/packaging/feeds/shared-packages/npm/@anthropic/my-sdk/versions/1.2.0?api-version=7.1" \
-H "Content-Type: application/json" \
-H "Authorization: Basic $(echo -n ':YOUR_PAT' | base64)" \
-d '{
"views": { "op": "add", "path": "/views/-", "value": "@Release" }
}'
In a YAML pipeline, you can promote packages after tests pass:
- task: Bash@3
displayName: 'Promote package to Release view'
inputs:
targetType: 'inline'
script: |
az artifacts universal publish \
--organization "https://dev.azure.com/myorg" \
--feed "shared-packages" \
--name "my-package" \
--version "$(Build.BuildNumber)" \
--description "Release build" \
--view "@Release"
Upstream Sources
Upstream sources let your feed proxy public registries. When a developer requests lodash from your private feed, the feed checks its local packages first, then fetches from the configured upstream (npmjs.org) and caches the result. This gives you several advantages:
- Single source of truth — Developers configure one registry URL and get both private and public packages.
- Availability — If npmjs.org goes down, cached packages still resolve.
- Security — You can block specific public packages by unlisting them from your feed.
You can configure upstream sources when creating the feed or add them afterward. Here are the common upstreams:
| Package Type | Upstream URL |
|---|---|
| npm | https://registry.npmjs.org |
| NuGet | https://api.nuget.org/v3/index.json |
| PyPI | https://pypi.org/simple |
| Maven Central | https://repo.maven.apache.org/maven2 |
To add an upstream source via the Azure CLI:
az artifacts feed update \
--name "shared-packages" \
--organization "https://dev.azure.com/myorg" \
--upstream-sources '[{
"name": "npmjs",
"protocol": "npm",
"location": "https://registry.npmjs.org",
"upstreamSourceType": "public"
}]'
You can also add other Azure Artifacts feeds as upstream sources. This is how you build a diamond pattern where team feeds flow into a shared org feed.
Feed Permissions and Access Control
Feed permissions determine who can read, publish, and manage packages. There are four permission levels:
| Role | Read | Publish | Unlist/Deprecate | Admin |
|---|---|---|---|---|
| Reader | Yes | No | No | No |
| Collaborator | Yes | Yes | No | No |
| Contributor | Yes | Yes | Yes | No |
| Owner | Yes | Yes | Yes | Yes |
For project-scoped feeds, the project's Build Service account is automatically added as a Collaborator so that pipelines can publish packages. For organization-scoped feeds, you need to add the Project Collection Build Service account manually.
# Add a user or group to feed permissions
curl -X PATCH \
"https://feeds.dev.azure.com/myorg/_apis/packaging/feeds/shared-packages/permissions?api-version=7.1" \
-H "Content-Type: application/json" \
-H "Authorization: Basic $(echo -n ':YOUR_PAT' | base64)" \
-d '[{
"identityDescriptor": "Microsoft.TeamFoundation.Identity;S-1-9-...",
"role": "contributor"
}]'
A common mistake is forgetting to grant the build service identity permission to the feed. If your pipeline fails with a 403 on publish, check the feed permissions first.
Connecting to Feeds
npm Configuration
Create a .npmrc file in your project root:
registry=https://pkgs.dev.azure.com/myorg/WebPlatform/_packaging/team-packages/npm/registry/
always-auth=true
For authentication, use the vsts-npm-auth helper or configure a PAT. Create a user-level .npmrc at ~/.npmrc:
; begin auth token
//pkgs.dev.azure.com/myorg/WebPlatform/_packaging/team-packages/npm/registry/:username=myorg
//pkgs.dev.azure.com/myorg/WebPlatform/_packaging/team-packages/npm/registry/:_password=BASE64_ENCODED_PAT
//pkgs.dev.azure.com/myorg/WebPlatform/_packaging/team-packages/npm/registry/:[email protected]
//pkgs.dev.azure.com/myorg/WebPlatform/_packaging/team-packages/npm/:username=myorg
//pkgs.dev.azure.com/myorg/WebPlatform/_packaging/team-packages/npm/:_password=BASE64_ENCODED_PAT
//pkgs.dev.azure.com/myorg/WebPlatform/_packaging/team-packages/npm/:[email protected]
; end auth token
Generate the base64-encoded PAT:
echo -n 'YOUR_PERSONAL_ACCESS_TOKEN' | base64
NuGet Configuration
Create or update nuget.config in your solution root:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
<add key="shared-packages"
value="https://pkgs.dev.azure.com/myorg/_packaging/shared-packages/nuget/v3/index.json" />
</packageSources>
<packageSourceCredentials>
<shared-packages>
<add key="Username" value="myorg" />
<add key="ClearTextPassword" value="YOUR_PAT" />
</shared-packages>
</packageSourceCredentials>
</configuration>
pip Configuration
pip install my-package \
--index-url "https://pkgs.dev.azure.com/myorg/_packaging/shared-packages/pypi/simple" \
--extra-index-url "https://pypi.org/simple"
Or configure pip.conf:
[global]
index-url=https://pkgs.dev.azure.com/myorg/_packaging/shared-packages/pypi/simple
extra-index-url=https://pypi.org/simple
Feed Retention Policies
Feeds grow over time. Without retention policies, you will accumulate hundreds of old package versions that nobody uses. Azure Artifacts lets you configure retention at the feed level:
# Set maximum versions per package to 50
curl -X PATCH \
"https://feeds.dev.azure.com/myorg/_apis/packaging/feeds/shared-packages/retentionpolicies?api-version=7.1" \
-H "Content-Type: application/json" \
-H "Authorization: Basic $(echo -n ':YOUR_PAT' | base64)" \
-d '{
"countLimit": 50,
"daysToKeepRecentlyDownloadedPackages": 30
}'
Key settings:
- countLimit — Maximum number of versions to retain per package. Oldest versions are deleted first.
- daysToKeepRecentlyDownloadedPackages — Packages downloaded within this window are exempt from cleanup regardless of the count limit.
Packages promoted to @Release or @Prerelease views are always exempt from retention cleanup. This is another reason to promote your production packages.
Storage Consumption and Billing
Azure Artifacts provides 2 GiB free per organization. After that, you purchase additional storage in 1 GiB increments. To check consumption:
az artifacts feed list \
--organization "https://dev.azure.com/myorg" \
--query "[].{Name:name, Id:id}" \
--output table
To reduce storage costs:
- Enable retention policies on every feed.
- Delete unused feeds entirely.
- Avoid publishing debug/snapshot builds to long-lived feeds.
- Use separate feeds for CI artifacts with aggressive retention.
Multi-Feed Strategy: Dev vs Release
A mature organization typically needs at least two feeds:
Development feed — Every CI build publishes here with a prerelease version. Retention is aggressive (keep last 10 versions). Teams consume from this feed during active development.
Release feed — Only promoted, tested packages end up here. Retention is generous (keep last 100 versions or more). Production builds and downstream teams consume from here.
Here is how this looks in a pipeline:
trigger:
- main
pool:
vmImage: 'ubuntu-latest'
variables:
feedName: 'team-packages'
isMain: $[eq(variables['Build.SourceBranch'], 'refs/heads/main')]
steps:
- task: NodeTool@0
inputs:
versionSpec: '20.x'
- task: npmAuthenticate@0
inputs:
workingFile: '.npmrc'
- script: npm ci
displayName: 'Install dependencies'
- script: npm test
displayName: 'Run tests'
- script: npm run build
displayName: 'Build package'
# Publish prerelease version on feature branches
- script: |
COMMIT_SHORT=$(echo $(Build.SourceVersion) | cut -c1-7)
npm version prerelease --preid="dev.${COMMIT_SHORT}" --no-git-tag-version
npm publish --tag dev
displayName: 'Publish prerelease'
condition: and(succeeded(), eq(variables.isMain, false))
# Publish release version on main
- script: npm publish
displayName: 'Publish release'
condition: and(succeeded(), eq(variables.isMain, true))
# Promote to @Release view on main
- task: Bash@3
displayName: 'Promote to Release view'
condition: and(succeeded(), eq(variables.isMain, true))
inputs:
targetType: 'inline'
script: |
PACKAGE_VERSION=$(node -p "require('./package.json').version")
PACKAGE_NAME=$(node -p "require('./package.json').name")
echo "Promoting ${PACKAGE_NAME}@${PACKAGE_VERSION} to @Release"
az artifacts universal publish \
--organization "$(System.CollectionUri)" \
--feed "$(feedName)" \
--name "${PACKAGE_NAME}" \
--version "${PACKAGE_VERSION}" \
--view "@Release"
Complete Working Example
Let us walk through the entire lifecycle: creating a feed, configuring npm, publishing from a pipeline, promoting through views, and consuming downstream.
Step 1: Create the Feed with Upstream Sources
az artifacts feed create \
--name "web-platform" \
--project "WebPlatform" \
--description "Web platform team packages" \
--organization "https://dev.azure.com/myorg"
Step 2: Configure .npmrc in Your Package Project
Project-level .npmrc:
registry=https://pkgs.dev.azure.com/myorg/WebPlatform/_packaging/web-platform/npm/registry/
always-auth=true
Step 3: Set Up package.json for Publishing
{
"name": "@myorg/ui-components",
"version": "1.0.0",
"description": "Shared UI component library",
"main": "dist/index.js",
"scripts": {
"build": "node scripts/build.js",
"test": "jest --coverage",
"prepublishOnly": "npm run build && npm test"
},
"publishConfig": {
"registry": "https://pkgs.dev.azure.com/myorg/WebPlatform/_packaging/web-platform/npm/registry/"
},
"files": [
"dist/",
"README.md"
]
}
Step 4: Pipeline to Publish and Promote
trigger:
branches:
include:
- main
- feature/*
pr:
branches:
include:
- main
pool:
vmImage: 'ubuntu-latest'
variables:
feedName: 'web-platform'
projectName: 'WebPlatform'
steps:
- task: NodeTool@0
inputs:
versionSpec: '20.x'
- task: npmAuthenticate@0
inputs:
workingFile: '.npmrc'
- script: npm ci
displayName: 'Install dependencies'
- script: npm test
displayName: 'Run tests'
- script: npm run build
displayName: 'Build'
# Publish on main branch only
- script: npm publish
displayName: 'Publish to feed'
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
# Promote to @Prerelease after publish
- task: Bash@3
displayName: 'Promote to Prerelease'
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
inputs:
targetType: 'inline'
script: |
PACKAGE_NAME=$(node -p "require('./package.json').name")
PACKAGE_VERSION=$(node -p "require('./package.json').version")
echo "##vso[task.setvariable variable=pkgName]${PACKAGE_NAME}"
echo "##vso[task.setvariable variable=pkgVersion]${PACKAGE_VERSION}"
# Gate: manual approval for @Release promotion
# This would typically be a separate release pipeline or stage
Step 5: Consuming Packages Downstream
In the consuming project, add a .npmrc:
@myorg:registry=https://pkgs.dev.azure.com/myorg/WebPlatform/_packaging/web-platform/npm/registry/
always-auth=true
Notice the scoped registry — only @myorg packages route to the private feed. Everything else goes to the default npm registry. Then install:
npm install @myorg/ui-components@latest
Step 6: Using the Package
var UIComponents = require("@myorg/ui-components");
var Button = UIComponents.Button;
var Modal = UIComponents.Modal;
function renderDashboard() {
var header = Button.create({
label: "Submit",
variant: "primary",
onClick: function() {
console.log("Button clicked");
}
});
var dialog = Modal.create({
title: "Confirm Action",
content: "Are you sure you want to proceed?",
onConfirm: function() {
console.log("Confirmed");
}
});
return { header: header, dialog: dialog };
}
module.exports = { renderDashboard: renderDashboard };
Common Issues and Troubleshooting
1. 401 Unauthorized When Installing Packages
This almost always means your PAT has expired or the .npmrc credentials are misconfigured. Verify:
# Check if your PAT is valid
curl -u "user:YOUR_PAT" \
"https://feeds.dev.azure.com/myorg/_apis/packaging/feeds?api-version=7.1"
# Re-encode your PAT
echo -n 'YOUR_NEW_PAT' | base64
Make sure the .npmrc has entries for both the registry path and the shortened path (with and without registry/). Both are required.
2. 409 Conflict on Publish — Version Already Exists
Azure Artifacts enforces immutable versions. You cannot overwrite 1.0.0 once published. Either bump the version or delete the existing version (not recommended for production feeds):
# Bump patch version
npm version patch
npm publish
# Or if you must delete (admin permissions required)
az artifacts universal delete \
--organization "https://dev.azure.com/myorg" \
--feed "web-platform" \
--name "my-package" \
--version "1.0.0"
3. Upstream Packages Not Resolving
If packages from npmjs.org are not resolving through your feed, check that upstream sources are enabled and correctly ordered. Azure Artifacts checks upstreams in the order they are listed. Also verify that the feed's upstream source has not been disabled:
az artifacts feed show \
--name "web-platform" \
--organization "https://dev.azure.com/myorg" \
--query "upstreamSources"
4. Pipeline Publish Fails with 403 Forbidden
The build service identity needs Contributor or Collaborator role on the feed. Go to feed settings, select Permissions, and add:
- For project-scoped feeds:
{Project Name} Build Service ({Org Name}) - For organization-scoped feeds:
Project Collection Build Service ({Org Name})
Grant at least the Collaborator role.
5. Slow Package Resolution
When a feed has many upstream sources, resolution can be slow because Azure Artifacts checks each upstream sequentially. Reduce upstreams to only the ones you actually need. For npm projects, you typically only need npmjs and any internal Azure Artifacts feeds.
Best Practices
Use scoped packages for internal libraries. Prefix your packages with
@yourorg/and configure scoped registries. This keeps public package resolution on the default registry and only routes your private packages through Azure Artifacts.Enable upstream sources instead of mixing registries. Configuring multiple registries in
.npmrcleads to resolution confusion and security risks. Use a single feed with upstream sources configured.Set retention policies on every feed from day one. Storage costs add up silently. Configure a count limit of 50-100 versions and a 30-day download protection window. Promoted packages are exempt.
Promote packages through views in your pipeline. Never manually promote packages. Automate the promotion from @Local to @Prerelease after integration tests pass, and from @Prerelease to @Release after approval gates.
Use project-scoped feeds by default. Organization-scoped feeds are visible to everyone. Start with project-scoped feeds and create a shared org-scoped feed only when cross-project consumption is needed.
Keep .npmrc files in version control, credentials out. The project
.npmrcwith the registry URL should be committed. The user.npmrcwith PAT credentials should never be committed. Add~/.npmrcpatterns to your.gitignoreif needed.Separate CI artifact feeds from release feeds. CI builds that publish on every commit generate enormous package churn. Use a dedicated feed with aggressive retention for CI artifacts and a stable feed with generous retention for releases.
Audit feed permissions quarterly. Teams change, people leave. Review who has Contributor and Owner access to your feeds. Remove service accounts that are no longer used.
Migrating from Other Registries
If you are moving from a self-hosted npm registry (Verdaccio, Nexus, Artifactory), the migration path is straightforward:
- Create your Azure Artifacts feed.
- Download all existing packages from the old registry.
- Re-publish each package version to the new feed.
Here is a script to bulk-publish:
#!/bin/bash
# migrate-packages.sh
# Publish all .tgz packages in a directory to Azure Artifacts
FEED_URL="https://pkgs.dev.azure.com/myorg/WebPlatform/_packaging/web-platform/npm/registry/"
for tarball in packages/*.tgz; do
echo "Publishing ${tarball}..."
npm publish "${tarball}" --registry "${FEED_URL}"
if [ $? -ne 0 ]; then
echo "Failed to publish ${tarball}" >> migration-errors.log
fi
sleep 1 # Avoid rate limiting
done
echo "Migration complete. Check migration-errors.log for failures."
Update all consuming projects' .npmrc files to point to the new feed, and verify that all packages resolve correctly before decommissioning the old registry.