Pipeline Integration with Azure Artifacts
A comprehensive guide to integrating Azure Pipelines with Azure Artifacts for automated package publishing, authentication tasks, version stamping, feed views, promotion workflows, and cross-pipeline consumption across npm, NuGet, and Python ecosystems.
Pipeline Integration with Azure Artifacts
Overview
Azure Pipelines and Azure Artifacts are designed to work together, but "designed to work together" does not mean "works out of the box without effort." The authentication tasks, version stamping strategies, feed view promotions, and cross-pipeline consumption patterns all require deliberate configuration. If you get it right, your pipeline publishes packages automatically with correct versions, promotes them through feed views, and makes them available to downstream pipelines without manual intervention. If you get it wrong, you spend hours debugging 401 errors and version conflicts.
I have built publishing pipelines for organizations that ship packages across npm, NuGet, and Python from the same monorepo. The patterns are consistent across ecosystems once you understand the authentication model and version stamping approach. This article covers the authenticate tasks for every ecosystem, version generation from build metadata, pre-release publishing from branches, feed view promotion, and a complete multi-ecosystem monorepo pipeline example.
Prerequisites
- An Azure DevOps organization with Azure Pipelines and Azure Artifacts enabled
- At least one Azure Artifacts feed configured
- Familiarity with YAML pipeline syntax
- A repository with packages to publish (npm, NuGet, Python, or a combination)
- Basic understanding of semantic versioning (SemVer)
Authentication Tasks
Azure Pipelines provides dedicated authentication tasks for each package ecosystem. These tasks inject credentials into the build agent's configuration so that package managers (npm, dotnet, pip, twine, Maven) can authenticate with your Azure Artifacts feed without managing PATs.
NuGetAuthenticate@1
This task configures NuGet credential providers on the build agent:
steps:
- task: NuGetAuthenticate@1
displayName: Authenticate with NuGet feeds
- script: |
dotnet restore
dotnet build --configuration Release
dotnet nuget push **/*.nupkg --source https://pkgs.dev.azure.com/my-org/my-project/_packaging/my-feed/nuget/v3/index.json --api-key az
displayName: Build and publish NuGet package
The task authenticates against all Azure Artifacts feeds the build service identity has access to. You do not need to specify which feed -- it handles all of them. The --api-key az value is a required placeholder; the actual authentication happens through the credential provider.
For external NuGet feeds (outside your Azure DevOps organization), use a service connection:
- task: NuGetAuthenticate@1
inputs:
nuGetServiceConnections: external-nuget-feed
npmAuthenticate@0
npm authentication requires a .npmrc file that references your feed. The task injects credentials into the .npmrc at runtime:
steps:
- task: npmAuthenticate@0
inputs:
workingFile: .npmrc
displayName: Authenticate npm
- script: npm ci
displayName: Install dependencies
- script: npm publish
displayName: Publish to Azure Artifacts
Your .npmrc must exist before the task runs and must reference your Azure Artifacts feed:
registry=https://pkgs.dev.azure.com/my-org/my-project/_packaging/my-feed/npm/registry/
always-auth=true
The task reads the .npmrc, finds the Azure Artifacts registry URLs, and injects authentication tokens. It does not create the .npmrc file for you.
PipAuthenticate@1
For Python packages, PipAuthenticate@1 configures pip to authenticate with your feed:
steps:
- task: PipAuthenticate@1
inputs:
artifactFeeds: my-feed
displayName: Authenticate pip
- script: |
pip install -r requirements.txt
displayName: Install Python dependencies
The task sets the PIP_INDEX_URL and PIP_EXTRA_INDEX_URL environment variables. These persist for all subsequent script steps in the job.
TwineAuthenticate@1
For publishing Python packages, TwineAuthenticate@1 creates a .pypirc file:
steps:
- task: TwineAuthenticate@1
inputs:
artifactFeed: my-feed
displayName: Authenticate twine
- script: |
python -m build
twine upload -r my-feed --config-file $(PYPIRC_PATH) dist/*
displayName: Build and publish Python package
The PYPIRC_PATH variable is set by the task and points to the generated .pypirc file. Always use --config-file $(PYPIRC_PATH) with twine to pick up the injected credentials.
MavenAuthenticate@0
For Maven/Gradle projects:
steps:
- task: MavenAuthenticate@0
inputs:
artifactsFeeds: my-feed
displayName: Authenticate Maven
- task: Maven@4
inputs:
mavenPomFile: pom.xml
goals: deploy
options: -DskipTests
displayName: Publish Maven package
The task generates a settings.xml with the correct credentials and configures Maven to use it.
Version Stamping from Build Numbers
Hard-coding version numbers in your source code and updating them manually before each release is error-prone and does not scale. Instead, generate versions from build metadata -- branch name, build number, commit hash.
NuGet Version Stamping
Pass the version as an MSBuild property:
variables:
majorVersion: 2
minorVersion: 1
${{ if eq(variables['Build.SourceBranch'], 'refs/heads/main') }}:
packageVersion: $(majorVersion).$(minorVersion).$(Build.BuildId)
${{ else }}:
packageVersion: $(majorVersion).$(minorVersion).$(Build.BuildId)-$(Build.SourceBranchName)
steps:
- script: |
dotnet pack --configuration Release \
-p:PackageVersion=$(packageVersion) \
--output $(Build.ArtifactStagingDirectory)
displayName: Pack with build version
This produces:
- From
main:2.1.4567(release version) - From
feature/auth:2.1.4567-auth(pre-release version) - From
bugfix/null-check:2.1.4567-null-check(pre-release version)
npm Version Stamping
npm uses package.json for the version. Update it before publishing:
variables:
baseVersion: 3.0.0
steps:
- script: |
if [ "$(Build.SourceBranchName)" = "main" ]; then
npm version $(baseVersion)-build.$(Build.BuildId) --no-git-tag-version
else
BRANCH=$(echo $(Build.SourceBranchName) | sed 's/[^a-zA-Z0-9]/-/g')
npm version $(baseVersion)-${BRANCH}.$(Build.BuildId) --no-git-tag-version
fi
displayName: Stamp version
- script: npm publish
displayName: Publish
The --no-git-tag-version flag prevents npm from creating a git commit and tag, which you do not want in CI.
Python Version Stamping
For Python, use the build module with dynamic versioning:
steps:
- script: |
VERSION="1.0.$(Build.BuildId)"
if [ "$(Build.SourceBranchName)" != "main" ]; then
BRANCH=$(echo $(Build.SourceBranchName) | sed 's/[^a-zA-Z0-9]//g')
VERSION="${VERSION}.dev$(Build.BuildId)"
fi
echo "##vso[task.setvariable variable=pyVersion]${VERSION}"
displayName: Calculate version
- script: |
sed -i "s/version = .*/version = \"$(pyVersion)\"/" pyproject.toml
python -m build
displayName: Build with stamped version
Pre-Release Packages from Feature Branches
A pattern I use in every project: feature branches publish pre-release packages so that downstream consumers can test integration before merging. The key is making pre-release versions sort correctly and clean up automatically.
trigger:
branches:
include:
- main
- feature/*
- release/*
variables:
feedName: my-packages
${{ if eq(variables['Build.SourceBranch'], 'refs/heads/main') }}:
isRelease: true
versionSuffix: ''
${{ elseif startsWith(variables['Build.SourceBranch'], 'refs/heads/release/') }}:
isRelease: true
versionSuffix: '-rc.$(Build.BuildId)'
${{ else }}:
isRelease: false
versionSuffix: '-dev.$(Build.BuildId)'
stages:
- stage: Build
jobs:
- job: BuildPackage
steps:
- task: NuGetAuthenticate@1
- script: |
dotnet pack --configuration Release \
-p:VersionSuffix=$(versionSuffix) \
--output $(Build.ArtifactStagingDirectory)
displayName: Pack
- task: PublishBuildArtifacts@1
inputs:
pathToPublish: $(Build.ArtifactStagingDirectory)
artifactName: packages
- stage: Publish
dependsOn: Build
jobs:
- job: PublishPackage
steps:
- task: DownloadBuildArtifacts@1
inputs:
buildType: current
downloadType: single
artifactName: packages
downloadPath: $(System.ArtifactsDirectory)
- task: NuGetAuthenticate@1
- script: |
dotnet nuget push $(System.ArtifactsDirectory)/packages/*.nupkg \
--source https://pkgs.dev.azure.com/my-org/my-project/_packaging/$(feedName)/nuget/v3/index.json \
--api-key az \
--skip-duplicate
displayName: Push to feed
This produces:
mainbuilds:1.0.0(release, promoted to @Release view)release/*builds:1.0.0-rc.4567(release candidate)feature/*builds:1.0.0-dev.4567(development pre-release)
Feed Views and Promotion Workflows
Feed views (@Local, @Prerelease, @Release) let you control which packages are visible to different consumers. The promotion workflow is:
- Package publishes to the feed, lands in @Local
- After testing, promote to @Prerelease for wider testing
- After validation, promote to @Release for production consumption
Promoting Packages in Pipelines
Use the Azure DevOps REST API to promote packages from a pipeline:
- stage: Promote
dependsOn: Publish
condition: and(succeeded(), eq(variables['isRelease'], 'true'))
jobs:
- deployment: PromoteToRelease
environment: production
strategy:
runOnce:
deploy:
steps:
- task: PowerShell@2
inputs:
targetType: inline
script: |
$packageName = "MyCompany.Utilities"
$packageVersion = "$(packageVersion)"
$feedId = "$(feedName)"
$org = "my-organization"
$project = "my-project"
$body = @{
views = @{
op = "add"
path = "/views/-"
value = "Release"
}
} | ConvertTo-Json
$url = "https://pkgs.dev.azure.com/$org/$project/_apis/packaging/feeds/$feedId/nuget/packages/$packageName/versions/${packageVersion}?api-version=7.1"
$headers = @{
Authorization = "Bearer $(System.AccessToken)"
"Content-Type" = "application/json"
}
Invoke-RestMethod -Uri $url -Method Patch -Headers $headers -Body $body
Write-Host "Promoted $packageName@$packageVersion to Release view"
displayName: Promote to Release view
Consuming from Specific Views
Configure downstream projects to consume from the @Release view for stability:
# .npmrc for consuming only released packages
registry=https://pkgs.dev.azure.com/my-org/my-project/_packaging/my-feed@Release/npm/registry/
always-auth=true
<!-- nuget.config for consuming only released packages -->
<configuration>
<packageSources>
<clear />
<add key="release-feed"
value="https://pkgs.dev.azure.com/my-org/my-project/_packaging/my-feed@Release/nuget/v3/index.json" />
</packageSources>
</configuration>
The @Release suffix in the URL restricts the feed to only expose packages that have been promoted to the Release view.
Cross-Pipeline Package Consumption
When pipeline B depends on a package published by pipeline A, you need to handle the dependency correctly.
Using Pipeline Resources
Pipeline resources let you trigger downstream pipelines when upstream packages are published:
# Pipeline B -- consumes packages from Pipeline A
resources:
pipelines:
- pipeline: library-pipeline
source: 'MyCompany.Utilities-CI'
trigger:
branches:
include:
- main
steps:
- task: NuGetAuthenticate@1
- script: dotnet restore
displayName: Restore (picks up latest published package)
Version Pinning Across Pipelines
For reproducible builds, pin the exact version of the upstream package rather than using floating ranges:
variables:
utilsVersion: 2.1.$(resources.pipeline.library-pipeline.runID)
steps:
- script: |
dotnet add package MyCompany.Utilities --version $(utilsVersion)
dotnet restore
displayName: Pin and restore upstream dependency
Complete Working Example
This is a multi-ecosystem monorepo pipeline that publishes npm and NuGet packages from the same repository, handles version stamping, and promotes to feed views.
Repository Structure
monorepo/
packages/
js-sdk/
package.json
.npmrc
src/
index.js
dotnet-sdk/
MyCompany.SDK.csproj
src/
Client.cs
azure-pipelines.yml
The npm Package
{
"name": "@mycompany/js-sdk",
"version": "1.0.0",
"description": "JavaScript SDK for MyCompany platform API",
"main": "src/index.js",
"scripts": {
"test": "jest",
"lint": "eslint src/"
},
"files": ["src/", "README.md"],
"dependencies": {
"node-fetch": "^2.7.0"
},
"devDependencies": {
"jest": "^29.7.0",
"eslint": "^8.56.0"
}
}
// packages/js-sdk/src/index.js
var fetch = require("node-fetch");
function PlatformClient(options) {
this.baseUrl = options.baseUrl;
this.apiKey = options.apiKey;
this.timeout = options.timeout || 30000;
}
PlatformClient.prototype.request = function(method, path, body) {
var url = this.baseUrl + path;
var self = this;
var requestOptions = {
method: method,
headers: {
"Authorization": "Bearer " + self.apiKey,
"Content-Type": "application/json"
},
timeout: self.timeout
};
if (body) {
requestOptions.body = JSON.stringify(body);
}
return fetch(url, requestOptions).then(function(response) {
if (!response.ok) {
var error = new Error("API request failed: " + response.status + " " + response.statusText);
error.status = response.status;
throw error;
}
return response.json();
});
};
PlatformClient.prototype.getUser = function(userId) {
return this.request("GET", "/users/" + userId);
};
PlatformClient.prototype.listUsers = function(params) {
var query = params ? "?" + new URLSearchParams(params).toString() : "";
return this.request("GET", "/users" + query);
};
PlatformClient.prototype.createUser = function(userData) {
return this.request("POST", "/users", userData);
};
module.exports = PlatformClient;
The .npmrc File
@mycompany:registry=https://pkgs.dev.azure.com/my-org/my-project/_packaging/my-feed/npm/registry/
always-auth=true
The NuGet Package
<!-- packages/dotnet-sdk/MyCompany.SDK.csproj -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<PackageId>MyCompany.SDK</PackageId>
<Authors>Platform Team</Authors>
<Description>.NET SDK for MyCompany platform API</Description>
<PackageTags>sdk;api;client</PackageTags>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Net.Http.Json" Version="8.0.0" />
</ItemGroup>
</Project>
The Pipeline
# azure-pipelines.yml
trigger:
branches:
include:
- main
- feature/*
- release/*
paths:
include:
- packages/
pool:
vmImage: ubuntu-latest
variables:
feedName: my-packages
majorMinor: '2.0'
buildVersion: $(majorMinor).$(Build.BuildId)
${{ if eq(variables['Build.SourceBranch'], 'refs/heads/main') }}:
isMainBranch: true
npmTag: latest
${{ else }}:
isMainBranch: false
npmTag: dev
stages:
# ========================================
# Stage 1: Build and Test Everything
# ========================================
- stage: Build
jobs:
- job: BuildJS
displayName: Build JavaScript SDK
steps:
- task: NodeTool@0
inputs:
versionSpec: 20.x
- task: npmAuthenticate@0
inputs:
workingFile: packages/js-sdk/.npmrc
- script: npm ci
workingDirectory: packages/js-sdk
displayName: Install JS dependencies
- script: npm test
workingDirectory: packages/js-sdk
displayName: Run JS tests
- script: npm run lint
workingDirectory: packages/js-sdk
displayName: Lint JS code
- script: |
if [ "$(isMainBranch)" = "true" ]; then
npm version $(buildVersion) --no-git-tag-version
else
BRANCH=$(echo $(Build.SourceBranchName) | sed 's/[^a-zA-Z0-9]/-/g')
npm version $(buildVersion)-${BRANCH} --no-git-tag-version
fi
workingDirectory: packages/js-sdk
displayName: Stamp npm version
- script: npm pack
workingDirectory: packages/js-sdk
displayName: Create npm tarball
- task: CopyFiles@2
inputs:
sourceFolder: packages/js-sdk
contents: '*.tgz'
targetFolder: $(Build.ArtifactStagingDirectory)/npm
- task: PublishBuildArtifacts@1
inputs:
pathToPublish: $(Build.ArtifactStagingDirectory)/npm
artifactName: npm-package
- job: BuildDotNet
displayName: Build .NET SDK
steps:
- task: UseDotNet@2
inputs:
packageType: sdk
version: 8.0.x
- task: NuGetAuthenticate@1
- script: dotnet restore packages/dotnet-sdk/MyCompany.SDK.csproj
displayName: Restore .NET dependencies
- script: dotnet build packages/dotnet-sdk/MyCompany.SDK.csproj --configuration Release --no-restore
displayName: Build .NET SDK
- script: dotnet test packages/dotnet-sdk/MyCompany.SDK.Tests/MyCompany.SDK.Tests.csproj --configuration Release
displayName: Run .NET tests
continueOnError: false
- script: |
if [ "$(isMainBranch)" = "true" ]; then
VERSION="$(buildVersion)"
else
BRANCH=$(echo $(Build.SourceBranchName) | sed 's/[^a-zA-Z0-9]/-/g')
VERSION="$(buildVersion)-${BRANCH}"
fi
dotnet pack packages/dotnet-sdk/MyCompany.SDK.csproj \
--configuration Release \
--no-build \
-p:PackageVersion=${VERSION} \
--output $(Build.ArtifactStagingDirectory)/nuget
displayName: Pack NuGet package
- task: PublishBuildArtifacts@1
inputs:
pathToPublish: $(Build.ArtifactStagingDirectory)/nuget
artifactName: nuget-package
# ========================================
# Stage 2: Publish to Azure Artifacts
# ========================================
- stage: Publish
dependsOn: Build
jobs:
- job: PublishNpm
displayName: Publish npm package
steps:
- task: DownloadBuildArtifacts@1
inputs:
buildType: current
downloadType: single
artifactName: npm-package
downloadPath: $(System.ArtifactsDirectory)
- task: npmAuthenticate@0
inputs:
workingFile: packages/js-sdk/.npmrc
- script: |
TARBALL=$(ls $(System.ArtifactsDirectory)/npm-package/*.tgz)
npm publish "$TARBALL" --tag $(npmTag) --registry https://pkgs.dev.azure.com/my-org/my-project/_packaging/$(feedName)/npm/registry/
displayName: Publish npm package
- job: PublishNuGet
displayName: Publish NuGet package
steps:
- task: DownloadBuildArtifacts@1
inputs:
buildType: current
downloadType: single
artifactName: nuget-package
downloadPath: $(System.ArtifactsDirectory)
- task: NuGetAuthenticate@1
- script: |
dotnet nuget push $(System.ArtifactsDirectory)/nuget-package/*.nupkg \
--source https://pkgs.dev.azure.com/my-org/my-project/_packaging/$(feedName)/nuget/v3/index.json \
--api-key az \
--skip-duplicate
displayName: Publish NuGet package
# ========================================
# Stage 3: Promote to Release View
# ========================================
- stage: Promote
dependsOn: Publish
condition: and(succeeded(), eq(variables['isMainBranch'], 'true'))
jobs:
- deployment: PromotePackages
environment: production
strategy:
runOnce:
deploy:
steps:
- task: PowerShell@2
inputs:
targetType: inline
script: |
$org = "my-org"
$project = "my-project"
$feed = "$(feedName)"
$version = "$(buildVersion)"
$token = "$(System.AccessToken)"
$headers = @{
Authorization = "Bearer $token"
"Content-Type" = "application/json"
}
$body = '{"views":{"op":"add","path":"/views/-","value":"Release"}}'
# Promote NuGet package
$nugetUrl = "https://pkgs.dev.azure.com/$org/$project/_apis/packaging/feeds/$feed/nuget/packages/MyCompany.SDK/versions/${version}?api-version=7.1"
try {
Invoke-RestMethod -Uri $nugetUrl -Method Patch -Headers $headers -Body $body
Write-Host "Promoted MyCompany.SDK@$version to Release"
} catch {
Write-Warning "Failed to promote NuGet: $_"
}
# Promote npm package
$npmUrl = "https://pkgs.dev.azure.com/$org/$project/_apis/packaging/feeds/$feed/npm/@mycompany/js-sdk/versions/${version}?api-version=7.1"
try {
Invoke-RestMethod -Uri $npmUrl -Method Patch -Headers $headers -Body $body
Write-Host "Promoted @mycompany/js-sdk@$version to Release"
} catch {
Write-Warning "Failed to promote npm: $_"
}
displayName: Promote packages to Release view
This pipeline:
- Builds and tests both the JavaScript and .NET SDKs in parallel
- Stamps versions from the build number, with pre-release suffixes for non-main branches
- Publishes both packages to the same Azure Artifacts feed
- Promotes main branch builds to the @Release feed view after deployment approval
Common Issues and Troubleshooting
1. npmAuthenticate Fails with "No matching registry"
Error:
##[error]Error: No matching registries found in .npmrc for the specified workingFile.
The .npmrc file does not contain an Azure Artifacts registry URL, or the URL format is wrong. The npmAuthenticate@0 task only processes URLs that match the pkgs.dev.azure.com pattern. Verify your .npmrc contains:
registry=https://pkgs.dev.azure.com/my-org/my-project/_packaging/my-feed/npm/registry/
The trailing slash matters. Without it, npm may append paths incorrectly.
2. NuGet Push Returns 409 Conflict
Error:
error: Response status code does not indicate success: 409 (Conflict).
The feed already contains 'MyCompany.SDK 2.0.4567'.
You are publishing a version that already exists. Add --skip-duplicate to the push command. This is expected when retrying a failed pipeline -- the package was published in the previous attempt.
3. System.AccessToken Returns 401
Error:
Invoke-RestMethod: Response status code does not indicate success: 401 (Unauthorized).
The $(System.AccessToken) is scoped to the current project. If your feed is in a different project or is organization-scoped, the token may not have access. Options:
- Grant the build service identity Contributor permission on the target feed
- Use a PAT stored in a variable group instead of
$(System.AccessToken) - For organization-scoped feeds, check Organization Settings > Pipelines > Settings and enable "Allow scripts to access the OAuth token"
4. Version Mismatch Between Build and Publish Stages
Error: The publish stage cannot find the package because the version calculated in the build stage is different from what the publish stage expects.
Build variables are scoped to their stage. If you calculate a version in stage 1, it is not available in stage 2 unless you pass it through an artifact or an output variable:
- script: |
echo "##vso[task.setvariable variable=packageVersion;isOutput=true]$(buildVersion)"
name: setVersion
Then reference it in the downstream stage with $[stageDependencies.Build.BuildJob.outputs['setVersion.packageVersion']].
5. PipAuthenticate Sets Wrong Index URL
Error: pip installs from PyPI instead of your Azure Artifacts feed.
PipAuthenticate@1 sets PIP_EXTRA_INDEX_URL, not PIP_INDEX_URL. This means PyPI remains the primary index. To use your feed as the primary index:
- task: PipAuthenticate@1
inputs:
artifactFeeds: my-feed
onlyAddExtraIndex: false
Setting onlyAddExtraIndex: false makes the task set PIP_INDEX_URL instead.
6. Feed View Promotion Returns 404
Error:
404 Not Found when calling the promotion API.
The package version string in the API URL must exactly match the published version, including any pre-release suffixes. URL-encode any special characters. Also verify the feed name and package name are correct -- the API uses the package name as it appears in the feed, not the file name.
Best Practices
Use the authenticate tasks instead of managing PATs.
NuGetAuthenticate@1,npmAuthenticate@0,PipAuthenticate@1, andMavenAuthenticate@0handle credential injection cleanly. Never store PATs in pipeline variables when$(System.AccessToken)works.Generate versions from build metadata, not source code. Build number, branch name, and commit hash are available in every pipeline. Use them to construct unique, sortable, traceable versions. Reserve manual version bumps for major and minor version changes only.
Publish pre-release from feature branches, release from main only. Feature branch builds should always produce pre-release versions (e.g.,
1.0.0-dev.4567). Only main branch builds produce release versions. This prevents accidental releases and makes feed cleanup easy.Use
--skip-duplicatefor all push commands. Pipeline retries are common. Making package pushes idempotent prevents 409 errors on retry without requiring manual intervention.Promote packages through feed views instead of publishing to multiple feeds. A single feed with @Local, @Prerelease, and @Release views is easier to manage than three separate feeds. Consumers point at the view that matches their stability requirements.
Gate promotions with deployment environments. Use Azure DevOps environments with approval gates for the promote stage. This ensures someone reviews the package before it reaches the @Release view and production consumers.
Cache package restores in pipelines. The
Cache@2task saves and restoresnode_modules,.nuget/packages, and Python virtual environments between builds. A warm cache reduces build time by minutes.Use pipeline resources for cross-pipeline dependencies. When pipeline B depends on packages from pipeline A, use the
resources.pipelinesdeclaration to trigger B when A completes. This is more reliable than polling the feed for new versions.Tag builds that produce released packages. After a successful promotion to @Release, tag the Git commit. This creates an audit trail linking every released package version to the exact source code that produced it.
Test your authentication locally before debugging in CI. Most pipeline authentication issues are misconfigured feed permissions or incorrect URLs. Verify the feed URL, build service permissions, and
.npmrc/nuget.configcontents before running the pipeline.