Back to Software Engineering & Delivery Mastery Series Azure DevOps Bootcamp

Module 9: Azure Artifacts & Package Management

June 3, 2026 Wasil Zafar 36 min read

Master Azure Artifacts for enterprise package management — feeds, NuGet, npm, Maven, Python, and Universal packages, upstream sources for proxying public registries, semantic versioning, retention policies, and seamless pipeline integration for publishing and consuming packages.

Table of Contents

  1. Fundamentals
  2. Package Types
  3. Management
  4. Practice

Why Package Management Matters

In Modules 4–8, you built pipelines that compile, test, and deploy applications. But here's a question those pipelines don't answer: how do teams share reusable code without copy-pasting it between repositories? How does your authentication library, shared UI components, or internal SDK reach other teams reliably?

The answer is packages — versioned, immutable bundles of code that can be published once and consumed by any team, any project, any pipeline. Azure Artifacts is your private package registry that stores, versions, and distributes these packages.

Analogy: Azure Artifacts is like an internal post office for your code packages — feeds are mailboxes where packages are delivered, upstream sources are forwarding addresses to public registries (npmjs.org, nuget.org), and retention policies are the rules for how long unclaimed packages are kept before being discarded to save space.

The Copy-Paste Problem

Without package management, teams resort to dangerous patterns:

Anti-Pattern Problem Package Solution
Copy-paste code Bug fixes never propagate, versions drift Publish a package, consumers update via version bumps
Git submodules Complex, fragile, poor versioning Package versions are explicit and atomic
Shared folder / file share No versioning, no authentication, no audit trail Feeds provide auth, versioning, and download history
Public registry only Can't publish proprietary code to npmjs.org Private feeds for internal packages, upstream for public

Private Packages vs Public Registries

Most organizations need both:

  • Public packages — Open-source libraries from npmjs.org, nuget.org, PyPI, Maven Central
  • Private packages — Internal libraries, shared SDKs, company-specific utilities

Azure Artifacts unifies both through upstream sources — a single feed that serves your private packages AND proxies/caches public ones. Developers configure one feed URL and get everything.

For artifact management theory and best practices, see Part 13: Artifact Management.

Feeds — Creating & Configuring

A feed is a container that stores packages. Think of it as a private registry — like having your own npmjs.org or nuget.org, but controlled by your organization.

Organization-Scoped vs Project-Scoped Feeds

Feed Scope Visibility Best For
Organization-scoped All projects in the organization can access Shared libraries used across multiple teams/projects
Project-scoped Only the specific project can access Project-specific packages, isolation between teams

Recommendation: Start with organization-scoped feeds for shared libraries. Use project-scoped feeds only when you need strict isolation (e.g., different business units that shouldn't see each other's packages).

Creating Feeds via the Web UI

  1. Navigate to Artifacts in the left sidebar
  2. Click "+ Create Feed"
  3. Name your feed (e.g., internal-packages)
  4. Choose scope: Organization or Project
  5. Choose visibility: Members of the organization, or specific project members
  6. Check "Include packages from common public sources" to enable upstream sources

Creating Feeds via Azure CLI

# Install the Azure DevOps extension for Azure CLI
az extension add --name azure-devops

# Authenticate to your organization
az devops configure --defaults organization=https://dev.azure.com/yourorg

# Create an organization-scoped feed
az artifacts feed create \
  --name "internal-packages" \
  --description "Shared libraries for all teams" \
  --include-upstream-sources

# Create a project-scoped feed
az artifacts feed create \
  --name "team-alpha-packages" \
  --project "TeamAlpha" \
  --description "Packages specific to Team Alpha"

# List all feeds in the organization
az artifacts feed list --output table

# Show feed details including upstream sources
az artifacts feed show --name "internal-packages"

Feed Permissions

Feeds have role-based access control:

Role Can Do Typical User
Reader Download/consume packages Developers consuming shared libraries
Collaborator Read + save packages from upstream Developers who trigger upstream caching
Contributor Read + publish new packages CI/CD pipelines, library maintainers
Owner All above + manage feed settings, permissions Platform/DevOps engineers
Security tip: Grant your build service account (Project Collection Build Service) the Contributor role so pipelines can publish packages. Never grant Owner to service accounts — that allows them to change feed settings.

