Artifacts

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:

  • main builds: 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:

  1. Package publishes to the feed, lands in @Local
  2. After testing, promote to @Prerelease for wider testing
  3. 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:

  1. Builds and tests both the JavaScript and .NET SDKs in parallel
  2. Stamps versions from the build number, with pre-release suffixes for non-main branches
  3. Publishes both packages to the same Azure Artifacts feed
  4. 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:

  1. Grant the build service identity Contributor permission on the target feed
  2. Use a PAT stored in a variable group instead of $(System.AccessToken)
  3. 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

  1. Use the authenticate tasks instead of managing PATs. NuGetAuthenticate@1, npmAuthenticate@0, PipAuthenticate@1, and MavenAuthenticate@0 handle credential injection cleanly. Never store PATs in pipeline variables when $(System.AccessToken) works.

  2. 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.

  3. 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.

  4. Use --skip-duplicate for all push commands. Pipeline retries are common. Making package pushes idempotent prevents 409 errors on retry without requiring manual intervention.

  5. 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.

  6. 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.

  7. Cache package restores in pipelines. The Cache@2 task saves and restores node_modules, .nuget/packages, and Python virtual environments between builds. A warm cache reduces build time by minutes.

  8. Use pipeline resources for cross-pipeline dependencies. When pipeline B depends on packages from pipeline A, use the resources.pipelines declaration to trigger B when A completes. This is more reliable than polling the feed for new versions.

  9. 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.

  10. 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.config contents before running the pipeline.

References

Powered by Contentful