NuGet Packages (.NET)

NuGet is the package manager for .NET. If your organization builds .NET libraries (class libraries, shared models, SDK wrappers), NuGet packages are how you distribute them.

Pipeline to Pack & Publish NuGet

# Pipeline: Build, pack, and publish a NuGet package to Azure Artifacts
# Triggers on main branch pushes to the shared library project

trigger:
  branches:
    include: [main]
  paths:
    include: ['src/SharedLibrary/**']

pool:
  vmImage: 'ubuntu-latest'

variables:
  buildConfiguration: 'Release'
  # Semantic version from .csproj or calculated
  majorMinorPatch: '2.1.0'
  # Append build number for pre-release versions
  packageVersion: '$(majorMinorPatch)-ci.$(Build.BuildId)'

steps:
  # Restore dependencies
  - task: DotNetCoreCLI@2
    displayName: 'Restore NuGet packages'
    inputs:
      command: 'restore'
      projects: 'src/SharedLibrary/**/*.csproj'

  # Build the library
  - task: DotNetCoreCLI@2
    displayName: 'Build library'
    inputs:
      command: 'build'
      projects: 'src/SharedLibrary/**/*.csproj'
      arguments: '--configuration $(buildConfiguration) --no-restore'

  # Run unit tests
  - task: DotNetCoreCLI@2
    displayName: 'Run tests'
    inputs:
      command: 'test'
      projects: 'tests/**/*.csproj'
      arguments: '--configuration $(buildConfiguration) --no-build'

  # Pack into a .nupkg file
  - task: DotNetCoreCLI@2
    displayName: 'Pack NuGet package'
    inputs:
      command: 'pack'
      packagesToPack: 'src/SharedLibrary/**/*.csproj'
      configuration: '$(buildConfiguration)'
      versioningScheme: 'byEnvVar'
      versionEnvVar: 'packageVersion'
      outputDir: '$(Build.ArtifactStagingDirectory)/nuget'

  # Publish to Azure Artifacts feed
  - task: NuGetCommand@2
    displayName: 'Push to Azure Artifacts'
    inputs:
      command: 'push'
      packagesToPush: '$(Build.ArtifactStagingDirectory)/nuget/*.nupkg'
      nuGetFeedType: 'internal'
      publishVstsFeed: 'internal-packages'  # Your feed name

Consuming NuGet Packages — nuget.config

To consume packages from your private feed, add a nuget.config file to the root of your consuming project:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <packageSources>
    <!-- Clear default sources to avoid confusion -->
    <clear />
    <!-- Your Azure Artifacts feed (serves both private + upstream public) -->
    <add key="InternalFeed"
         value="https://pkgs.dev.azure.com/yourorg/_packaging/internal-packages/nuget/v3/index.json" />
  </packageSources>
  <packageSourceCredentials>
    <!-- Credentials managed by Azure Artifacts Credential Provider -->
    <!-- In pipelines, NuGetAuthenticate task handles this automatically -->
    <InternalFeed>
      <add key="Username" value="anything" />
      <add key="ClearTextPassword" value="%ARTIFACTS_PAT%" />
    </InternalFeed>
  </packageSourceCredentials>
</configuration>

In pipelines, you don't need credentials in the file — the NuGetAuthenticate@1 task injects credentials automatically:

# In your consuming project's pipeline
steps:
  - task: NuGetAuthenticate@1
    displayName: 'Authenticate to Azure Artifacts'

  - task: DotNetCoreCLI@2
    displayName: 'Restore (uses authenticated feed)'
    inputs:
      command: 'restore'
      projects: '**/*.csproj'
      feedsToUse: 'config'
      nugetConfigPath: 'nuget.config'

npm Packages (Node.js)

npm is the package manager for Node.js and front-end JavaScript. Publishing internal npm packages lets teams share UI components, utility libraries, and API clients.

Pipeline to Publish npm Package

# Pipeline: Build and publish an npm package to Azure Artifacts
# The package is scoped: @yourorg/shared-utils

trigger:
  branches:
    include: [main]
  paths:
    include: ['packages/shared-utils/**']

pool:
  vmImage: 'ubuntu-latest'

steps:
  # Authenticate npm to your Azure Artifacts feed
  - task: npmAuthenticate@0
    displayName: 'Authenticate npm'
    inputs:
      workingFile: 'packages/shared-utils/.npmrc'

  # Install dependencies
  - script: |
      cd packages/shared-utils
      npm ci
    displayName: 'Install dependencies'

  # Run tests
  - script: |
      cd packages/shared-utils
      npm test
    displayName: 'Run tests'

  # Build (if TypeScript or bundled)
  - script: |
      cd packages/shared-utils
      npm run build
    displayName: 'Build package'

  # Publish to Azure Artifacts
  - script: |
      cd packages/shared-utils
      npm publish
    displayName: 'Publish to feed'

.npmrc Configuration

Place a .npmrc file in your project root to point npm at your Azure Artifacts feed:

# .npmrc — Configure npm to use Azure Artifacts feed
# Replace 'yourorg' and 'internal-packages' with your values

# Registry for your scoped packages (@yourorg/*)
@yourorg:registry=https://pkgs.dev.azure.com/yourorg/_packaging/internal-packages/npm/registry/

# Authentication — in pipelines, npmAuthenticate fills this automatically
# For local development, run: npx vsts-npm-auth -config .npmrc
//pkgs.dev.azure.com/yourorg/_packaging/internal-packages/npm/registry/:username=anything
//pkgs.dev.azure.com/yourorg/_packaging/internal-packages/npm/registry/:_password=${ARTIFACTS_TOKEN}
//pkgs.dev.azure.com/yourorg/_packaging/internal-packages/npm/registry/:email=not-required@example.com
//pkgs.dev.azure.com/yourorg/_packaging/internal-packages/npm/:username=anything
//pkgs.dev.azure.com/yourorg/_packaging/internal-packages/npm/:_password=${ARTIFACTS_TOKEN}
//pkgs.dev.azure.com/yourorg/_packaging/internal-packages/npm/:email=not-required@example.com

Local development authentication: Developers run npx vsts-npm-auth -config .npmrc once to set up credentials interactively. The token is stored in the user-level .npmrc (not committed to source control).

Maven Packages (Java)

Maven is the standard build tool and package manager for Java. Azure Artifacts supports Maven repositories for publishing JARs, WARs, and other Java artifacts.

Pipeline for Maven Publish

# Pipeline: Build and publish a Maven artifact to Azure Artifacts
# Publishes to the 'internal-packages' feed

trigger:
  branches:
    include: [main]
  paths:
    include: ['libs/java-common/**']

pool:
  vmImage: 'ubuntu-latest'

steps:
  # Authenticate Maven to Azure Artifacts
  - task: MavenAuthenticate@0
    displayName: 'Authenticate Maven'
    inputs:
      artifactsFeeds: 'internal-packages'

  # Build, test, and publish
  - task: Maven@4
    displayName: 'Maven build and deploy'
    inputs:
      mavenPomFile: 'libs/java-common/pom.xml'
      goals: 'deploy'
      options: '-DskipTests=false'
      publishJUnitResults: true
      testResultsFiles: '**/TEST-*.xml'

pom.xml Distribution Management

Add the distribution management section to your pom.xml to tell Maven where to publish:

<!-- pom.xml — Add to your library's pom.xml -->
<distributionManagement>
  <repository>
    <id>internal-packages</id>
    <url>https://pkgs.dev.azure.com/yourorg/_packaging/internal-packages/maven/v1</url>
  </repository>
  <snapshotRepository>
    <id>internal-packages</id>
    <url>https://pkgs.dev.azure.com/yourorg/_packaging/internal-packages/maven/v1</url>
  </snapshotRepository>
</distributionManagement>

Python Packages

Azure Artifacts supports Python packages via the PyPI-compatible interface. This lets you publish internal Python libraries and install them with pip just like any public package.

Pipeline for Python Package Publish

# Pipeline: Build and publish a Python package to Azure Artifacts
# Uses twine for uploading wheel/sdist distributions

trigger:
  branches:
    include: [main]
  paths:
    include: ['libs/py-common/**']

pool:
  vmImage: 'ubuntu-latest'

steps:
  - task: UsePythonVersion@0
    displayName: 'Use Python 3.11'
    inputs:
      versionSpec: '3.11'

  # Install build dependencies
  - script: |
      python -m pip install --upgrade pip
      pip install build twine
    displayName: 'Install build tools'

  # Run tests
  - script: |
      cd libs/py-common
      pip install -e ".[dev]"
      pytest tests/ --junitxml=results.xml
    displayName: 'Run tests'

  - task: PublishTestResults@2
    inputs:
      testResultsFiles: 'libs/py-common/results.xml'
      testRunTitle: 'Python package tests'

  # Build wheel and sdist
  - script: |
      cd libs/py-common
      python -m build
    displayName: 'Build distributions'

  # Publish to Azure Artifacts using twine
  - task: TwineAuthenticate@1
    displayName: 'Authenticate twine'
    inputs:
      artifactFeed: 'internal-packages'

  - script: |
      cd libs/py-common
      twine upload -r internal-packages --config-file $(PYPIRC_PATH) dist/*
    displayName: 'Upload to Azure Artifacts'

Consuming Python Packages — pip.conf

# pip.conf (Linux/macOS: ~/.config/pip/pip.conf)
# pip.ini (Windows: %APPDATA%\pip\pip.ini)
# Points pip to your Azure Artifacts feed as an extra index

[global]
extra-index-url = https://pkgs.dev.azure.com/yourorg/_packaging/internal-packages/pypi/simple/

# In pipelines, use the PipAuthenticate task instead:
# - task: PipAuthenticate@1
#     inputs:
#       artifactFeeds: 'internal-packages'

Universal Packages

Universal Packages are Azure Artifacts' answer to "what if my artifact isn't a NuGet/npm/Maven/Python package?" They can store any file or folder of any size — making them perfect for artifacts that don't fit traditional package manager formats.

Use Cases for Universal Packages

  • Machine learning models — Trained model files (500 MB+) versioned alongside code
  • Game assets — Textures, audio, level data distributed to build agents
  • Firmware binaries — Compiled firmware for embedded devices
  • Large test datasets — Test fixtures too large for Git
  • Compiled documentation — Generated docs distributed as versioned bundles
  • Desktop installers — MSI/DMG/DEB files for internal distribution

Publishing Universal Packages

# Pipeline: Publish a trained ML model as a Universal Package
# Universal Packages handle large files efficiently (chunked upload)

trigger:
  branches:
    include: [main]
  paths:
    include: ['models/**']

pool:
  vmImage: 'ubuntu-latest'

steps:
  # Train or prepare the model (simplified example)
  - script: |
      mkdir -p $(Build.ArtifactStagingDirectory)/model
      cp models/trained-model.onnx $(Build.ArtifactStagingDirectory)/model/
      cp models/config.json $(Build.ArtifactStagingDirectory)/model/
      cp models/tokenizer.json $(Build.ArtifactStagingDirectory)/model/
    displayName: 'Prepare model files'

  # Publish as Universal Package
  - task: UniversalPackages@0
    displayName: 'Publish ML model as Universal Package'
    inputs:
      command: 'publish'
      publishDirectory: '$(Build.ArtifactStagingDirectory)/model'
      feedsToUsePublish: 'internal'
      vstsFeedPublish: 'internal-packages'
      vstsFeedPackagePublish: 'sentiment-model'
      versionOption: 'custom'
      versionPublish: '3.2.$(Build.BuildId)'
      packagePublishDescription: 'Sentiment analysis ONNX model v3.2'

Downloading Universal Packages

# In a consuming pipeline — download the model for deployment
steps:
  - task: UniversalPackages@0
    displayName: 'Download ML model'
    inputs:
      command: 'download'
      downloadDirectory: '$(Build.SourcesDirectory)/artifacts/model'
      feedsToUse: 'internal'
      vstsFeed: 'internal-packages'
      vstsFeedPackage: 'sentiment-model'
      vstsPackageVersion: '3.2.*'  # Latest patch in 3.2.x
# CLI: Download a Universal Package locally
az artifacts universal download \
  --organization https://dev.azure.com/yourorg \
  --feed internal-packages \
  --name sentiment-model \
  --version 3.2.1 \
  --path ./local-model-dir

Upstream Sources

Upstream sources are one of Azure Artifacts' most powerful features — they let your feed act as a proxy and cache for public registries. Developers configure a single feed URL and get both your private packages AND public packages from npmjs.org, nuget.org, PyPI, or Maven Central.

How Upstream Sources Work

Upstream Source Resolution Flow
flowchart TD
    A[Developer requests package X] --> B{Is X in your feed?}
    B -->|Yes| C[Serve from feed cache ⚡]
    B -->|No| D{Is X in upstream source?}
    D -->|Yes| E[Fetch from public registry]
    E --> F[Cache in your feed]
    F --> C
    D -->|No| G[Package not found ]
                            

Why Upstream Sources Matter

Reliability: Upstream sources mean your builds never fail because npmjs.org is down — cached packages are served from your Artifacts feed. This is a free reliability win. Once a public package is cached, it's served directly from Azure Artifacts even if the upstream registry is offline.

Additional benefits:

  • Single source of truth — One feed URL in config files, not multiple registry URLs
  • Security scanning — All packages flow through one point where you can audit them
  • Network efficiency — Popular packages are cached, reducing external bandwidth
  • Compliance — You control which public packages are allowed (via upstream filtering)

Configuring Upstream Sources

When creating a feed, check "Include packages from common public sources" to automatically add:

Package Type Default Upstream What It Proxies
NuGet nuget.org All public .NET packages
npm npmjs.org All public Node.js packages
Python PyPI (pypi.org) All public Python packages
Maven Maven Central All public Java packages

You can also add other Azure Artifacts feeds as upstream sources — creating a hierarchy where a team feed inherits packages from an organization-wide feed.

Versioning Strategies

Package versioning is how consumers know what changed and whether an update is safe. Azure Artifacts enforces immutability — once you publish version 1.2.3, that version can never be overwritten. This guarantees reproducible builds.

Semantic Versioning (SemVer)

The industry standard is Semantic Versioning (major.minor.patch):

Component Increment When Example
Major (X.0.0) Breaking changes — consumers must update their code 2.0.0 → Removed deprecated API
Minor (0.X.0) New features — backward compatible 2.1.0 → Added new helper method
Patch (0.0.X) Bug fixes — no API changes 2.1.1 → Fixed null reference bug

Pre-Release Versions

Pre-release suffixes indicate packages that aren't production-ready:

  • 1.0.0-alpha.1 — Early development, API may change drastically
  • 1.0.0-beta.1 — Feature-complete but testing in progress
  • 1.0.0-rc.1 — Release candidate, final testing before stable
  • 1.0.0-ci.12345 — CI build, not for manual consumption

Views: @Release, @Prerelease, @Local

Azure Artifacts views let you categorize packages by quality level. Consumers can subscribe to a specific view to control what they receive:

View Contains Who Uses It
@Local All published versions (default) CI/CD pipelines, library developers
@Prerelease Versions promoted for testing QA teams, integration testing
@Release Production-quality versions only Production applications

Version Management in Pipelines

# Versioning strategy: CI builds get pre-release suffix
# Release builds use clean semantic version

variables:
  majorMinor: '3.1'
  # For CI builds: 3.1.0-ci.BuildId
  # For release builds: 3.1.PatchNumber
  ${{ if eq(variables['Build.SourceBranchName'], 'main') }}:
    packageVersion: '$(majorMinor).$(Build.BuildId)'
    isRelease: true
  ${{ else }}:
    packageVersion: '$(majorMinor).0-ci.$(Build.BuildId)'
    isRelease: false

steps:
  - script: echo "Publishing version $(packageVersion)"
    displayName: 'Display version'

  # ... build and pack steps ...

  # Promote to Release view after publishing (only for main branch)
  - task: PowerShell@2
    displayName: 'Promote to Release view'
    condition: eq(variables.isRelease, true)
    inputs:
      targetType: 'inline'
      script: |
        # Promote package to @Release view using REST API
        $org = "https://pkgs.dev.azure.com/yourorg"
        $feed = "internal-packages"
        $pkg = "MyLibrary"
        $version = "$(packageVersion)"

        $body = @{
          views = @{ op = "add"; path = "/views/-"; value = "Release" }
        } | ConvertTo-Json -Depth 5

        $uri = "$org/_apis/packaging/feeds/$feed/nuget/packages/$pkg/versions/$version?api-version=7.1-preview.1"
        Invoke-RestMethod -Uri $uri -Method Patch -Body $body `
          -ContentType "application/json" `
          -Headers @{ Authorization = "Bearer $(System.AccessToken)" }

Retention Policies

Without retention policies, feeds grow indefinitely — every CI build publishes a new pre-release version, and after a year you have thousands of packages nobody will ever use. Retention policies automatically clean up old versions to manage storage costs.

Storage Costs

Tier Storage Cost
Free tier 2 GB per organization $0
Additional storage Per GB per month ~$2/GB/month

Configuring Retention

Navigate to Feed Settings → Retention policies:

  • Maximum number of versions per package — Keep only the last N versions (e.g., 25). Older versions are automatically deleted.
  • Days to keep recently downloaded packages — Packages downloaded in the last N days are protected from cleanup regardless of version count.

Protecting Specific Versions

Retention policies won't delete packages that are:

  • Promoted to a view — Packages in @Release or @Prerelease are never auto-deleted
  • Recently downloaded — Active packages are protected by the "days since last download" rule
  • Pinned — Manually pinned versions are exempt from cleanup
Best practice: Set retention to keep 25 versions per package with 30-day download protection. Promote production-quality versions to @Release (which are never cleaned up). This keeps CI churn under control while preserving every version that matters.

Pipeline Integration

The real power of Azure Artifacts comes when packages are published and consumed automatically by pipelines — no manual steps, no credential juggling, no "it works on my machine" package versions.

Authentication Tasks

Each package type has a dedicated authentication task that injects credentials for the current pipeline run:

Package Type Auth Task What It Does
NuGet NuGetAuthenticate@1 Injects credentials into NuGet credential provider
npm npmAuthenticate@0 Writes auth tokens into .npmrc
Maven MavenAuthenticate@0 Injects credentials into Maven settings.xml
Python (pip) PipAuthenticate@1 Sets PIP_EXTRA_INDEX_URL with credentials
Python (twine) TwineAuthenticate@1 Generates .pypirc with upload credentials

Complete Pipeline: Publish & Consume

# Complete multi-stage pipeline demonstrating:
# 1. Build & publish a shared library to Azure Artifacts
# 2. Consume that library in a downstream application build

trigger:
  branches:
    include: [main]

stages:
  # ─── STAGE 1: PUBLISH LIBRARY ───────────────────────────
  - stage: PublishLibrary
    displayName: 'Publish Shared Library'
    jobs:
      - job: PackAndPublish
        pool:
          vmImage: 'ubuntu-latest'
        steps:
          - task: DotNetCoreCLI@2
            displayName: 'Build library'
            inputs:
              command: 'build'
              projects: 'src/SharedAuth/*.csproj'
              arguments: '--configuration Release'

          - task: DotNetCoreCLI@2
            displayName: 'Pack NuGet'
            inputs:
              command: 'pack'
              packagesToPack: 'src/SharedAuth/*.csproj'
              configuration: 'Release'
              versioningScheme: 'byBuildNumber'
              outputDir: '$(Build.ArtifactStagingDirectory)'

          - task: NuGetCommand@2
            displayName: 'Push to feed'
            inputs:
              command: 'push'
              packagesToPush: '$(Build.ArtifactStagingDirectory)/*.nupkg'
              nuGetFeedType: 'internal'
              publishVstsFeed: 'internal-packages'

  # ─── STAGE 2: CONSUME IN APPLICATION ────────────────────
  - stage: BuildApp
    displayName: 'Build Application (Consumes Library)'
    dependsOn: PublishLibrary
    jobs:
      - job: BuildWebApp
        pool:
          vmImage: 'ubuntu-latest'
        steps:
          # Authenticate to feed for package restore
          - task: NuGetAuthenticate@1
            displayName: 'Auth to Azure Artifacts'

          - task: DotNetCoreCLI@2
            displayName: 'Restore (includes private package)'
            inputs:
              command: 'restore'
              projects: 'src/WebApp/*.csproj'
              feedsToUse: 'config'
              nugetConfigPath: 'nuget.config'

          - task: DotNetCoreCLI@2
            displayName: 'Build application'
            inputs:
              command: 'build'
              projects: 'src/WebApp/*.csproj'
              arguments: '--configuration Release --no-restore'

          - task: DotNetCoreCLI@2
            displayName: 'Publish web app'
            inputs:
              command: 'publish'
              projects: 'src/WebApp/*.csproj'
              arguments: '--configuration Release --output $(Build.ArtifactStagingDirectory)'

          - publish: $(Build.ArtifactStagingDirectory)
            artifact: 'webapp'
            displayName: 'Upload build artifact'

Cross-Project Package Consumption

When consuming packages from a feed in a different project, ensure:

  1. The feed is organization-scoped (or the consuming project has explicit access)
  2. The consuming pipeline's build service identity has Reader role on the feed
  3. Add the feed as a service connection if using project-scoped feeds across projects

Exercises

Exercise 1 Difficulty: Intermediate

Create a Feed & Publish a NuGet Package

Goal: Create an Azure Artifacts feed, publish a NuGet package from a pipeline, and consume it in a separate project.

  1. Create an organization-scoped feed named bootcamp-feed.
  2. Create a simple .NET class library (e.g., StringUtils with a Capitalize() method).
  3. Write a pipeline that builds, packs, and publishes the NuGet package to your feed.
  4. Create a second project with a .NET console app that references StringUtils.
  5. Add a nuget.config pointing to your feed and verify dotnet restore succeeds.
  6. Call StringUtils.Capitalize("hello") and verify it returns "Hello".

Success criteria: The consuming project builds successfully using the package from your private feed. The package appears in the feed with correct version metadata.

NuGet Feeds Pipeline Publishing
Exercise 2 Difficulty: Intermediate

Configure Upstream Sources & Verify Caching

Goal: Set up upstream sources for npm, install a public package through your feed, and verify it's cached.

  1. Create a feed with npmjs.org as an upstream source (or verify it exists on your existing feed).
  2. Create a Node.js project with a .npmrc pointing exclusively to your feed.
  3. Run npm install lodash — the package should resolve through your feed.
  4. Check your feed in the Azure DevOps web UI — verify lodash appears as a cached upstream package.
  5. Temporarily disable network access to npmjs.org (or disconnect from internet) and run npm install again from a clean node_modules.
  6. The install should succeed because the package is served from your feed's cache.

Success criteria: Public packages are cached in your feed. Package installs succeed even when the upstream registry is unreachable (using the cached version).

Upstream Sources npm Caching
Exercise 3 Difficulty: Advanced

Implement Semantic Versioning with Pre-Release Promotions

Goal: Build a versioning workflow where CI publishes pre-release packages and releases are promoted to @Release view.

  1. Modify your NuGet pipeline to publish pre-release versions on feature branches (e.g., 1.0.0-ci.BuildId).
  2. Publish stable versions only from the main branch (e.g., 1.0.BuildId).
  3. After a successful main-branch build, promote the package to the @Release view using the REST API or CLI.
  4. Configure a consuming project to use the @Release view URL — verify it only sees promoted versions.
  5. Publish a pre-release version and confirm it does NOT appear in the @Release view consumer.

Success criteria: @Release view only contains explicitly promoted versions. Pre-release versions are isolated to @Local. Consumers on @Release see only stable, tested packages.

Versioning Views Promotion
Exercise 4 Difficulty: Intermediate

Retention Policies & Universal Package for ML Model

Goal: Configure retention policies to manage feed bloat, and publish a Universal Package for a large binary artifact.

  1. Configure retention on your feed: max 10 versions per package, 14-day download protection.
  2. Publish 12 versions of a test package rapidly (use a loop in a pipeline).
  3. Wait for the retention job to run (or trigger manually) — verify the oldest 2 versions are cleaned up.
  4. Promote one version to @Release and verify it's protected from retention cleanup.
  5. Create a folder with a dummy ML model file (any file > 50 MB) and publish it as a Universal Package.
  6. Download the Universal Package in a second pipeline using the UniversalPackages@0 task.

Success criteria: Retention policies keep only 10 versions (plus protected ones). The Universal Package publishes and downloads successfully regardless of file size.

Retention Universal Packages Storage

Next in the Bootcamp

In Module 10: Security & Governance, we'll cover permissions, policies, audit logs, compliance features, service connections security, secret management, and organizational governance patterns for Azure DevOps at enterprise scale.